Support for video/audio/images in folders + collectibles + media pages
This commit is contained in:
parent
e6230a47d9
commit
ec70acdf50
6
TODO.md
6
TODO.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -9,3 +9,11 @@ const { href } = Astro.props;
|
|||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
||||
<a href={href}><slot /></a>
|
||||
|
||||
{/* ------------------------------------------- CSS -------------------------------------------- */}
|
||||
|
||||
<style>
|
||||
a {
|
||||
height: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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
|
||||
/>
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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} </span>}
|
||||
<span id="title">{title} </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} </span>}
|
||||
<span id="title">{title} </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 {
|
||||
|
|
|
@ -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
|
||||
/>
|
|
@ -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")}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
/>
|
|
@ -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}`}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue