Support for video/audio/images in folders + collectibles + media pages

This commit is contained in:
DrMint 2024-04-08 13:59:09 +02:00
parent e6230a47d9
commit ec70acdf50
23 changed files with 613 additions and 104 deletions

View File

@ -3,13 +3,10 @@
## Short term
- [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
- Save cookies for longer than just the session
- [Scripts] Can't run the scripts using node (ts-node?)
- Support for nameless section
- [Timeline] Error if collectible not published?
@ -34,6 +31,7 @@
- Consider official search plugin for payload https://payloadcms.com/docs/plugins/search
- Convert Rich text to simple text for indexing and open graph purposes
- Anonymous comments
- [Images] add images group (which could be named or not)
## Bonus

View File

@ -0,0 +1,27 @@
---
import type { EndpointAudio } from "src/shared/payload/payload-sdk";
interface Props {
audio: EndpointAudio;
}
const {
audio: { url, mimeType },
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<audio controls>
<source src={url} type={mimeType} />
</audio>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
audio {
width: 100%;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
</style>

View File

@ -46,9 +46,13 @@ const { title, icon, class: className, ariaLabel, id } = Astro.props;
transition-duration: 250ms;
transition-property: padding-top, box-shadow, background-color, color, border-color;
&.with-title > svg {
width: 1.2em;
height: 1.2em;
&.with-title {
padding-right: 1.2em;
& > svg {
width: 1.2em;
height: 1.2em;
}
}
> svg {

View File

@ -0,0 +1,44 @@
---
import { getI18n } from "src/i18n/i18n";
import Button from "./Button.astro";
interface Props {
href: string;
filename: string;
}
const { href, filename } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<download-button href={href} filename={filename} class="when-js when-no-print">
<Button title={t("global.downloadButton")} icon="material-symbols:download" />
</download-button>
{/* ------------------------------------------- JS --------------------------------------------- */}
<script>
import { customElement } from "src/utils/customElements";
customElement("download-button", (elem) => {
const href = elem.getAttribute("href");
const filename = elem.getAttribute("filename");
if (!href || !filename) return;
elem.addEventListener("click", async () => {
const res = await fetch(href);
const blob = await res.blob();
const blobURL = window.URL.createObjectURL(blob);
var link = document.createElement("a");
link.download = filename;
link.href = blobURL;
link.click();
link.remove();
});
});
</script>

View File

@ -9,3 +9,11 @@ const { href } = Astro.props;
{/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={href}><slot /></a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
a {
height: fit-content;
}
</style>

View File

@ -0,0 +1,38 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointAudio } from "src/shared/payload/payload-sdk";
interface Props {
audio: EndpointAudio;
}
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const {
audio: { id, translations, tagGroups, filename, thumbnail },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
// TODO: Add this later
// const attributes = [
// {
// title: "Duration",
// icon: "material-symbols:hourglass-empty",
// values: [duration.toString()],
// },
// ];
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
title={title}
thumbnail={thumbnail}
href={getLocalizedUrl(`/audios/${id}`)}
tagGroups={tagGroups}
icon="material-symbols:music-note"
iconHoverLabel={t("global.previewTypes.audio")}
smallTitle
/>

View File

@ -7,7 +7,7 @@ interface Props {
collectible: EndpointCollectible;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const {
collectible: { slug, translations, thumbnail, tagGroups },
@ -25,5 +25,7 @@ const { title, pretitle, subtitle } = getLocalizedMatch(translations);
thumbnail={thumbnail}
href={getLocalizedUrl(`/collectibles/${slug}`)}
tagGroups={tagGroups}
icon="material-symbols:category"
iconHoverLabel={t("global.previewTypes.collectible")}
disableRoundedTop
/>

View File

@ -1,7 +1,10 @@
---
import type { EndpointTagsGroup, PayloadImage } from "src/shared/payload/payload-sdk";
import type { PayloadImage } from "src/shared/payload/payload-sdk";
import InlineTagGroups from "pages/[locale]/collectibles/_components/ContentsSection/InlineTagGroups.astro";
import Card from "components/Card.astro";
import { Icon } from "astro-icon/components";
import type { ComponentProps } from "astro/types";
import { getI18n } from "src/i18n/i18n";
interface Props {
thumbnail?: PayloadImage | undefined;
@ -9,10 +12,15 @@ interface Props {
title: string;
subtitle?: string | undefined;
href?: string | undefined;
tagGroups?: EndpointTagsGroup[];
tagGroups?: ComponentProps<typeof InlineTagGroups>["tagGroups"];
disableRoundedTop?: boolean;
smallTitle?: boolean;
icon?: string;
iconHoverLabel?: string;
}
const { t } = await getI18n(Astro.locals.currentLocale);
const {
thumbnail,
title,
@ -20,13 +28,16 @@ const {
subtitle,
href,
tagGroups = [],
smallTitle = false,
disableRoundedTop = false,
icon = "material-symbols:unknown-document",
iconHoverLabel = t("global.previewTypes.unknown"),
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<Card href={href} disableRoundedTop={disableRoundedTop}>
<Card href={href} disableRoundedTop={disableRoundedTop && thumbnail !== undefined}>
<div id="card">
{
thumbnail && (
@ -34,12 +45,22 @@ const {
)
}
<div>
<p>
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>}
</p>
<div id="icon-container" class:list={{ "thumbnail-alt": !thumbnail }} title={iconHoverLabel}>
<Icon name={icon} width={32} height={32} />
</div>
<div id="footer">
{
smallTitle ? (
<p>{title}</p>
) : (
<p>
{pretitle && <span id="pretitle">{pretitle}&nbsp;</span>}
<span id="title">{title}&nbsp;</span>
{subtitle && <span id="subtitle">{subtitle}</span>}
</p>
)
}
{
tagGroups.length > 0 && (
@ -74,20 +95,49 @@ const {
}
#card {
position: relative;
& > img {
width: 100%;
height: auto;
}
& > div {
& > #icon-container {
&.thumbnail-alt {
margin: 0.4em;
margin-bottom: unset;
aspect-ratio: 3/2;
background-color: var(--color-elevation-2);
color: var(--color-base-400);
display: grid;
place-content: center;
& > :global(svg) {
width: 64px;
height: 64px;
}
}
&:not(.thumbnail-alt) {
position: absolute;
top: 0.4em;
left: 0.4em;
padding: 0.5em;
backdrop-filter: blur(5px);
background-color: color-mix(in srgb, var(--color-elevation-2) 60%, transparent);
}
border-radius: 0.7em;
}
& > #footer {
padding: 1em;
padding-top: 1.5em;
display: flex;
flex-direction: column;
gap: 1.2em;
& > p {
line-height: 0.8;
line-height: 1;
display: grid;
overflow-wrap: anywhere;
font-size: clamp(0.5em, 0.35em + 0.75vw, 1em);
@ -96,12 +146,13 @@ const {
& > #pretitle {
font-family: var(--font-sans-serifs);
font-weight: 400;
margin-bottom: 0.8em;
margin-bottom: 0.5em;
}
& > #title {
line-height: 0.8;
font-family: var(--font-serif);
font-size: 200%;
font-size: 150%;
}
& > #subtitle {

View File

@ -0,0 +1,30 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointImage } from "src/shared/payload/payload-sdk";
interface Props {
image: EndpointImage;
}
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const {
image: thumbnail,
image: { id, translations, tagGroups, filename },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
title={title}
thumbnail={thumbnail}
href={getLocalizedUrl(`/images/${id}`)}
tagGroups={tagGroups}
icon="material-symbols:imagesmode"
iconHoverLabel={t("global.previewTypes.image")}
smallTitle
/>

View File

@ -7,7 +7,7 @@ interface Props {
page: EndpointPage;
}
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const {
page: { slug, translations, thumbnail, tagGroups },
@ -25,4 +25,6 @@ const { title, pretitle, subtitle } = getLocalizedMatch(translations);
thumbnail={thumbnail}
href={getLocalizedUrl(`/pages/${slug}`)}
tagGroups={tagGroups}
icon="material-symbols:docs"
iconHoverLabel={t("global.previewTypes.page")}
/>

View File

@ -0,0 +1,29 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointVideo } from "src/shared/payload/payload-sdk";
interface Props {
video: EndpointVideo;
}
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
const {
video: { id, translations, tagGroups, filename, thumbnail },
} = Astro.props;
const { title } = translations.length > 0 ? getLocalizedMatch(translations) : { title: filename };
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
title={title}
thumbnail={thumbnail}
href={getLocalizedUrl(`/videos/${id}`)}
tagGroups={tagGroups}
icon="material-symbols:smart-display"
iconHoverLabel={t("global.previewTypes.video")}
smallTitle
/>

View File

@ -1,6 +1,7 @@
---
import ErrorMessage from "components/ErrorMessage.astro";
import { getI18n } from "src/i18n/i18n";
import { Collections } from "src/shared/payload/payload-sdk";
interface Props {
doc: {
@ -16,10 +17,14 @@ const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
doc.relationTo === "folders" ? (
doc.relationTo === Collections.Folders ? (
<a href={getLocalizedUrl(`/folders/${doc.value.slug}`)}>
<slot />
</a>
) : doc.relationTo === Collections.Collectibles ? (
<a href={getLocalizedUrl(`/collectibles/${doc.value.slug}`)}>
<slot />
</a>
) : (
<ErrorMessage
title={`Unknown internal link: ${doc.relationTo}`}

View File

@ -1,10 +1,11 @@
---
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 AudioPlayer from "components/AudioPlayer.astro";
import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
interface Props {
node: RichTextUploadAudioNode;
@ -12,43 +13,30 @@ interface Props {
}
const {
node: {
value: { id, url, mimeType, translations },
},
node: { value },
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(translations);
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(value.translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div>
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:headphones" />
<Icon name="material-symbols:music-note" />
{title}
</HeaderTitle>
<audio controls>
<source src={url} type={mimeType} />
</audio>
<OpenMediaPageButton url={`/audios/${id}`} />
<AudioPlayer audio={value} />
<OpenMediaPageButton url={getLocalizedUrl(`/audios/${value.id}`)} />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
div {
margin-block: 4em;
& > audio {
margin-top: 0.4em;
margin-bottom: 0.2em;
width: 100%;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
display: flex;
flex-direction: column;
gap: 0.5em;
}
</style>

View File

@ -18,11 +18,11 @@ const {
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { title }: { title?: string } =
translations.length > 0 ? getLocalizedMatch(translations) : {};
const mediaPage = `/images/${id}`;
const mediaPage = getLocalizedUrl(`/images/${id}`);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -31,14 +31,12 @@ const mediaPage = `/images/${id}`;
{
title && (
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:image-outline" />
<Icon name="material-symbols:imagesmode" />
{title}
</HeaderTitle>
)
}
<a href={mediaPage}><img src={url} /></a>
<OpenMediaPageButton url={mediaPage} />
</div>
@ -47,14 +45,16 @@ const mediaPage = `/images/${id}`;
<style>
div {
margin-block: 4em;
display: flex;
flex-direction: column;
gap: 0.5em;
& > a > img {
width: 100%;
height: auto;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
margin-top: 0.4em;
margin-bottom: 0.2em;
margin-bottom: -0.5em;
}
}
</style>

View File

@ -1,9 +1,9 @@
---
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 VideoPlayer from "components/VideoPlayer.astro";
import OpenMediaPageButton from "./OpenMediaPageButton.astro";
import { getI18n } from "src/i18n/i18n";
import HeaderTitle from "components/HeaderTitle.astro";
import { Icon } from "astro-icon/components";
@ -13,34 +13,23 @@ interface Props {
}
const {
node: {
value: { id, url, thumbnail, mimeType, subtitles, translations },
},
node: { value },
context,
} = Astro.props;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(translations);
const { getLocalizedUrl, getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { title } = getLocalizedMatch(value.translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div>
<HeaderTitle header={context.depth + 2}>
<Icon name="material-symbols:movie-outline" />
<Icon name="material-symbols:smart-display" />
{title}
</HeaderTitle>
<video controls poster={thumbnail?.url}>
<source src={url} type={mimeType} />
{
subtitles.map(({ language, url }) => (
<track label={formatLocale(language)} src={url} kind="subtitles" srclang={language} />
))
}
</video>
<OpenMediaPageButton url={`/videos/${id}`} />
<VideoPlayer video={value} />
<OpenMediaPageButton url={getLocalizedUrl(`/videos/${value.id}`)} />
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
@ -48,14 +37,8 @@ const { title } = getLocalizedMatch(translations);
<style>
div {
margin-block: 4em;
& > video {
margin-top: 0.4em;
margin-bottom: 0.2em;
width: 100%;
height: auto;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
display: flex;
flex-direction: column;
gap: 0.5em;
}
</style>

View File

@ -0,0 +1,33 @@
---
import type { EndpointVideo } from "src/shared/payload/payload-sdk";
import { formatLocale } from "src/utils/format";
interface Props {
video: EndpointVideo;
}
const {
video: { url, thumbnail, mimeType, subtitles },
} = Astro.props;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<video controls poster={thumbnail?.url}>
<source src={url} type={mimeType} />
{
subtitles.map(({ language, url }) => (
<track label={formatLocale(language)} src={url} kind="subtitles" srclang={language} />
))
}
</video>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
video {
width: 100%;
border-radius: 16px;
box-shadow: 0 5px 20px -10px var(--color-shadow);
}
</style>

View File

@ -112,4 +112,11 @@ export type WordingKey =
| "global.sources.typeLabel.collectible.range.page"
| "global.sources.typeLabel.collectible.range.timestamp"
| "global.sources.typeLabel.collectible.range.custom"
| "global.openMediaPage";
| "global.openMediaPage"
| "global.downloadButton"
| "global.previewTypes.video"
| "global.previewTypes.page"
| "global.previewTypes.image"
| "global.previewTypes.audio"
| "global.previewTypes.collectible"
| "global.previewTypes.unknown";

View File

@ -0,0 +1,63 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AudioPlayer from "components/AudioPlayer.astro";
import DownloadButton from "components/DownloadButton.astro";
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 { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
const audio = await fetchOr404(() => payload.getAudioByID(id!));
if (audio instanceof Response) {
return audio;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { translations, tagGroups, filename, url } = audio;
const { title, description } = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<div id="container">
<AudioPlayer audio={audio} />
<div>
<h1>{title}</h1>
{description && <RichText content={description} />}
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
<DownloadButton href={url} filename={filename} />
</div>
</div>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 6em;
margin-top: 6em;
align-items: center;
> :global(audio) {
width: 100%;
}
h1 {
max-width: 35em;
}
div {
display: flex;
flex-direction: column;
gap: 2em;
align-items: start;
}
}
</style>

View File

@ -2,10 +2,12 @@
import ErrorMessage from "components/ErrorMessage.astro";
import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
import { Collections, type EndpointCollectible } from "src/shared/payload/payload-sdk";
import { formatInlineTitle } from "src/utils/format";
import InlineTagGroups from "./InlineTagGroups.astro";
import Card from "components/Card.astro";
import AudioPlayer from "components/AudioPlayer.astro";
import VideoPlayer from "components/VideoPlayer.astro";
interface Props {
content: EndpointCollectible["contents"][number];
@ -16,8 +18,21 @@ const {
content: { content, range },
} = Astro.props;
const href =
content.relationTo === "pages" ? getLocalizedUrl(`/pages/${content.value.slug}`) : undefined;
const href = (() => {
switch (content.relationTo) {
case Collections.Pages:
return getLocalizedUrl(`/pages/${content.value.slug}`);
case Collections.Videos:
return getLocalizedUrl(`/videos/${content.value.id}`);
case Collections.Audios:
return getLocalizedUrl(`/audios/${content.value.id}`);
default:
return undefined;
}
})();
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -26,9 +41,13 @@ const href =
<div id="row">
<div id="title">
{
content.relationTo === "generic-contents" ? (
content.relationTo === Collections.GenericContents ? (
<p>{getLocalizedMatch(content.value.translations).name}</p>
) : content.relationTo === "pages" ? (
) : content.relationTo === Collections.Pages ? (
<p>{formatInlineTitle(getLocalizedMatch(content.value.translations))}</p>
) : content.relationTo === Collections.Audios ? (
<p>{formatInlineTitle(getLocalizedMatch(content.value.translations))}</p>
) : content.relationTo === Collections.Videos ? (
<p>{formatInlineTitle(getLocalizedMatch(content.value.translations))}</p>
) : (
<ErrorMessage
@ -63,20 +82,37 @@ const href =
</div>
{
content.relationTo === "pages" && content.value.tagGroups.length > 0 && (
<div id="tags">
<InlineTagGroups tagGroups={content.value.tagGroups} />
content.relationTo === Collections.Audios ? (
<div class="media">
<AudioPlayer audio={content.value} />
</div>
) : (
content.relationTo === Collections.Videos && (
<div class="media">
<VideoPlayer video={content.value} />
</div>
)
)
}
{
(content.relationTo === Collections.Pages ||
content.relationTo === Collections.Audios ||
content.relationTo === Collections.Videos) &&
content.value.tagGroups.length > 0 && (
<div id="tags">
<InlineTagGroups tagGroups={content.value.tagGroups} />
</div>
)
}
</div>
</Card>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
:global(a > #card) > #row {
& > #title {
:global(a > #card) {
& > #row > #title {
transition-duration: 150ms;
transition-property: text-decoration-color, color;
@ -84,12 +120,12 @@ const href =
text-decoration-color: transparent;
}
&:hover > #title {
&:hover > #row > #title {
color: var(--color-base-750);
text-decoration-color: var(--color-base-650);
}
&:active > #title {
&:active > #row > #title {
color: var(--color-base-1000);
text-decoration-color: var(--color-base-1000);
}
@ -117,5 +153,11 @@ const href =
& > #tags {
grid-column: span 2;
}
& > .media {
box-sizing: border-box;
width: 100%;
grid-column: span 3;
}
}
</style>

View File

@ -4,22 +4,34 @@ import { getI18n } from "src/i18n/i18n";
import type { EndpointTagsGroup } from "src/shared/payload/payload-sdk";
interface Props {
tagGroups: EndpointTagsGroup[];
tagGroups: (EndpointTagsGroup | { title: string; icon: string; values: string[] })[];
}
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 id="tags">
{
tagGroups.map(({ icon, translations, tags }) => (
groups.map(({ icon, title, values }) => (
<div>
<Icon name={icon} />
<p>{getLocalizedMatch(translations).name}</p>
<div>{tags.map(({ translations }) => getLocalizedMatch(translations).name).join(", ")}</div>
<p>{title}</p>
<div>{values.join(", ")}</div>
</div>
))
}

View File

@ -1,6 +1,6 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import { payload } from "src/shared/payload/payload-sdk";
import { Collections, payload } from "src/shared/payload/payload-sdk";
import RichText from "components/RichText/RichText.astro";
import FoldersSection from "./_components/FoldersSection.astro";
import { fetchOr404 } from "src/utils/responses";
@ -9,6 +9,9 @@ import { getI18n } from "src/i18n/i18n";
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
import PagePreview from "components/Previews/PagePreview.astro";
import { formatRichTextToString } from "src/utils/format";
import ImagePreview from "components/Previews/ImagePreview.astro";
import AudioPreview from "components/Previews/AudioPreview.astro";
import VideoPreview from "components/Previews/VideoPreview.astro";
const { slug } = Astro.params;
@ -62,12 +65,21 @@ const meta = getLocalizedMatch(folder.translations);
{
folder.files.map(({ relationTo, value }) => {
switch (relationTo) {
case "collectibles":
case Collections.Collectibles:
return <CollectiblePreview collectible={value} />;
case "pages":
case Collections.Pages:
return <PagePreview page={value} />;
case Collections.Images:
return <ImagePreview image={value} />;
case Collections.Audios:
return <AudioPreview audio={value} />;
case Collections.Videos:
return <VideoPreview video={value} />;
default:
return (
<ErrorMessage
@ -98,7 +110,6 @@ const meta = getLocalizedMatch(folder.translations);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: clamp(6px, 2vmin, 16px);
place-items: start;
}
}
</style>

View File

@ -0,0 +1,67 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import DownloadButton from "components/DownloadButton.astro";
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 { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
const image = await fetchOr404(() => payload.getImageByID(id!));
if (image instanceof Response) {
return image;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { url, width, height, filename, translations, tagGroups } = image;
const { title, description } =
translations.length > 0
? getLocalizedMatch(translations)
: { title: filename, description: undefined };
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<div id="container">
<img src={url} width={width} height={height} />
<div>
<h1>{title}</h1>
{description && <RichText content={description} />}
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
<DownloadButton href={url} filename={filename} />
</div>
</div>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 6em;
align-items: center;
img {
max-height: 60vh;
max-width: 100%;
width: auto;
object-fit: contain;
}
h1 {
max-width: 35em;
}
div {
display: flex;
flex-direction: column;
gap: 2em;
align-items: start;
}
}
</style>

View File

@ -0,0 +1,65 @@
---
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import DownloadButton from "components/DownloadButton.astro";
import RichText from "components/RichText/RichText.astro";
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 { fetchOr404 } from "src/utils/responses";
const { id } = Astro.params;
const video = await fetchOr404(() => payload.getVideoByID(id!));
if (video instanceof Response) {
return video;
}
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { translations, tagGroups, filename, url } = video;
const { title, description } = getLocalizedMatch(translations);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppEmptyLayout>
<div id="container">
<VideoPlayer video={video} />
<div>
<h1>{title}</h1>
{description && <RichText content={description} />}
{tagGroups.length > 0 && <TagGroups {tagGroups} />}
<DownloadButton href={url} filename={filename} />
</div>
</div>
</AppEmptyLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 6em;
align-items: center;
> :global(video) {
max-height: 60vh;
max-width: 100%;
width: auto;
object-fit: contain;
}
h1 {
max-width: 35em;
}
div {
display: flex;
flex-direction: column;
gap: 2em;
align-items: start;
}
}
</style>