diff --git a/TODO.md b/TODO.md index 5254120..e4830d8 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,9 @@ - [Timeline] inline links to pages not working (timeline 2026/06) - Save cookies for longer than just the session +- [Image] media page +- [Video] media page +- [Audio] media page ## Mid term diff --git a/src/components/AppLayout/AppLayout.astro b/src/components/AppLayout/AppLayout.astro index e56f123..0c61ead 100644 --- a/src/components/AppLayout/AppLayout.astro +++ b/src/components/AppLayout/AppLayout.astro @@ -5,7 +5,6 @@ import Topbar from "./components/Topbar/Topbar.astro"; import Footer from "./components/Footer.astro"; import AppLayoutTitle from "./components/AppLayoutTitle.astro"; import type { ComponentProps } from "astro/types"; -import type { PayloadImage } from "src/shared/payload/payload-sdk"; interface Props { openGraph?: ComponentProps["openGraph"]; @@ -17,7 +16,7 @@ interface Props { illustration?: string; illustrationSize?: string; illustrationPosition?: string; - backgroundImage?: PayloadImage | undefined; + backgroundImage?: ComponentProps["img"] | undefined; hideFooterLinks?: boolean; hideHomeButton?: boolean; } diff --git a/src/components/AppLayout/components/AppLayoutBackgroundImg.astro b/src/components/AppLayout/components/AppLayoutBackgroundImg.astro index 06955da..fcd2e12 100644 --- a/src/components/AppLayout/components/AppLayoutBackgroundImg.astro +++ b/src/components/AppLayout/components/AppLayoutBackgroundImg.astro @@ -3,7 +3,7 @@ import type { PayloadImage } from "src/shared/payload/payload-sdk"; import { getRandomId } from "src/utils/random"; interface Props { - img: PayloadImage; + img: Pick; } const { diff --git a/src/components/AppLayout/components/Html.astro b/src/components/AppLayout/components/Html.astro index 33b6416..1422851 100644 --- a/src/components/AppLayout/components/Html.astro +++ b/src/components/AppLayout/components/Html.astro @@ -50,23 +50,14 @@ const { currentTheme } = Astro.locals; "light-theme": currentTheme === "light", "dark-theme": currentTheme === "dark", "texture-dots": !isIOS, - }} -> + }}> - - + + {/* Meta & OpenGraph */} @@ -80,10 +71,7 @@ const { currentTheme } = Astro.locals; - + @@ -133,9 +121,7 @@ const { currentTheme } = Astro.locals; -{ - /* ------------------------------------------- CSS -------------------------------------------- */ -} +{/* ------------------------------------------- CSS -------------------------------------------- */} -{ - /* ------------------------------------------- JS --------------------------------------------- */ -} +{/* ------------------------------------------- JS --------------------------------------------- */} diff --git a/src/components/HeaderTitle.astro b/src/components/HeaderTitle.astro new file mode 100644 index 0000000..30844e1 --- /dev/null +++ b/src/components/HeaderTitle.astro @@ -0,0 +1,53 @@ +--- +interface Props { + id?: string; + header: number; +} + +const { header, id } = Astro.props; +--- + +{/* ------------------------------------------- HTML ------------------------------------------- */} + +{ + header === 1 ? ( +

+ +

+ ) : header === 2 ? ( +

+ +

+ ) : header === 3 ? ( +

+ +

+ ) : header === 4 ? ( +

+ +

+ ) : header === 5 ? ( +
+ +
+ ) : ( +
+ +
+ ) +} + +{/* ------------------------------------------- CSS -------------------------------------------- */} + + diff --git a/src/components/Previews/CollectiblePreview.astro b/src/components/Previews/CollectiblePreview.astro index bb892fa..1973cba 100644 --- a/src/components/Previews/CollectiblePreview.astro +++ b/src/components/Previews/CollectiblePreview.astro @@ -1,10 +1,10 @@ --- import GenericPreview from "components/Previews/GenericPreview.astro"; import { getI18n } from "src/i18n/i18n"; -import type { EndpointCollectiblePreview } from "src/shared/payload/payload-sdk"; +import type { EndpointCollectible } from "src/shared/payload/payload-sdk"; interface Props { - collectible: EndpointCollectiblePreview; + collectible: EndpointCollectible; } const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); diff --git a/src/components/Previews/PagePreview.astro b/src/components/Previews/PagePreview.astro index 0bc8518..1de3033 100644 --- a/src/components/Previews/PagePreview.astro +++ b/src/components/Previews/PagePreview.astro @@ -1,10 +1,10 @@ --- import GenericPreview from "components/Previews/GenericPreview.astro"; import { getI18n } from "src/i18n/i18n"; -import type { EndpointPagePreview } from "src/shared/payload/payload-sdk"; +import type { EndpointPage } from "src/shared/payload/payload-sdk"; interface Props { - page: EndpointPagePreview; + page: EndpointPage; } const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); diff --git a/src/components/RichText/components/RTBlock/components/RTSection.astro b/src/components/RichText/components/RTBlock/components/RTSection.astro index c283141..84ac4be 100644 --- a/src/components/RichText/components/RTBlock/components/RTSection.astro +++ b/src/components/RichText/components/RTBlock/components/RTSection.astro @@ -1,4 +1,5 @@ --- +import HeaderTitle from "components/HeaderTitle.astro"; import RichText from "components/RichText/RichText.astro"; import type { RichTextSectionBlock } from "src/shared/payload/payload-sdk"; import type { RichTextContext } from "src/utils/richText"; @@ -13,34 +14,10 @@ const { node, context } = Astro.props; {/* ------------------------------------------- HTML ------------------------------------------- */} -{ - context.depth < 2 ? ( -

- {`${node.anchorHash} `} - {node.fields.blockName} -

- ) : context.depth === 2 ? ( -

- {`${node.anchorHash} `} - {node.fields.blockName} -

- ) : context.depth === 3 ? ( -

- {`${node.anchorHash} `} - {node.fields.blockName} -

- ) : context.depth === 4 ? ( -
- {`${node.anchorHash} `} - {node.fields.blockName} -
- ) : ( -
- {`${node.anchorHash} `} - {node.fields.blockName} -
- ) -} + + {`${node.anchorHash} `} + {node.fields.blockName} + @@ -51,6 +28,6 @@ const { node, context } = Astro.props; color: var(--color-base-650); font-weight: 500; font-size: 70%; - margin-right: 0.3em; + place-self: end; } diff --git a/src/components/RichText/components/RTUpload/RTUpload.astro b/src/components/RichText/components/RTUpload/RTUpload.astro index 005046b..8538189 100644 --- a/src/components/RichText/components/RTUpload/RTUpload.astro +++ b/src/components/RichText/components/RTUpload/RTUpload.astro @@ -19,9 +19,7 @@ interface Props { const { node, context } = Astro.props; --- -{ - /* ------------------------------------------- HTML ------------------------------------------- */ -} +{/* ------------------------------------------- HTML ------------------------------------------- */} { isUploadNodeImageNode(node) ? ( diff --git a/src/components/RichText/components/RTUpload/components/OpenMediaPageButton.astro b/src/components/RichText/components/RTUpload/components/OpenMediaPageButton.astro new file mode 100644 index 0000000..8ecc068 --- /dev/null +++ b/src/components/RichText/components/RTUpload/components/OpenMediaPageButton.astro @@ -0,0 +1,38 @@ +--- +import { Icon } from "astro-icon/components"; +import { getI18n } from "src/i18n/i18n"; + +interface Props { + url: string; +} + +const { url } = Astro.props; +const { t } = await getI18n(Astro.locals.currentLocale); +--- + +{/* ------------------------------------------- HTML ------------------------------------------- */} + + + +{/* ------------------------------------------- CSS -------------------------------------------- */} + + diff --git a/src/components/RichText/components/RTUpload/components/RTAudio.astro b/src/components/RichText/components/RTUpload/components/RTAudio.astro index 8459229..8d7d289 100644 --- a/src/components/RichText/components/RTUpload/components/RTAudio.astro +++ b/src/components/RichText/components/RTUpload/components/RTAudio.astro @@ -1,32 +1,54 @@ --- import { type RichTextUploadAudioNode } from "src/shared/payload/payload-sdk"; import type { RichTextContext } from "src/utils/richText"; +import OpenMediaPageButton from "./OpenMediaPageButton.astro"; +import { getI18n } from "src/i18n/i18n"; +import HeaderTitle from "components/HeaderTitle.astro"; +import { Icon } from "astro-icon/components"; interface Props { node: RichTextUploadAudioNode; context: RichTextContext; } -const { node } = Astro.props; +const { + node: { + value: { id, url, mimeType, translations }, + }, + context, +} = Astro.props; + +const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale); +const { title } = getLocalizedMatch(translations); --- -{ - /* ------------------------------------------- HTML ------------------------------------------- */ -} +{/* ------------------------------------------- HTML ------------------------------------------- */} - +
+ + + {title} + -{ - /* ------------------------------------------- CSS -------------------------------------------- */ -} + + + +
+ +{/* ------------------------------------------- CSS -------------------------------------------- */} diff --git a/src/components/RichText/components/RTUpload/components/RTImage.astro b/src/components/RichText/components/RTUpload/components/RTImage.astro index 2e83f7a..dc9864b 100644 --- a/src/components/RichText/components/RTUpload/components/RTImage.astro +++ b/src/components/RichText/components/RTUpload/components/RTImage.astro @@ -1,27 +1,60 @@ --- import { type RichTextUploadImageNode } from "src/shared/payload/payload-sdk"; import type { RichTextContext } from "src/utils/richText"; +import OpenMediaPageButton from "./OpenMediaPageButton.astro"; +import { getI18n } from "src/i18n/i18n"; +import HeaderTitle from "components/HeaderTitle.astro"; +import { Icon } from "astro-icon/components"; interface Props { node: RichTextUploadImageNode; context: RichTextContext; } -const { node } = Astro.props; +const { + node: { + value: { id, url, translations }, + }, + context, +} = Astro.props; + +const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale); +const { title }: { title?: string } = + translations.length > 0 ? getLocalizedMatch(translations) : {}; + +const mediaPage = `/images/${id}`; --- {/* ------------------------------------------- HTML ------------------------------------------- */} - +
+ { + title && ( + + + {title} + + ) + } + + + + +
{/* ------------------------------------------- CSS -------------------------------------------- */} diff --git a/src/components/RichText/components/RTUpload/components/RTVideo.astro b/src/components/RichText/components/RTUpload/components/RTVideo.astro index 99a90ad..285aa17 100644 --- a/src/components/RichText/components/RTUpload/components/RTVideo.astro +++ b/src/components/RichText/components/RTUpload/components/RTVideo.astro @@ -1,29 +1,61 @@ --- +import { getI18n } from "src/i18n/i18n"; import { type RichTextUploadVideoNode } from "src/shared/payload/payload-sdk"; +import { formatLocale } from "src/utils/format"; import type { RichTextContext } from "src/utils/richText"; +import OpenMediaPageButton from "./OpenMediaPageButton.astro"; +import HeaderTitle from "components/HeaderTitle.astro"; +import { Icon } from "astro-icon/components"; interface Props { node: RichTextUploadVideoNode; context: RichTextContext; } -const { node } = Astro.props; +const { + node: { + value: { id, url, thumbnail, mimeType, subtitles, translations }, + }, + context, +} = Astro.props; + +const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale); +const { title } = getLocalizedMatch(translations); --- {/* ------------------------------------------- HTML ------------------------------------------- */} - +
+ + + {title} + + + + + +
{/* ------------------------------------------- CSS -------------------------------------------- */} diff --git a/src/i18n/wordings-keys.ts b/src/i18n/wordings-keys.ts index ce368e1..fc4f406 100644 --- a/src/i18n/wordings-keys.ts +++ b/src/i18n/wordings-keys.ts @@ -111,4 +111,5 @@ export type WordingKey = | "global.sources.typeLabel.folder" | "global.sources.typeLabel.collectible.range.page" | "global.sources.typeLabel.collectible.range.timestamp" - | "global.sources.typeLabel.collectible.range.custom"; + | "global.sources.typeLabel.collectible.range.custom" + | "global.openMediaPage"; diff --git a/src/pages/[locale]/folders/_components/FoldersSection.astro b/src/pages/[locale]/folders/_components/FoldersSection.astro index ae7a8bf..0fca06d 100644 --- a/src/pages/[locale]/folders/_components/FoldersSection.astro +++ b/src/pages/[locale]/folders/_components/FoldersSection.astro @@ -1,11 +1,11 @@ --- -import type { EndpointFolderPreview } from "src/shared/payload/payload-sdk"; +import type { EndpointFolder } from "src/shared/payload/payload-sdk"; import FolderCard from "./FolderCard.astro"; import { getI18n } from "src/i18n/i18n"; interface Props { title?: string | undefined; - folders: EndpointFolderPreview[]; + folders: EndpointFolder[]; } const { title, folders } = Astro.props; diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index a240cda..a1171ad 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -17,10 +17,8 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale); openGraph={{ title: t("home.title") }} backgroundImage={{ url: "/img/background-image.webp", - filename: "background-image", height: 2279, width: 1920, - mimeType: "image/webp", }} hideFooterLinks hideHomeButton> diff --git a/src/pages/[locale]/timeline/index.astro b/src/pages/[locale]/timeline/index.astro index 70c591e..9795a05 100644 --- a/src/pages/[locale]/timeline/index.astro +++ b/src/pages/[locale]/timeline/index.astro @@ -21,10 +21,8 @@ const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.cu diff --git a/src/shared/payload/payload-sdk.ts b/src/shared/payload/payload-sdk.ts index 96de979..46ea1b6 100644 --- a/src/shared/payload/payload-sdk.ts +++ b/src/shared/payload/payload-sdk.ts @@ -551,7 +551,6 @@ export interface Audio { }; [k: string]: unknown; } | null; - subfile?: string | VideoSubtitle | null; id?: string | null; }[]; tags?: (string | Tag)[] | null; @@ -597,19 +596,6 @@ export interface MediaThumbnail { }; }; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "videos-subtitles". - */ -export interface VideoSubtitle { - id: string; - url?: string | null; - filename?: string | null; - mimeType?: string | null; - filesize?: number | null; - width?: number | null; - height?: number | null; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "videos". @@ -617,7 +603,7 @@ export interface VideoSubtitle { export interface Video { id: string; duration: number; - thumbnail: string | MediaThumbnail; + thumbnail?: string | MediaThumbnail | null; translations: { language: string | Language; title: string; @@ -658,6 +644,19 @@ export interface Video { width?: number | null; height?: number | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "videos-subtitles". + */ +export interface VideoSubtitle { + id: string; + url?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "videos-channels". @@ -666,7 +665,7 @@ export interface VideosChannel { id: string; url: string; title: string; - subscribers?: number | null; + subscribers: number; videos?: (string | Video)[] | null; } /** @@ -1099,17 +1098,17 @@ export interface RichTextUploadNode extends RichTextNode { export interface RichTextUploadImageNode extends RichTextUploadNode { relationTo: Collections.Images; - value: Image; + value: EndpointImage; } export interface RichTextUploadVideoNode extends RichTextUploadNode { relationTo: Collections.Videos; - value: Video; + value: EndpointVideo; } export interface RichTextUploadAudioNode extends RichTextUploadNode { relationTo: Collections.Audios; - value: Audio; + value: EndpointAudio; } export interface RichTextTextNode extends RichTextNode { @@ -1179,10 +1178,10 @@ export const isNodeUploadNode = (node: RichTextNode): node is RichTextUploadNode export const isUploadNodeImageNode = (node: RichTextUploadNode): node is RichTextUploadImageNode => node.relationTo === Collections.Images; - export const isUploadNodeVideoNode = (node: RichTextUploadNode): node is RichTextUploadVideoNode => +export const isUploadNodeVideoNode = (node: RichTextUploadNode): node is RichTextUploadVideoNode => node.relationTo === Collections.Videos; - export const isUploadNodeAudioNode = (node: RichTextUploadNode): node is RichTextUploadAudioNode => +export const isUploadNodeAudioNode = (node: RichTextUploadNode): node is RichTextUploadAudioNode => node.relationTo === Collections.Audios; export const isNodeListNode = (node: RichTextNode): node is RichTextListNode => @@ -1338,7 +1337,7 @@ const request = async (url: string, init?: RequestInit): Promise => { // SDK and Types -export type EndpointFolderPreview = { +export type EndpointFolder = { slug: string; icon?: string; translations: { @@ -1346,33 +1345,42 @@ export type EndpointFolderPreview = { name: string; description?: RichTextContent; }[]; -}; - -export type EndpointFolder = EndpointFolderPreview & { sections: - | { type: "single"; subfolders: EndpointFolderPreview[] } + | { type: "single"; subfolders: EndpointFolder[] } | { type: "multiple"; sections: { translations: { language: string; name: string }[]; - subfolders: EndpointFolderPreview[]; + subfolders: EndpointFolder[]; }[]; }; files: ( | { - relationTo: "collectibles"; - value: EndpointCollectiblePreview; + relationTo: Collections.Collectibles; + value: EndpointCollectible; } | { - relationTo: "pages"; - value: EndpointPagePreview; + relationTo: Collections.Pages; + value: EndpointPage; + } + | { + relationTo: Collections.Images; + value: EndpointImage; + } + | { + relationTo: Collections.Audios; + value: EndpointAudio; + } + | { + relationTo: Collections.Videos; + value: EndpointVideo; } )[]; parentPages: EndpointSource[]; }; export type EndpointWebsiteConfig = { - homeFolders: (EndpointFolderPreview & { + homeFolders: (EndpointFolder & { lightThumbnail?: PayloadImage; darkThumbnail?: PayloadImage; })[]; @@ -1420,23 +1428,18 @@ export type EndpointTagsGroup = { tags: EndpointTag[]; }; -export type EndpointPagePreview = { +export type EndpointPage = { slug: string; type: PageType; thumbnail?: PayloadImage; authors: EndpointRecorder[]; tagGroups: EndpointTagsGroup[]; + backgroundImage?: PayloadImage; translations: { language: string; pretitle?: string; title: string; subtitle?: string; - }[]; -}; - -export type EndpointPage = EndpointPagePreview & { - backgroundImage?: PayloadImage; - translations: (EndpointPagePreview["translations"][number] & { sourceLanguage: string; summary?: RichTextContent; content: RichTextContent; @@ -1444,11 +1447,11 @@ export type EndpointPage = EndpointPagePreview & { translators: EndpointRecorder[]; proofreaders: EndpointRecorder[]; toc: TableOfContentEntry[]; - })[]; + }[]; parentPages: EndpointSource[]; }; -export type EndpointCollectiblePreview = { +export type EndpointCollectible = { slug: string; thumbnail?: PayloadImage; translations: { @@ -1461,9 +1464,6 @@ export type EndpointCollectiblePreview = { tagGroups: EndpointTagsGroup[]; releaseDate?: string; languages: string[]; -}; - -export type EndpointCollectible = EndpointCollectiblePreview & { backgroundImage?: PayloadImage; nature: CollectibleNature; gallery: PayloadImage[]; @@ -1484,15 +1484,23 @@ export type EndpointCollectible = EndpointCollectiblePreview & { bindingType?: CollectibleBindingTypes; pageOrder?: CollectiblePageOrders; }; - subitems: EndpointCollectiblePreview[]; + subitems: EndpointCollectible[]; contents: { content: | { - relationTo: "pages"; - value: EndpointPagePreview; + relationTo: Collections.Pages; + value: EndpointPage; } | { - relationTo: "generic-contents"; + relationTo: Collections.Audios; + value: EndpointAudio; + } + | { + relationTo: Collections.Videos; + value: EndpointVideo; + } + | { + relationTo: Collections.GenericContents; value: { translations: { language: string; @@ -1557,21 +1565,72 @@ export type EndpointSource = | { type: "url"; url: string; label: string } | { type: "collectible"; - collectible: EndpointCollectiblePreview; + collectible: EndpointCollectible; range?: | { type: "page"; page: number } | { type: "timestamp"; timestamp: string } | { type: "custom"; translations: { language: string; note: string }[] }; } - | { type: "page"; page: EndpointPagePreview } - | { type: "folder"; folder: EndpointFolderPreview }; + | { type: "page"; page: EndpointPage } + | { type: "folder"; folder: EndpointFolder }; -export type PayloadImage = { +export type EndpointMedia = { + id: string; url: string; + filename: string; + mimeType: string; + filesize: number; + updatedAt: string; + createdAt: string; + tagGroups: EndpointTagsGroup[]; + translations: { + language: string; + title: string; + description?: RichTextContent; + }[]; +}; + +export type EndpointImage = EndpointMedia & { width: number; height: number; +}; + +export type EndpointAudio = EndpointMedia & { + thumbnail?: PayloadImage; + duration: number; +}; + +export type EndpointVideo = EndpointMedia & { + thumbnail?: PayloadImage; + subtitles: { + language: string; + url: string; + }[]; + platform?: { + channel: { + url: string; + title: string; + subscribers: number; + }; + views?: number; + likes?: number; + dislikes?: number; + url: string; + publishedDate: string; + }; + duration: number; +}; + +export type PayloadMedia = { + url: string; mimeType: string; filename: string; + filesize: number; +}; + +export type PayloadImage = PayloadMedia & { + width: number; + height: number; }; export const payload = { @@ -1595,4 +1654,10 @@ export const payload = { await (await request(payloadApiUrl(Collections.ChronologyEvents, `all`))).json(), getChronologyEventByID: async (id: string): Promise => await (await request(payloadApiUrl(Collections.ChronologyEvents, `id/${id}`))).json(), + getImageByID: async (id: string): Promise => + await (await request(payloadApiUrl(Collections.Images, `id/${id}`))).json(), + getAudioByID: async (id: string): Promise => + await (await request(payloadApiUrl(Collections.Audios, `id/${id}`))).json(), + getVideoByID: async (id: string): Promise => + await (await request(payloadApiUrl(Collections.Videos, `id/${id}`))).json(), };