Added files support in folders and collectibles

This commit is contained in:
DrMint 2024-06-22 21:21:56 +02:00
parent 66ac5bd519
commit e854d88d89
21 changed files with 682 additions and 202 deletions

View File

@ -8,6 +8,8 @@ PAYLOAD_USER=myemail@domain.com
PAYLOAD_PASSWORD=somepassword123 PAYLOAD_PASSWORD=somepassword123
WEB_HOOK_TOKEN=webhookd5e6ea45ef4e66eaa151612bdcb599df WEB_HOOK_TOKEN=webhookd5e6ea45ef4e66eaa151612bdcb599df
ENABLE_PRECACHING=true
## OPEN EXCHANGE RATE ## OPEN EXCHANGE RATE
OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df OER_APP_ID=oerappid5e6ea45ef4e66eaa151612bdcb599df

View File

@ -9,6 +9,7 @@
## Short term ## Short term
- [Bugs] On android Chrome, the setting button in the header flashes for a few ms when the page is loading
- [Feat] [caching] Use getURLs for precaching + precache everything - [Feat] [caching] Use getURLs for precaching + precache everything
- [Bugs] Make sure uploads name are slug-like and with an extension. - [Bugs] Make sure uploads name are slug-like and with an extension.
- [Bugs] Nyupun can't upload subtitles files - [Bugs] Nyupun can't upload subtitles files
@ -42,6 +43,8 @@
## Long term ## Long term
- [Feat] Invalidate Back/Forward Cache when changing language/theme/currency
- [Feat] Hovering on a preview card could give a more detailed summary/preview (with all attributes)
- [Feat] Explore posibilities for View Transitions - [Feat] Explore posibilities for View Transitions
- [Feat] Revemp theme system using light-dark https://caniuse.com/mdn-css_types_color_light-dark - [Feat] Revemp theme system using light-dark https://caniuse.com/mdn-css_types_color_light-dark
- [Feat] Add reduce motion to element that zoom when hovering - [Feat] Add reduce motion to element that zoom when hovering

View File

@ -1,6 +1,6 @@
{ {
"name": "v3.accords-library.com", "name": "v3.accords-library.com",
"version": "3.0.0-beta.5", "version": "3.0.0-beta.6",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",

View File

@ -5,16 +5,21 @@ import Button from "./Button.astro";
interface Props { interface Props {
href: string; href: string;
filename: string; filename: string;
useBlob?: boolean;
} }
const { href, filename } = Astro.props; const { href, filename, useBlob = false } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale); const { t } = await getI18n(Astro.locals.currentLocale);
--- ---
{/* ------------------------------------------- HTML ------------------------------------------- */} {/* ------------------------------------------- HTML ------------------------------------------- */}
<download-button href={href} filename={filename} class="when-js when-no-print"> <download-button
href={href}
filename={filename}
class="when-js when-no-print"
data-use-blob={useBlob}>
<Button title={t("global.downloadButton")} icon="material-symbols:download" /> <Button title={t("global.downloadButton")} icon="material-symbols:download" />
</download-button> </download-button>
@ -26,17 +31,23 @@ const { t } = await getI18n(Astro.locals.currentLocale);
customElement("download-button", (elem) => { customElement("download-button", (elem) => {
const href = elem.getAttribute("href"); const href = elem.getAttribute("href");
const filename = elem.getAttribute("filename"); const filename = elem.getAttribute("filename");
const useBlob = elem.hasAttribute("data-use-blob");
if (!href || !filename) return; if (!href || !filename) return;
elem.addEventListener("click", async () => { elem.addEventListener("click", async () => {
const res = await fetch(href); let url;
const blob = await res.blob(); if (useBlob) {
const blobURL = window.URL.createObjectURL(blob); const res = await fetch(href);
const blob = await res.blob();
url = window.URL.createObjectURL(blob);
} else {
url = href;
}
var link = document.createElement("a"); var link = document.createElement("a");
link.download = filename; link.download = filename;
link.href = blobURL; link.href = url;
link.click(); link.click();
link.remove(); link.remove();
}); });

View File

@ -108,7 +108,7 @@ const hasNavigation = previousImageHref || nextImageHref;
{attributes.length > 0 && <Attributes attributes={attributes} />} {attributes.length > 0 && <Attributes attributes={attributes} />}
{credits.length > 0 && <Credits credits={credits} />} {credits.length > 0 && <Credits credits={credits} />}
{metaAttributes.length > 0 && <Attributes attributes={metaAttributes} />} {metaAttributes.length > 0 && <Attributes attributes={metaAttributes} />}
{filename && <DownloadButton href={url} filename={filename} />} {filename && <DownloadButton href={url} filename={filename} useBlob />}
</div> </div>
</div> </div>

View File

@ -0,0 +1,71 @@
---
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointFilePreview } from "src/shared/payload/payload-sdk";
import { getFileIcon } from "src/utils/attributes";
interface Props {
file: EndpointFilePreview;
}
const { getLocalizedMatch, getLocalizedUrl, t, formatFilesize } = await getI18n(
Astro.locals.currentLocale
);
const {
file: { id, translations, attributes, filename, thumbnail, mimeType, filesize },
} = Astro.props;
const { pretitle, title, subtitle, language } =
translations.length > 0
? getLocalizedMatch(translations)
: { pretitle: undefined, title: filename, subtitle: undefined, language: undefined };
const hasTitle = title !== filename;
const attributesWithMeta = [
...attributes,
...(hasTitle
? [
{
title: t("global.media.attributes.filename"),
icon: "material-symbols:unknown-document",
values: [{ name: filename }],
},
]
: []),
{
title: t("global.media.attributes.filesize"),
icon: "material-symbols:hard-drive",
values: [{ name: formatFilesize(filesize) }],
},
];
const getFileTypeLabel = (): string => {
switch (mimeType) {
case "application/zip":
return t("global.previewTypes.zip");
case "application/pdf":
return t("global.previewTypes.pdf");
default:
return mimeType;
}
};
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<GenericPreview
pretitle={pretitle}
title={title}
subtitle={subtitle}
lang={language}
thumbnail={thumbnail}
href={getLocalizedUrl(`/files/${id}`)}
attributes={attributesWithMeta}
icon={getFileIcon(mimeType)}
iconHoverLabel={getFileTypeLabel()}
smallTitle={!hasTitle}
/>

View File

@ -147,4 +147,7 @@ export type WordingKey =
| "collectibles.nature" | "collectibles.nature"
| "collectibles.languages" | "collectibles.languages"
| "collectibles.nature.physical" | "collectibles.nature.physical"
| "collectibles.nature.digital"; | "collectibles.nature.digital"
| "global.previewTypes.zip"
| "global.previewTypes.pdf"
| "collectibles.files";

8
src/icons/image-file.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="32" height="32" viewBox="0 0 24 24" version="1.1" id="svg1" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<path fill="currentColor"
d="M 6,22 C 5.45,22 4.979,21.804 4.587,21.412 4.195,21.02 3.9993333,20.549333 4,20 V 4 C 4,3.45 4.196,2.979 4.588,2.587 4.98,2.195 5.4506667,1.9993333 6,2 h 8 l 6,6 v 12 c 0,0.55 -0.196,1.021 -0.588,1.413 C 19.02,21.805 18.549333,22.000667 18,22 Z M 13,9 h 5 L 13,4 Z m -5.8880933,9.669723 h 9.5724213 l -2.991381,-3.98851 -2.393106,3.190808 -1.7948288,-2.393105 z M 9.1061613,13.08581 c 0.332376,0 0.6150281,-0.116465 0.8479576,-0.349393 0.2329281,-0.23293 0.3491271,-0.515317 0.3485951,-0.84716 0,-0.332376 -0.116464,-0.615029 -0.3493937,-0.847957 -0.2329286,-0.232929 -0.5153151,-0.349128 -0.847159,-0.348596 -0.3323761,0 -0.6150282,0.116464 -0.8479571,0.349393 -0.232929,0.23293 -0.3491278,0.515316 -0.3485957,0.84716 0,0.332376 0.1164644,0.615028 0.3493934,0.847957 0.2329289,0.232929 0.5153154,0.349128 0.8471594,0.348596 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

8
src/icons/pdf-file.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="32" height="32" viewBox="0 0 24 24" version="1.1" id="svg1" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<path fill="currentColor"
d="M 6,22 C 5.45,22 4.979,21.804 4.587,21.412 4.195,21.02 3.9993333,20.549333 4,20 V 4 C 4,3.45 4.196,2.979 4.588,2.587 4.98,2.195 5.4506667,1.9993333 6,2 h 8 l 6,6 v 12 c 0,0.55 -0.196,1.021 -0.588,1.413 C 19.02,21.805 18.549333,22.000667 18,22 Z M 13,9 h 5 L 13,4 Z m -6.299094,9.065324 h 1 v -2 h 1 c 0.283333,0 0.521,-0.096 0.713,-0.288 0.192,-0.192 0.287667,-0.429333 0.287,-0.712 v -1 c 0,-0.283334 -0.096,-0.521 -0.288,-0.713 -0.192,-0.192 -0.429333,-0.287667 -0.712,-0.287 h -2 z m 1,-3 v -1 h 1 v 1 z m 3,3 h 2 c 0.283333,0 0.520999,-0.096 0.712999,-0.288 0.192,-0.192 0.287667,-0.429333 0.287,-0.712 v -3 c 0,-0.283334 -0.096,-0.521 -0.288,-0.713 -0.192,-0.192 -0.429332,-0.287667 -0.711999,-0.287 h -2 z m 1,-1 v -3 h 1 v 3 z m 2.999999,1 h 1 v -2 h 1 v -1 h -1 v -1 h 1 v -1 h -2 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -10,6 +10,7 @@ import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { getFileIcon } from "src/utils/attributes";
const id = Astro.params.id!; const id = Astro.params.id!;
const audio = await fetchOr404(() => payload.getAudioByID(id)); const audio = await fetchOr404(() => payload.getAudioByID(id));
@ -30,6 +31,7 @@ const {
createdAt, createdAt,
updatedAt, updatedAt,
thumbnail, thumbnail,
mimeType,
} = audio; } = audio;
const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations); const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations);
@ -39,7 +41,7 @@ const metaAttributes = [
? [ ? [
{ {
title: t("global.media.attributes.filename"), title: t("global.media.attributes.filename"),
icon: "material-symbols:audio-file", icon: getFileIcon(mimeType),
values: [{ name: filename }], values: [{ name: filename }],
withBorder: false, withBorder: false,
}, },

View File

@ -0,0 +1,52 @@
---
import FilePreview from "components/Previews/FilePreview.astro";
import TitleIcon from "components/TitleIcon.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointCollectible } from "src/shared/payload/payload-sdk";
interface Props {
files: EndpointCollectible["files"];
}
const { files } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="container">
<TitleIcon title={t("collectibles.files")} icon="material-symbols:file-save" />
<div id="values">
{files.map((file) => <FilePreview file={file} />)}
</div>
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
margin-top: 6em;
display: flex;
flex-direction: column;
gap: 2em;
& > #values {
display: grid;
gap: clamp(6px, 2vmin, 16px);
grid-template-columns: repeat(auto-fill, 270px);
@media (max-width: 600.5px) {
grid-template-columns: 1fr 1fr;
row-gap: 12px;
}
@media (max-width: 24rem) {
grid-template-columns: 1fr;
}
align-items: start;
}
}
</style>

View File

@ -5,6 +5,7 @@ import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { getFileIcon } from "src/utils/attributes";
const slug = Astro.params.slug!; const slug = Astro.params.slug!;
const index = Astro.params.index!; const index = Astro.params.index!;
@ -18,7 +19,7 @@ if (galleryImage instanceof Response) {
} }
const { parentPages, previousIndex, nextIndex, image } = galleryImage; const { parentPages, previousIndex, nextIndex, image } = galleryImage;
const { filename, translations, createdAt, updatedAt, credits, attributes } = image; const { filename, translations, createdAt, updatedAt, credits, attributes, mimeType } = image;
const { pretitle, title, subtitle, description, language } = const { pretitle, title, subtitle, description, language } =
translations.length > 0 translations.length > 0
@ -36,7 +37,7 @@ const metaAttributes = [
? [ ? [
{ {
title: t("global.media.attributes.filename"), title: t("global.media.attributes.filename"),
icon: "material-symbols:unknown-document", icon: getFileIcon(mimeType),
values: [{ name: filename }], values: [{ name: filename }],
withBorder: false, withBorder: false,
}, },

View File

@ -19,6 +19,7 @@ import type { Attribute } from "src/utils/attributes";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { sizesToSrcset } from "src/utils/img"; import { sizesToSrcset } from "src/utils/img";
import RichText from "components/RichText/RichText.astro"; import RichText from "components/RichText/RichText.astro";
import SubFilesSection from "./_components/SubFilesSection.astro";
const slug = Astro.params.slug!; const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl, t, formatDate, formatPrice } = await getI18n( const { getLocalizedMatch, getLocalizedUrl, t, formatDate, formatPrice } = await getI18n(
@ -43,6 +44,7 @@ const {
gallery, gallery,
scans, scans,
subitems, subitems,
files,
parentPages, parentPages,
attributes, attributes,
contents, contents,
@ -221,7 +223,7 @@ if (price) {
</Fragment> </Fragment>
{subitems.length > 0 && <SubitemSection subitems={subitems} />} {subitems.length > 0 && <SubitemSection subitems={subitems} />}
{files.length > 0 && <SubFilesSection files={files} />}
{contents.length > 0 && <ContentsSection contents={contents} />} {contents.length > 0 && <ContentsSection contents={contents} />}
</AsideLayout> </AsideLayout>
</AppLayout> </AppLayout>

View File

@ -0,0 +1,200 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Attributes from "components/Attributes.astro";
import Credits from "components/Credits.astro";
import DownloadButton from "components/DownloadButton.astro";
import RichText from "components/RichText/RichText.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
import { getFileIcon } from "src/utils/attributes";
import { Icon } from "astro-icon/components";
import { sizesToSrcset } from "src/utils/img";
const id = Astro.params.id!;
const video = await fetchOr404(() => payload.getFileByID(id));
if (video instanceof Response) {
return video;
}
const { getLocalizedMatch, t, formatFilesize, formatDate } = await getI18n(
Astro.locals.currentLocale
);
const {
translations,
attributes,
filename,
url,
credits,
mimeType,
filesize,
updatedAt,
createdAt,
thumbnail,
} = video;
const { pretitle, title, subtitle, description, language } =
translations.length > 0
? getLocalizedMatch(translations)
: {
pretitle: undefined,
title: filename,
subtitle: undefined,
description: undefined,
language: undefined,
};
const metaAttributes = [
...(filename && title !== filename && thumbnail
? [
{
title: t("global.media.attributes.filename"),
icon: getFileIcon(mimeType),
values: [{ name: filename }],
withBorder: false,
},
]
: []),
{
title: t("global.media.attributes.filesize"),
icon: "material-symbols:hard-drive",
values: [{ name: formatFilesize(filesize) }],
withBorder: false,
},
{
title: t("global.media.attributes.createdAt"),
icon: "material-symbols:calendar-add-on",
values: [{ name: formatDate(new Date(createdAt)) }],
withBorder: false,
},
{
title: t("global.media.attributes.updatedAt"),
icon: "material-symbols:edit-calendar",
values: [{ name: formatDate(new Date(updatedAt)) }],
withBorder: false,
},
];
const smallTitle = title === filename;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
}}>
<div id="container">
{
thumbnail ? (
<a
id="image-anchor"
href={url}
target="_blank"
style={`aspect-ratio:${thumbnail.width}/${thumbnail.height};`}>
<img
src={url}
srcset={sizesToSrcset(thumbnail.sizes)}
sizes={`(max-aspect-ratio: ${thumbnail.width / 0.9}/${thumbnail.height / 0.7}) 90vw, ${(thumbnail.width / thumbnail.height) * 70}vh`}
width={thumbnail.width}
height={thumbnail.height}
/>
</a>
) : (
<a href={url} id="icon-container">
<Icon name={getFileIcon(mimeType)} width={32} height={32} />
<p>{filename}</p>
</a>
)
}
<div id="info">
{
smallTitle ? (
<h1 class="font-4xl" lang={language}>
{title}
</h1>
) : (
<AppLayoutTitle pretitle={pretitle} title={title} subtitle={subtitle} lang={language} />
)
}
{description && <RichText content={description} context={{ lang: language }} />}
{attributes.length > 0 && <Attributes attributes={attributes} />}
{credits.length > 0 && <Credits credits={credits} />}
{metaAttributes.length > 0 && <Attributes attributes={metaAttributes} />}
{filename && <DownloadButton href={url} filename={filename} />}
</div>
</div>
</AppLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#container {
display: flex;
flex-direction: column;
gap: 6em;
align-items: center;
h1 {
max-width: 35em;
}
& > #image-anchor {
transition: 100ms scale;
box-shadow: 0 5px 20px -10px var(--color-shadow-0);
&:hover,
&:focus-visible {
scale: 102%;
}
max-height: 70vh;
}
& > #icon-container {
display: grid;
place-content: center;
place-items: center;
gap: 0.5em;
border-radius: 0.7em;
padding: 3em;
box-sizing: border-box;
margin: 0.4em;
max-width: 35em;
width: 100%;
aspect-ratio: 3/2;
background-color: var(--color-elevation-2);
color: var(--color-base-400);
text-align: center;
& > svg {
width: clamp(16px, 25vw, 96px);
height: clamp(16px, 25vw, 96px);
}
transition: 100ms scale;
&:hover,
&:focus-visible {
scale: 102%;
}
}
& > #info {
display: flex;
flex-direction: column;
gap: 4em;
align-items: start;
@media (max-width: 35rem) {
gap: 6em;
}
}
}
</style>

View File

@ -14,6 +14,7 @@ import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro"; import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import RichText from "components/RichText/RichText.astro"; import RichText from "components/RichText/RichText.astro";
import FilePreview from "components/Previews/FilePreview.astro";
const slug = Astro.params.slug!; const slug = Astro.params.slug!;
@ -74,6 +75,9 @@ const meta = getLocalizedMatch(folder.translations);
case Collections.Videos: case Collections.Videos:
return <VideoPreview video={value} />; return <VideoPreview video={value} />;
case Collections.Files:
return <FilePreview file={value} />;
default: default:
return ( return (
<ErrorMessage <ErrorMessage

View File

@ -5,6 +5,7 @@ import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { getFileIcon } from "src/utils/attributes";
const id = Astro.params.id!; const id = Astro.params.id!;
const image = await fetchOr404(() => payload.getImageByID(id)); const image = await fetchOr404(() => payload.getImageByID(id));
@ -13,7 +14,7 @@ if (image instanceof Response) {
} }
const { getLocalizedMatch, formatDate, t } = await getI18n(Astro.locals.currentLocale); const { getLocalizedMatch, formatDate, t } = await getI18n(Astro.locals.currentLocale);
const { filename, translations, attributes, credits, createdAt, updatedAt } = image; const { filename, translations, attributes, credits, createdAt, updatedAt, mimeType } = image;
const { pretitle, title, subtitle, description, language } = const { pretitle, title, subtitle, description, language } =
translations.length > 0 translations.length > 0
@ -31,7 +32,7 @@ const metaAttributes = [
? [ ? [
{ {
title: t("global.media.attributes.filename"), title: t("global.media.attributes.filename"),
icon: "material-symbols:unknown-document", icon: getFileIcon(mimeType),
values: [{ name: filename }], values: [{ name: filename }],
withBorder: false, withBorder: false,
}, },

View File

@ -10,6 +10,7 @@ import { getI18n } from "src/i18n/i18n";
import { payload } from "src/utils/payload"; import { payload } from "src/utils/payload";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format"; import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses"; import { fetchOr404 } from "src/utils/responses";
import { getFileIcon } from "src/utils/attributes";
const id = Astro.params.id!; const id = Astro.params.id!;
const video = await fetchOr404(() => payload.getVideoByID(id)); const video = await fetchOr404(() => payload.getVideoByID(id));
@ -30,6 +31,7 @@ const {
updatedAt, updatedAt,
createdAt, createdAt,
thumbnail, thumbnail,
mimeType,
} = video; } = video;
const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations); const { pretitle, title, subtitle, description, language } = getLocalizedMatch(translations);
@ -39,7 +41,7 @@ const metaAttributes = [
? [ ? [
{ {
title: t("global.media.attributes.filename"), title: t("global.media.attributes.filename"),
icon: "material-symbols:video-file", icon: getFileIcon(mimeType),
values: [{ name: filename }], values: [{ name: filename }],
withBorder: false, withBorder: false,
}, },

View File

@ -1,177 +1,177 @@
{ {
"disclaimer": "Usage subject to terms: https://openexchangerates.org/terms", "disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
"license": "https://openexchangerates.org/license", "license": "https://openexchangerates.org/license",
"timestamp": 1718769601, "timestamp": 1719061206,
"base": "USD", "base": "USD",
"rates": { "rates": {
"AED": 3.673, "AED": 3.673,
"AFN": 70.163141, "AFN": 70.649788,
"ALL": 93.552377, "ALL": 93.642272,
"AMD": 388.086509, "AMD": 387.350187,
"ANG": 1.802556, "ANG": 1.798477,
"AOA": 854.494667, "AOA": 854.5,
"ARS": 905.759291, "ARS": 904.605803,
"AUD": 1.501458, "AUD": 1.50355,
"AWG": 1.8, "AWG": 1.8,
"AZN": 1.7, "AZN": 1.7,
"BAM": 1.821501, "BAM": 1.827253,
"BBD": 2, "BBD": 2,
"BDT": 117.292047, "BDT": 117.251367,
"BGN": 1.8212, "BGN": 1.827253,
"BHD": 0.376891, "BHD": 0.376512,
"BIF": 2869.980338, "BIF": 2869.433797,
"BMD": 1, "BMD": 1,
"BND": 1.35155, "BND": 1.351988,
"BOB": 6.897451, "BOB": 6.894941,
"BRL": 5.4413, "BRL": 5.4321,
"BSD": 1, "BSD": 1,
"BTC": 0.000015288954, "BTC": 0.000015559783,
"BTN": 83.247452, "BTN": 83.329274,
"BWP": 13.517364, "BWP": 13.475058,
"BYN": 3.26637, "BYN": 3.265661,
"BZD": 2.012145, "BZD": 2.011398,
"CAD": 1.372048, "CAD": 1.36985,
"CDF": 2835.224044, "CDF": 2821.505656,
"CHF": 0.884319, "CHF": 0.893684,
"CLF": 0.033902, "CLF": 0.033937,
"CLP": 935.45, "CLP": 940.97,
"CNH": 7.273883, "CNH": 7.2877,
"CNY": 7.2559, "CNY": 7.2613,
"COP": 4122.284393, "COP": 4163.3111,
"CRC": 525.542692, "CRC": 521.605154,
"CUC": 1, "CUC": 1,
"CUP": 25.75, "CUP": 25.75,
"CVE": 102.693429, "CVE": 103.017701,
"CZK": 23.131, "CZK": 23.3152,
"DJF": 177.73558, "DJF": 177.670831,
"DKK": 6.947223, "DKK": 6.9744,
"DOP": 59.183449, "DOP": 58.896627,
"DZD": 134.598577, "DZD": 134.486,
"EGP": 47.7088, "EGP": 47.71,
"ERN": 15, "ERN": 15,
"ETB": 57.520491, "ETB": 57.579015,
"EUR": 0.931299, "EUR": 0.935235,
"FJD": 2.25895, "FJD": 2.24275,
"FKP": 0.786911, "FKP": 0.790451,
"GBP": 0.786911, "GBP": 0.790451,
"GEL": 2.84, "GEL": 2.805,
"GGP": 0.786911, "GGP": 0.790451,
"GHS": 15.02333, "GHS": 15.118419,
"GIP": 0.786911, "GIP": 0.790451,
"GMD": 67.75, "GMD": 67.75,
"GNF": 8593.158878, "GNF": 8590.530533,
"GTQ": 7.745895, "GTQ": 7.743261,
"GYD": 208.758212, "GYD": 208.651413,
"HKD": 7.808162, "HKD": 7.80515,
"HNL": 25.078995, "HNL": 24.683515,
"HRK": 7.016854, "HRK": 7.0464,
"HTG": 132.267879, "HTG": 132.26513,
"HUF": 366.849287, "HUF": 370.73,
"IDR": 16349.509717, "IDR": 16477.55,
"ILS": 3.716955, "ILS": 3.7596,
"IMP": 0.786911, "IMP": 0.790451,
"INR": 83.381051, "INR": 83.565401,
"IQD": 1306.660001, "IQD": 1307.235516,
"IRR": 42087.5, "IRR": 42087.5,
"ISK": 139.04, "ISK": 139.4,
"JEP": 0.786911, "JEP": 0.790451,
"JMD": 155.440387, "JMD": 155.407057,
"JOD": 0.7089, "JOD": 0.7087,
"JPY": 157.859, "JPY": 159.68497365,
"KES": 129.27, "KES": 129.228764,
"KGS": 87.6016, "KGS": 86.7587,
"KHR": 4112.767316, "KHR": 4113.574874,
"KMF": 458.849846, "KMF": 460.549891,
"KPW": 900, "KPW": 900,
"KRW": 1382.002686, "KRW": 1389.39,
"KWD": 0.306682, "KWD": 0.306432,
"KYD": 0.831866, "KYD": 0.831597,
"KZT": 459.016771, "KZT": 464.455765,
"LAK": 21889.130162, "LAK": 21935.263396,
"LBP": 89643.929605, "LBP": 89360.676,
"LKR": 304.461947, "LKR": 304.713417,
"LRD": 193.81036, "LRD": 193.69365,
"LSL": 18.097846, "LSL": 17.898504,
"LYD": 4.840137, "LYD": 4.83767,
"MAD": 9.975878, "MAD": 9.938151,
"MDL": 17.828505, "MDL": 17.800024,
"MGA": 4470.377749, "MGA": 4518.181995,
"MKD": 57.384069, "MKD": 57.490542,
"MMK": 2096.43, "MMK": 2096.43,
"MNT": 3450, "MNT": 3450,
"MOP": 8.027251, "MOP": 8.035984,
"MRU": 39.448914, "MRU": 39.296492,
"MUR": 46.666698, "MUR": 46.840006,
"MVR": 15.395, "MVR": 15.4,
"MWK": 1730.787269, "MWK": 1730.176733,
"MXN": 18.4099, "MXN": 18.1108,
"MYR": 4.708, "MYR": 4.713,
"MZN": 63.885, "MZN": 63.899991,
"NAD": 18.097846, "NAD": 17.97,
"NGN": 1484.33, "NGN": 1488,
"NIO": 36.741079, "NIO": 36.735646,
"NOK": 10.573511, "NOK": 10.5565,
"NPR": 133.44253, "NPR": 133.326482,
"NZD": 1.63038, "NZD": 1.633854,
"OMR": 0.384941, "OMR": 0.384941,
"PAB": 1, "PAB": 1,
"PEN": 3.789035, "PEN": 3.797543,
"PGK": 3.890178, "PGK": 3.892185,
"PHP": 58.671002, "PHP": 58.831495,
"PKR": 278.046054, "PKR": 277.9113,
"PLN": 4.041156, "PLN": 4.044728,
"PYG": 7507.477573, "PYG": 7511.504736,
"QAR": 3.640673, "QAR": 3.639557,
"RON": 4.6337, "RON": 4.6525,
"RSD": 109.022, "RSD": 109.468038,
"RUB": 85.747634, "RUB": 88.982683,
"RWF": 1305.577777, "RWF": 1310.225337,
"SAR": 3.751877, "SAR": 3.751791,
"SBD": 8.471937, "SBD": 8.454445,
"SCR": 13.937, "SCR": 14.149709,
"SDG": 601, "SDG": 601,
"SEK": 10.433372, "SEK": 10.5075,
"SGD": 1.350881, "SGD": 1.3552,
"SHP": 0.786911, "SHP": 0.790451,
"SLL": 20969.5, "SLL": 20969.5,
"SOS": 571.352636, "SOS": 570.280724,
"SRD": 31.231, "SRD": 30.797,
"SSP": 130.26, "SSP": 130.26,
"STD": 22281.8, "STD": 22281.8,
"STN": 22.817654, "STN": 22.889709,
"SVC": 8.734971, "SVC": 8.731723,
"SYP": 2512.53, "SYP": 2512.53,
"SZL": 18.092071, "SZL": 17.88917,
"THB": 36.730254, "THB": 36.666511,
"TJS": 10.681083, "TJS": 10.607558,
"TMT": 3.5, "TMT": 3.51,
"TND": 3.125274, "TND": 3.12907,
"TOP": 2.361474, "TOP": 2.359788,
"TRY": 32.617701, "TRY": 32.83,
"TTD": 6.781034, "TTD": 6.784802,
"TWD": 32.385, "TWD": 32.37465,
"TZS": 2615, "TZS": 2619.42356,
"UAH": 40.591421, "UAH": 40.439198,
"UGX": 3711.377309, "UGX": 3741.782229,
"USD": 1, "USD": 1,
"UYU": 39.353278, "UYU": 39.304902,
"UZS": 12604.759961, "UZS": 12612.753681,
"VES": 36.348426, "VES": 36.327958,
"VND": 25451.768898, "VND": 25458.250123,
"VUV": 118.722, "VUV": 118.722,
"WST": 2.8, "WST": 2.8,
"XAF": 610.891881, "XAF": 613.473942,
"XAG": 0.03397836, "XAG": 0.03384954,
"XAU": 0.00042947, "XAU": 0.00043086,
"XCD": 2.70255, "XCD": 2.70255,
"XDR": 0.757639, "XDR": 0.757322,
"XOF": 610.891881, "XOF": 613.473942,
"XPD": 0.00113672, "XPD": 0.00109708,
"XPF": 111.133493, "XPF": 111.603222,
"XPT": 0.00102682, "XPT": 0.00101001,
"YER": 250.350066, "YER": 250.349961,
"ZAR": 18.039756, "ZAR": 17.96396,
"ZMW": 25.779293, "ZMW": 25.421591,
"ZWL": 322 "ZWL": 322
} }
} }

View File

@ -30,6 +30,7 @@ export interface Config {
videos: Video; videos: Video;
"videos-subtitles": VideoSubtitle; "videos-subtitles": VideoSubtitle;
"videos-channels": VideosChannel; "videos-channels": VideosChannel;
files: File;
scans: Scan; scans: Scan;
tags: Tag; tags: Tag;
attributes: Attribute; attributes: Attribute;
@ -414,6 +415,10 @@ export interface Folder {
relationTo: "audios"; relationTo: "audios";
value: string | Audio; value: string | Audio;
} }
| {
relationTo: "files";
value: string | File;
}
)[] )[]
| null; | null;
updatedAt: string; updatedAt: string;
@ -536,9 +541,8 @@ export interface Collectible {
bindingType?: ("Paperback" | "Hardcover") | null; bindingType?: ("Paperback" | "Hardcover") | null;
pageOrder?: ("Left to right" | "Right to left") | null; pageOrder?: ("Left to right" | "Right to left") | null;
}; };
folders?: (string | Folder)[] | null;
parentItems?: (string | Collectible)[] | null;
subitems?: (string | Collectible)[] | null; subitems?: (string | Collectible)[] | null;
files?: (string | File)[] | null;
contents?: contents?:
| { | {
content: content:
@ -603,6 +607,8 @@ export interface Collectible {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
folders?: (string | Folder)[] | null;
parentItems?: (string | Collectible)[] | null;
updatedBy: string | Recorder; updatedBy: string | Recorder;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
@ -676,49 +682,35 @@ export interface Currency {
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "generic-contents". * via the `definition` "files".
*/ */
export interface GenericContent { export interface File {
id: string; id: string;
name: string;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "audios".
*/
export interface Audio {
id: string;
duration: number;
thumbnail?: string | MediaThumbnail | null; thumbnail?: string | MediaThumbnail | null;
translations: { translations?:
language: string | Language; | {
pretitle?: string | null; language: string | Language;
title: string; pretitle?: string | null;
subtitle?: string | null; title: string;
description?: { subtitle?: string | null;
root: { description?: {
type: string; root: {
children: { type: string;
type: string; children: {
version: number; type: string;
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
[k: string]: unknown; [k: string]: unknown;
}[]; } | null;
direction: ("ltr" | "rtl") | null; id?: string | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | ""; }[]
indent: number; | null;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[];
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
credits?: Credits; credits?: Credits;
updatedAt: string; updatedAt: string;
@ -823,6 +815,64 @@ export interface MediaThumbnail {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "generic-contents".
*/
export interface GenericContent {
id: string;
name: string;
translations: {
language: string | Language;
name: string;
id?: string | null;
}[];
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "audios".
*/
export interface Audio {
id: string;
duration: number;
thumbnail?: string | MediaThumbnail | null;
translations: {
language: string | Language;
pretitle?: string | null;
title: string;
subtitle?: string | null;
description?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[];
attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null;
credits?: Credits;
updatedAt: string;
createdAt: string;
url?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "videos". * via the `definition` "videos".
@ -1200,6 +1250,7 @@ export enum Collections {
Collectibles = "collectibles", Collectibles = "collectibles",
CreditsRole = "credits-roles", CreditsRole = "credits-roles",
Currencies = "currencies", Currencies = "currencies",
Files = "files",
Folders = "folders", Folders = "folders",
GenericContents = "generic-contents", GenericContents = "generic-contents",
Images = "images", Images = "images",
@ -1548,6 +1599,10 @@ export type EndpointFolder = EndpointFolderPreview & {
relationTo: Collections.Videos; relationTo: Collections.Videos;
value: EndpointVideoPreview; value: EndpointVideoPreview;
} }
| {
relationTo: Collections.Files;
value: EndpointFilePreview;
}
)[]; )[];
parentPages: EndpointSource[]; parentPages: EndpointSource[];
}; };
@ -1717,6 +1772,7 @@ export type EndpointCollectible = EndpointCollectiblePreview & {
pageOrder?: CollectiblePageOrders; pageOrder?: CollectiblePageOrders;
}; };
subitems: EndpointCollectiblePreview[]; subitems: EndpointCollectiblePreview[];
files: EndpointFilePreview[];
contents: { contents: {
content: content:
| { | {
@ -1992,6 +2048,16 @@ export type EndpointVideo = EndpointMedia & {
duration: number; duration: number;
}; };
export type EndpointFilePreview = EndpointMediaPreview & {
filesize: number;
thumbnail?: EndpointPayloadImage;
};
export type EndpointFile = EndpointMedia & {
filesize: number;
thumbnail?: EndpointPayloadImage;
};
export type EndpointPayloadImage = PayloadImage & { export type EndpointPayloadImage = PayloadImage & {
sizes: PayloadImage[]; sizes: PayloadImage[];
openGraph?: PayloadImage; openGraph?: PayloadImage;
@ -2017,6 +2083,7 @@ export type EndpointAllPaths = {
videos: string[]; videos: string[];
audios: string[]; audios: string[];
images: string[]; images: string[];
files: string[];
recorders: string[]; recorders: string[];
chronologyEvents: string[]; chronologyEvents: string[];
}; };
@ -2059,6 +2126,7 @@ export const getSDKEndpoint = {
getImageByIDEndpoint: (id: string) => `/${Collections.Images}/id/${id}`, getImageByIDEndpoint: (id: string) => `/${Collections.Images}/id/${id}`,
getAudioByIDEndpoint: (id: string) => `/${Collections.Audios}/id/${id}`, getAudioByIDEndpoint: (id: string) => `/${Collections.Audios}/id/${id}`,
getVideoByIDEndpoint: (id: string) => `/${Collections.Videos}/id/${id}`, getVideoByIDEndpoint: (id: string) => `/${Collections.Videos}/id/${id}`,
getFileByIDEndpoint: (id: string) => `/${Collections.Files}/id/${id}`,
getRecorderByIDEndpoint: (id: string) => `/${Collections.Recorders}/id/${id}`, getRecorderByIDEndpoint: (id: string) => `/${Collections.Recorders}/id/${id}`,
getAllPathsEndpoint: () => `/all-paths`, getAllPathsEndpoint: () => `/all-paths`,
getLoginEndpoint: () => `/${Collections.Recorders}/login`, getLoginEndpoint: () => `/${Collections.Recorders}/login`,
@ -2153,6 +2221,8 @@ export const getPayloadSDK = ({
await request(getSDKEndpoint.getAudioByIDEndpoint(id)), await request(getSDKEndpoint.getAudioByIDEndpoint(id)),
getVideoByID: async (id: string): Promise<EndpointVideo> => getVideoByID: async (id: string): Promise<EndpointVideo> =>
await request(getSDKEndpoint.getVideoByIDEndpoint(id)), await request(getSDKEndpoint.getVideoByIDEndpoint(id)),
getFileByID: async (id: string): Promise<EndpointFile> =>
await request(getSDKEndpoint.getFileByIDEndpoint(id)),
getRecorderByID: async (id: string): Promise<EndpointRecorder> => getRecorderByID: async (id: string): Promise<EndpointRecorder> =>
await request(getSDKEndpoint.getRecorderByIDEndpoint(id)), await request(getSDKEndpoint.getRecorderByIDEndpoint(id)),
getAllPaths: async (): Promise<EndpointAllPaths> => getAllPaths: async (): Promise<EndpointAllPaths> =>

View File

@ -5,3 +5,28 @@ export type Attribute = {
values: { name: string; href?: string | undefined; lang?: string | undefined }[]; values: { name: string; href?: string | undefined; lang?: string | undefined }[];
withBorder?: boolean | undefined; withBorder?: boolean | undefined;
}; };
export const getFileIcon = (mimeType: string): string => {
const firstPart = mimeType.split("/")[0];
switch (firstPart) {
case "video":
return "material-symbols:video-file";
case "image":
return "image-file";
case "audio":
return "material-symbols:audio-file";
}
switch (mimeType) {
case "application/zip":
return "material-symbols:folder-zip";
case "application/pdf":
return "pdf-file";
}
return "material-symbols:unknown-document";
};

View File

@ -11,6 +11,8 @@ let expiration: number | undefined = undefined;
const responseCache = new Map<string, any>(); const responseCache = new Map<string, any>();
const idsCacheMap = new Map<string, Set<string>>(); const idsCacheMap = new Map<string, Set<string>>();
const isPrecachingEnabled = import.meta.env.ENABLE_PRECACHING === "true";
export const payload = getPayloadSDK({ export const payload = getPayloadSDK({
apiURL: import.meta.env.PAYLOAD_API_URL, apiURL: import.meta.env.PAYLOAD_API_URL,
email: import.meta.env.PAYLOAD_USER, email: import.meta.env.PAYLOAD_USER,
@ -36,7 +38,7 @@ export const payload = getPayloadSDK({
const cachedResponse = responseCache.get(url); const cachedResponse = responseCache.get(url);
if (cachedResponse) { if (cachedResponse) {
console.log("[ResponseCaching] Retrieved cache response for", url); console.log("[ResponseCaching] Retrieved cache response for", url);
return cachedResponse; return structuredClone(cachedResponse);
} }
}, },
set: (url, response) => { set: (url, response) => {
@ -118,6 +120,11 @@ export const refreshWebsiteConfig = async () => {
let payloadInitialized = false; let payloadInitialized = false;
export const initPayload = async () => { export const initPayload = async () => {
if (!payloadInitialized) { if (!payloadInitialized) {
if (!isPrecachingEnabled) {
payloadInitialized = true;
return;
}
const result = await payload.getAllPaths(); const result = await payload.getAllPaths();
for (const slug of result.pages) { for (const slug of result.pages) {
@ -182,6 +189,14 @@ export const initPayload = async () => {
} }
} }
for (const id of result.files) {
try {
await payload.getFileByID(id);
} catch (e) {
console.warn("[Precaching] Couldn't precache file", id, e);
}
}
try { try {
await payload.getChronologyEvents(); await payload.getChronologyEvents();
} catch (e) { } catch (e) {