Added relations pages to collectibles and pages

This commit is contained in:
DrMint 2024-08-16 09:05:48 +02:00
parent 1a6e0b315b
commit 403f7e087d
36 changed files with 1228 additions and 709 deletions

View File

@ -6,10 +6,14 @@
- [Bugs] Keziah reported some lag spikes when scrolling on the home page (Firefox on Windows)
- [Feat] [Analytics] Add analytics
- [Bugs] [Tooltips] Tooltip in under next element (example in timeline)
- [Bugs] [Language override] Maso actor is not focusable with keyboard nav
- [Bugs] [KeyboardNav]:
- Maso actor is not focusable with keyboard nav
- Parent pages not focusable
- Search button is double-focusable (once the link, and one the button)
## Short term
- [Feat] Add links to all the timeline image and document on Timeline page
- [Bugs] Make sure uploads name are slug-like and with an extension.
- [Bugs] Nyupun can't upload subtitles files
- [Bugs] https://v3.accords-library.com/en/collectibles/dod-original-soundtrack/scans obi is way too big

1250
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,20 +19,20 @@
"node": ">=19.7.0"
},
"dependencies": {
"@astrojs/check": "^0.9.1",
"@astrojs/node": "^8.3.2",
"accept-language": "^3.0.18",
"astro": "4.13.0",
"@astrojs/check": "^0.9.2",
"@astrojs/node": "^8.3.3",
"accept-language": "^3.0.20",
"astro": "4.14.0",
"astro-icon": "^1.1.0",
"tippy.js": "^6.3.7",
"ua-parser-js": "^1.0.38"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.1.85",
"@iconify-json/material-symbols": "^1.1.87",
"@types/ua-parser-js": "^0.7.39",
"astro-meta-tags": "^0.3.0",
"autoprefixer": "^10.4.19",
"postcss-preset-env": "^9.6.0",
"autoprefixer": "^10.4.20",
"postcss-preset-env": "^10.0.1",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^5.5.4"

View File

@ -195,22 +195,16 @@ export class PageCache {
return [`/folders/${change.slug}`];
case SDKEndpointNames.getCollectible:
return [`/collectibles/${change.slug}`];
return [`/collectibles/${change.slug}`, `/collectibles/${change.slug}/relations`];
case SDKEndpointNames.getCollectibleGallery:
return [`/collectibles/${change.slug}/gallery`];
// case SDKEndpointNames.getCollectibleGalleryImage:
// return [`/collectibles/${change.slug}/gallery/${change.index}`];
case SDKEndpointNames.getCollectibleScans:
return [`/collectibles/${change.slug}/scans`];
// case SDKEndpointNames.getCollectibleScanPage:
// return [`/collectibles/${change.slug}/scans/${change.index}`];
case SDKEndpointNames.getPage:
return [`/pages/${change.slug}`];
return [`/pages/${change.slug}`, `/pages/${change.slug}/relations`];
case SDKEndpointNames.getAudioByID:
return [`/audios/${change.id}`];

View File

@ -4,25 +4,20 @@ import Topbar from "./components/Topbar/Topbar.astro";
import Footer from "./components/Footer.astro";
import AppLayoutBackgroundImg from "./components/AppLayoutBackgroundImg.astro";
import type { ComponentProps } from "astro/types";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
openGraph?: ComponentProps<typeof Html>["openGraph"];
backlinks?: EndpointRelation[];
backgroundImage?: ComponentProps<typeof AppLayoutBackgroundImg>["img"] | undefined;
hideFooterLinks?: boolean;
hideHomeButton?: boolean;
hideSearchButton?: boolean;
class?: string | undefined;
topBar?: ComponentProps<typeof Topbar>;
}
const {
openGraph,
backlinks,
backgroundImage,
hideFooterLinks = false,
hideHomeButton = false,
hideSearchButton = false,
topBar = {},
...otherProps
} = Astro.props;
---
@ -32,11 +27,7 @@ const {
<Html openGraph={openGraph}>
{backgroundImage && <AppLayoutBackgroundImg img={backgroundImage} />}
<header>
<Topbar
backlinks={backlinks}
hideHomeButton={hideHomeButton}
hideSearchButton={hideSearchButton}
/>
<Topbar {...topBar} />
</header>
<main {...otherProps.class ? otherProps : {}}><slot /></main>
<Footer withLinks={!hideFooterLinks} />

View File

@ -105,6 +105,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
grid-template-columns: auto auto;
gap: 0.2em 1em;
place-content: start;
place-items: start;
flex-grow: 1;
@media (min-width: 720.5px) {

View File

@ -674,10 +674,14 @@ const isIOS = parser.getOS().name === "iOS";
display: flex;
place-items: center;
gap: 0.4em;
padding: 0.7em 0.8em;
padding: 0.7em 1.1em;
border-radius: 9999px;
cursor: pointer;
&:has(svg) {
padding-left: 0.8em;
}
transition: 150ms background-color;
&:hover,

View File

@ -4,17 +4,23 @@ import Button from "components/Button.astro";
import ThemeSelector from "./components/ThemeSelector.astro";
import LanguageSelector from "./components/LanguageSelector.astro";
import CurrencySelector from "./components/CurrencySelector.astro";
import ParentPagesButton from "./components/ParentPagesButton.astro";
import RelationsButton from "./components/RelationsButton.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
backlinks?: EndpointRelation[] | undefined;
hideHomeButton?: boolean;
hideSearchButton?: boolean;
relations?: EndpointRelation[] | undefined;
relationPageUrl?: string | undefined;
hideHomeButton?: boolean | undefined;
hideSearchButton?: boolean | undefined;
}
const { backlinks = [], hideHomeButton = false, hideSearchButton = false } = Astro.props;
const {
relations = [],
relationPageUrl,
hideHomeButton = false,
hideSearchButton = false,
} = Astro.props;
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
@ -23,14 +29,16 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<nav id="topbar" class="when-no-print">
{
(!hideHomeButton || backlinks.length > 0) && (
(!hideHomeButton || relations.length > 0) && (
<div id="left" class="hide-scrollbar">
<a href={getLocalizedUrl("")} class="pressable-label">
<Icon name="material-symbols:home" width={16} height={16} />
<p>{t("home.title")}</p>
</a>
{backlinks.length > 0 && <ParentPagesButton backlinks={backlinks} />}
{relations.length > 0 && (
<RelationsButton relations={relations} relationPageUrl={relationPageUrl} />
)}
</div>
)
}

View File

@ -1,59 +0,0 @@
---
import Tooltip from "components/Tooltip.astro";
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import ReturnToButton from "./ReturnToButton.astro";
import RelationRow from "components/RelationRow.astro";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
backlinks: EndpointRelation[];
}
const { backlinks } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
backlinks.length === 1 && backlinks[0] ? (
<ReturnToButton relation={backlinks[0]} />
) : (
<Tooltip trigger="click" class="when-js">
<div id="tooltip-content" slot="tooltip-content">
<p>{t("header.nav.parentPages.tooltip")}</p>
<div>
{backlinks.map((relation) => (
<RelationRow relation={relation} />
))}
</div>
</div>
<button class="pressable-label">
<Icon name="material-symbols:keyboard-return" />
<p>
{t("header.nav.parentPages.label", {
count: backlinks.length,
})}
</p>
</button>
</Tooltip>
)
}
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#tooltip-content {
> p {
margin-bottom: 1em;
}
> div {
display: flex;
flex-direction: column;
gap: 0.8em;
}
}
</style>

View File

@ -0,0 +1,87 @@
---
import Tooltip from "components/Tooltip.astro";
import { Icon } from "astro-icon/components";
import { getI18n } from "src/i18n/i18n";
import ReturnToButton from "./ReturnToButton.astro";
import RelationRow from "components/RelationRow.astro";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
relations: EndpointRelation[];
relationPageUrl?: string | undefined;
}
const { relations, relationPageUrl } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
const buttonLabel = t("header.nav.parentPages.label", {
count: relations.length,
});
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
{
relations.length === 1 && relations[0] ? (
<ReturnToButton relation={relations[0]} />
) : (
<>
<Tooltip trigger="click" class="when-js">
<div id="tooltip-content" slot="tooltip-content">
<p>{t("header.nav.parentPages.tooltip")}</p>
<div>
{relations.slice(0, 5).map((relation) => (
<RelationRow relation={relation} />
))}
{relationPageUrl && (
<>
<hr />
<a href={relationPageUrl} class="pressable-link">
{t("header.nav.parentPages.tooltip.viewAll")}
</a>
</>
)}
</div>
</div>
<div class="pressable-label">
<Icon name="material-symbols:keyboard-return" />
<p>{buttonLabel}</p>
</div>
</Tooltip>
{relationPageUrl && (
<a class="pressable-label when-no-js" href={relationPageUrl}>
<Icon name="material-symbols:keyboard-return" />
<div>{buttonLabel}</div>
</a>
)}
</>
)
}
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#tooltip-content {
> p {
margin-bottom: 1em;
}
> div {
display: flex;
flex-direction: column;
gap: 0.8em;
& > hr {
border: none;
border-top: 2px dotted var(--color-base-500);
margin-top: 8px;
}
& > .pressable-link {
padding: 4px 10px;
margin: -4px -10px;
}
}
}
</style>

View File

@ -0,0 +1,37 @@
---
import Button from "components/Button.astro";
import Card from "components/Card.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointRelation } from "src/shared/payload/endpoint-types";
interface Props {
parent: EndpointRelation;
}
const { parent } = Astro.props;
const { formatEndpointRelation, t } = await getI18n(Astro.locals.currentLocale);
const { href, target, label } = formatEndpointRelation(parent);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<Card class="card_subpage">
<p class="font-l">{t("global.subpageCard.message", { title: label })}</p>
<a href={href} target={target}>
<Button title={t("global.subpageCard.returnButton")} icon="material-symbols:keyboard-return" />
</a>
</Card>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
.card_subpage {
padding: 2em;
display: flex;
flex-direction: column;
gap: 1em;
width: fit-content;
place-items: start;
}
</style>

View File

@ -43,6 +43,6 @@ const getTruncatedText = () => {
title={getTruncatedText()}
href={getLocalizedUrl(`/timeline#${formatTimelineDateToId(date)}`)}
icon="material-symbols:calendar-month"
iconHoverLabel={t("global.collections.chronologyEvents", { count: 1 })}
iconHoverLabel={t("global.collections.chronologyEvents")}
smallTitle
/>

View File

@ -2,35 +2,38 @@
import GenericPreview from "components/Previews/GenericPreview.astro";
import { getI18n } from "src/i18n/i18n";
import { Collections } from "src/shared/payload/constants";
import type { EndpointFolder } from "src/shared/payload/endpoint-types";
import type { EndpointFolder, EndpointFolderPreview } from "src/shared/payload/endpoint-types";
import type { Attribute } from "src/utils/attributes";
interface Props {
folder: EndpointFolder;
folder: EndpointFolder | EndpointFolderPreview;
}
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
const {
folder: { translations, slug, files, sections, backlinks },
} = Astro.props;
const { folder } = Astro.props;
const { language, title } = getLocalizedMatch(translations);
const { language, title } = getLocalizedMatch(folder.translations);
const fileCount = files.length;
const attributes: Attribute[] = [];
const subfolderCount =
sections.type === "single"
? sections.subfolders.length
: sections.sections.reduce((acc, section) => acc + section.subfolders.length, 0);
if ("files" in folder) {
const { backlinks, files, sections } = folder;
const attributes: Attribute[] = [
{
const fileCount = files.length;
const subfolderCount =
sections.type === "single"
? sections.subfolders.length
: sections.sections.reduce((acc, section) => acc + section.subfolders.length, 0);
attributes.push({
icon: "material-symbols:box",
title: t("global.folders.attributes.content.label"),
values: [{ name: t("global.folders.attributes.content.value", { fileCount, subfolderCount }) }],
},
{
});
attributes.push({
icon: "material-symbols:keyboard-return",
title: t("global.folders.attributes.parent"),
values: backlinks.flatMap((link) => {
@ -38,8 +41,8 @@ const attributes: Attribute[] = [
const name = getLocalizedMatch(link.value.translations).title;
return { name };
}),
},
];
});
}
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -47,7 +50,7 @@ const attributes: Attribute[] = [
<GenericPreview
title={title}
lang={language}
href={getLocalizedUrl(`/folders/${slug}`)}
href={getLocalizedUrl(`/folders/${folder.slug}`)}
attributes={attributes}
icon="material-symbols:folder-open"
iconHoverLabel={t("global.collections.folders", { count: 1 })}

View File

@ -22,34 +22,45 @@ const {
{/* ------------------------------------------- HTML ------------------------------------------- */}
<a href={href} target={target} rel={rel}>
<div class="font-xs">{typeLabel}</div><p lang={lang}>{label}</p>
<p>
<span id="type" class="font-xs">{typeLabel}</span><span id="label" lang={lang}>{label}</span>
</p>
</a>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
a {
display: flex;
flex-wrap: wrap;
place-items: center;
gap: 0.1em 0.3em;
& > p {
text-decoration: underline dotted 0.1em;
text-decoration-color: transparent;
transition-duration: 150ms;
transition-property: text-decoration-color, color;
line-height: 1.2;
}
border-radius: 16px;
padding: 4px 10px;
margin: -4px -10px;
&:hover > p,
&:focus-visible > p {
& > p {
display: flex;
place-items: start;
gap: 0.1em 0.3em;
& > #label {
text-decoration: underline dotted 0.1em;
text-decoration-color: transparent;
transition-duration: 150ms;
transition-property: text-decoration-color, color;
line-height: 1.2;
}
& > #type {
background-color: var(--color-base-300);
border-radius: 9999px;
padding: 0.3em 0.6em;
flex-shrink: 0;
margin-left: -0.5em;
}
}
&:hover > p > #label,
&:focus-visible > p > #label {
color: var(--color-base-750);
text-decoration-color: var(--color-base-650);
}
@ -58,17 +69,9 @@ const {
outline-width: 1.5 px;
}
&:active > p {
&:active > p > #label {
color: var(--color-base-650);
text-decoration-color: var(--color-base-550);
}
& > div {
background-color: var(--color-base-300);
border-radius: 9999px;
padding: 0.3em 0.6em;
flex-shrink: 0;
margin-left: -0.5em;
}
}
</style>

View File

@ -0,0 +1,140 @@
---
import ChronologyEventPreview from "components/Previews/ChronologyEventPreview.astro";
import CollectiblePreview from "components/Previews/CollectiblePreview.astro";
import FolderPreview from "components/Previews/FolderPreview.astro";
import { Collections } from "src/shared/payload/constants";
import type {
EndpointChronologyEvent,
EndpointCollectiblePreview,
EndpointFolderPreview,
EndpointRelation,
} from "src/shared/payload/endpoint-types";
import ReturnToParentCard from "./AppLayout/components/Topbar/components/ReturnToParentCard.astro";
import AppLayoutTitle from "./AppLayout/components/AppLayoutTitle.astro";
import { getI18n } from "src/i18n/i18n";
import AppLayout from "./AppLayout/AppLayout.astro";
interface Props {
parentPage: EndpointRelation;
backlinks: EndpointRelation[];
}
const { backlinks, parentPage } = Astro.props;
const { formatEndpointRelation, t } = await getI18n(Astro.locals.currentLocale);
const { label } = formatEndpointRelation(parentPage);
const collectibles: EndpointCollectiblePreview[] = [];
const folders: EndpointFolderPreview[] = [];
const events: EndpointChronologyEvent[] = [];
backlinks.forEach((relation) => {
switch (relation.type) {
case Collections.Collectibles:
collectibles.push(relation.value);
break;
case Collections.Folders:
folders.push(relation.value);
break;
case Collections.ChronologyEvents:
events.push(relation.value);
break;
default:
break;
}
});
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout class="app">
<AppLayoutTitle title={t("global.relationPage.title")} />
<ReturnToParentCard parent={parentPage} />
{
collectibles.length > 0 && (
<section>
<h2 class="font-3xl font-serif">
{t("global.collections.collectibles", { count: collectibles.length })}
</h2>
<p>{t("global.relationPage.collectibles", { title: label, count: folders.length })}</p>
<div class="grid">
{collectibles.map((collectible) => (
<CollectiblePreview collectible={collectible} />
))}
</div>
</section>
)
}
{
folders.length > 0 && (
<section>
<h2 class="font-3xl font-serif">
{t("global.collections.folders", { count: folders.length })}
</h2>
<p>{t("global.relationPage.folders", { title: label, count: folders.length })}</p>
<div class="grid">
{folders.map((folder) => (
<FolderPreview folder={folder} />
))}
</div>
</section>
)
}
{
events.length > 0 && (
<section>
<h2 class="font-3xl font-serif">{t("global.collections.chronologyEvents")}</h2>
<p>{t("global.relationPage.timelineEvents", { title: label, count: folders.length })}</p>
<div class="grid">
{events.map(({ date, events }) =>
events.map((event) => <ChronologyEventPreview date={date} event={event} />)
)}
</div>
</section>
)
}
</AppLayout>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
.app {
display: flex;
flex-direction: column;
gap: 32px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: clamp(6px, 2vmin, 16px);
align-items: start;
@media (max-width: 40rem) {
grid-template-columns: 1fr 1fr;
row-gap: 12px;
}
@media (max-width: 24rem) {
grid-template-columns: 1fr;
}
}
section {
margin-block: 3em;
& > h2 {
margin-bottom: 0.5em;
}
& > p {
margin-bottom: 3em;
}
}
</style>

View File

@ -1,6 +1,11 @@
import type { WordingKey } from "src/i18n/wordings-keys";
import { contextCache } from "src/services";
import { capitalize, formatInlineTitle } from "src/utils/format";
import {
capitalize,
formatInlineTitle,
formatRichTextToString,
formatTimelineDateToId,
} from "src/utils/format";
import type { EndpointChronologyEvent, EndpointRelation } from "src/shared/payload/endpoint-types";
import { Collections } from "src/shared/payload/constants";
@ -281,9 +286,16 @@ export const getI18n = async (locale: string) => {
}
};
const suffix =
relation.subpage === "scans"
? "/scans"
: relation.subpage === "gallery"
? "/gallery"
: "";
const translation = getLocalizedMatch(relation.value.translations);
return {
href: getLocalizedUrl(`/collectibles/${relation.value.slug}`),
href: getLocalizedUrl(`/collectibles/${relation.value.slug}${suffix}`),
typeLabel: t("global.sources.typeLabel.collectible"),
label: formatInlineTitle(translation) + getRangeLabel(),
lang: translation.language,
@ -310,21 +322,37 @@ export const getI18n = async (locale: string) => {
};
}
case Collections.ChronologyEvents: {
if (!relation.value.events[0]) break;
const translation = getLocalizedMatch(relation.value.events[0].translations);
let label =
translation.title ??
(translation.description && formatRichTextToString(translation.description));
if (!label) break;
return {
href: getLocalizedUrl(`/timeline#${formatTimelineDateToId(relation.value.date)}`),
typeLabel: t("global.sources.typeLabel.timeline"),
label,
};
}
/* TODO: Handle other types of relations */
case Collections.Audios:
case Collections.ChronologyEvents:
case Collections.Files:
case Collections.Images:
case Collections.Recorders:
case Collections.Tags:
case Collections.Videos:
default:
return {
href: "/404",
label: `Invalid type ${relation["type"]}`,
typeLabel: "Error",
};
}
return {
href: "/404",
label: `Invalid type ${relation["type"]}`,
typeLabel: "Error",
};
};
return {

View File

@ -176,4 +176,12 @@ export type WordingKey =
| "paginator.goLastPageButton"
| "global.folders.attributes.content.label"
| "global.folders.attributes.content.value"
| "global.folders.attributes.parent";
| "global.folders.attributes.parent"
| "global.sources.typeLabel.timeline"
| "global.subpageCard.message"
| "global.subpageCard.returnButton"
| "header.nav.parentPages.tooltip.viewAll"
| "global.relationPage.folders"
| "global.relationPage.collectibles"
| "global.relationPage.timelineEvents"
| "global.relationPage.title";

View File

@ -10,7 +10,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout class="app" hideHomeButton>
<AppLayout class="app" topBar={{ hideHomeButton: true }}>
<div id="text-container">
<h1 class="font-serif font-5xl">404</h1>
<h2 class="font-4xl">Not found</h2>

View File

@ -16,7 +16,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout class="app" hideHomeButton>
<AppLayout class="app" topBar={{ hideHomeButton: true }}>
<div class="top">
<div id="text-container">
<h1 class="font-serif font-5xl">500</h1>

View File

@ -73,13 +73,13 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
audio,
}}>
}}
topBar={{ relations: backlinks }}>
<div id="container">
<AudioPlayer audio={audio} class="audio_id-audio-player" />

View File

@ -68,7 +68,7 @@ const metaAttributes = [
description: description && formatRichTextToString(description),
thumbnail: image,
}}
backlinks={backlinks}>
topBar={{ relations: backlinks }}>
<Lightbox
image={image}
pretitle={pretitle}

View File

@ -6,7 +6,7 @@ import { payload } from "src/services";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import { fetchOr404 } from "src/utils/responses";
import { sizesToSrcset } from "src/utils/img";
import RichText from "components/RichText/RichText.astro";
import ReturnToParentCard from "components/AppLayout/components/Topbar/components/ReturnToParentCard.astro";
const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl, t } = await getI18n(Astro.locals.currentLocale);
@ -15,7 +15,7 @@ const response = await fetchOr404(() => payload.getCollectibleGallery(slug));
if (response instanceof Response) {
return response;
}
const { translations, backlinks, images, thumbnail } = response.data;
const { translations, images, thumbnail, backlinks } = response.data;
const translation = getLocalizedMatch(translations);
---
@ -28,20 +28,9 @@ const translation = getLocalizedMatch(translations);
description: translation.description && formatRichTextToString(translation.description),
thumbnail,
}}
backlinks={backlinks}
class="app">
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
lang={translation.language}
/>
{
translation.description && (
<RichText content={translation.description} context={{ lang: translation.language }} />
)
}
<AppLayoutTitle title={t("collectibles.gallery.title")} />
{backlinks[0] && <ReturnToParentCard parent={backlinks[0]} />}
<div>
{

View File

@ -19,7 +19,8 @@ import { sizesToSrcset } from "src/utils/img";
import RichText from "components/RichText/RichText.astro";
import SubFilesSection from "./_components/SubFilesSection.astro";
import PriceInfo from "./_components/PriceInfo.astro";
import { CollectibleNature } from "src/shared/payload/constants";
import { CollectibleNature, Collections } from "src/shared/payload/constants";
import { sortBy } from "src/utils/array";
const slug = Astro.params.slug!;
const { getLocalizedMatch, getLocalizedUrl, t, formatDate } = await getI18n(
@ -117,6 +118,8 @@ if (languages.length > 0) {
withBorder: true,
});
}
sortBy(backlinks, ({ type }) => type, [Collections.Collectibles, Collections.Folders] as const);
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
@ -127,7 +130,10 @@ if (languages.length > 0) {
description: description && formatRichTextToString(description),
thumbnail,
}}
backlinks={backlinks}
topBar={{
relations: backlinks,
relationPageUrl: getLocalizedUrl(`/collectibles/${slug}/relations`),
}}
backgroundImage={backgroundImage ?? thumbnail}>
<AsideLayout reducedAsideWidth>
<Fragment slot="header">

View File

@ -0,0 +1,22 @@
---
import RelationsPage from "components/RelationsPage.astro";
import { payload } from "src/services";
import { Collections } from "src/shared/payload/constants";
import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const response = await fetchOr404(() => payload.getCollectible(slug));
if (response instanceof Response) {
return response;
}
const collectible = response.data;
const { backlinks } = collectible;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<RelationsPage
parentPage={{ type: Collections.Collectibles, value: collectible }}
backlinks={backlinks}
/>

View File

@ -30,7 +30,7 @@ const translation = getLocalizedMatch(translations);
title: `${formatInlineTitle(translation)} (${index})`,
description: translation.description && formatRichTextToString(translation.description),
}}
backlinks={backlinks}>
topBar={{ relations: backlinks }}>
<Lightbox
image={image}
title={formatScanIndexShort(index)}

View File

@ -1,13 +1,13 @@
---
import AppLayout from "components/AppLayout/AppLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Credits from "components/Credits.astro";
import { getI18n } from "src/i18n/i18n";
import { payload } from "src/services";
import { fetchOr404 } from "src/utils/responses";
import ScanPreview from "./_components/ScanPreview.astro";
import { formatInlineTitle, formatRichTextToString } from "src/utils/format";
import RichText from "components/RichText/RichText.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import ReturnToParentCard from "components/AppLayout/components/Topbar/components/ReturnToParentCard.astro";
const slug = Astro.params.slug!;
const { getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
@ -16,7 +16,7 @@ const response = await fetchOr404(() => payload.getCollectibleScans(slug));
if (response instanceof Response) {
return response;
}
const { translations, credits, cover, pages, dustjacket, obi, backlinks, thumbnail } =
const { translations, credits, cover, pages, dustjacket, obi, thumbnail, backlinks } =
response.data;
const translation = getLocalizedMatch(translations);
@ -45,20 +45,9 @@ const hasOutsideObi = obi ? Object.keys(obi).some((value) => !value.includes("in
description: translation.description && formatRichTextToString(translation.description),
thumbnail,
}}
backlinks={backlinks}
class="app">
<AppLayoutTitle
title={translation.title}
pretitle={translation.pretitle}
subtitle={translation.subtitle}
lang={translation.language}
/>
{
translation.description && (
<RichText content={translation.description} context={{ lang: translation.language }} />
)
}
<AppLayoutTitle title={t("collectibles.scans.title")} />
{backlinks[0] && <ReturnToParentCard parent={backlinks[0]} />}
{credits.length > 0 && <Credits credits={credits} />}

View File

@ -84,12 +84,12 @@ const smallTitle = title === filename;
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
}}>
}}
topBar={{ relations: backlinks }}>
<div id="container">
{
thumbnail ? (

View File

@ -35,7 +35,7 @@ const { language, title, description } = getLocalizedMatch(translations);
title: title,
description: description && formatRichTextToString(description),
}}
backlinks={backlinks}
topBar={{ relations: backlinks }}
class="app">
<AppLayoutTitle title={title} lang={language} />
{description && <RichText content={description} context={{ lang: language }} />}

View File

@ -83,12 +83,12 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail: image,
}}>
}}
topBar={{ relations: backlinks }}>
<Lightbox
image={image}
pretitle={pretitle}

View File

@ -16,7 +16,7 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
openGraph={{ title: t("home.title") }}
backgroundImage={contextCache.config.home.backgroundImage}
hideFooterLinks
hideHomeButton
topBar={{ hideHomeButton: true }}
class="app">
<HomeTitle />
<p class="prose" set:html={t("home.description")} />

View File

@ -15,7 +15,7 @@ if (response instanceof Response) {
const page = response.data;
const { backlinks, thumbnail, translations, backgroundImage } = page;
const { getLocalizedMatch } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const meta = getLocalizedMatch(translations);
---
@ -27,7 +27,7 @@ const meta = getLocalizedMatch(translations);
description: meta.summary && formatRichTextToString(meta.summary),
thumbnail: thumbnail,
}}
backlinks={backlinks}
topBar={{ relations: backlinks, relationPageUrl: getLocalizedUrl(`/pages/${slug}/relations`) }}
backgroundImage={backgroundImage ?? thumbnail}>
<Page slug={slug} lang={Astro.locals.currentLocale} page={page} />
</AppLayout>

View File

@ -0,0 +1,19 @@
---
import RelationsPage from "components/RelationsPage.astro";
import { payload } from "src/services";
import { Collections } from "src/shared/payload/constants";
import { fetchOr404 } from "src/utils/responses";
const slug = Astro.params.slug!;
const response = await fetchOr404(() => payload.getPage(slug));
if (response instanceof Response) {
return response;
}
const collectible = response.data;
const { backlinks } = collectible;
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<RelationsPage parentPage={{ type: Collections.Pages, value: collectible }} backlinks={backlinks} />

View File

@ -86,7 +86,7 @@ const getSearchUrl = (newState: State): string => {
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout hideSearchButton>
<AppLayout topBar={{ hideSearchButton: true }}>
<div class="center">
<HomeTitle />

View File

@ -73,13 +73,13 @@ const metaAttributes = [
{/* ------------------------------------------- HTML ------------------------------------------- */}
<AppLayout
backlinks={backlinks}
openGraph={{
title: formatInlineTitle({ pretitle, title, subtitle }),
description: description && formatRichTextToString(description),
thumbnail,
video,
}}>
}}
topBar={{ relations: backlinks }}>
<div id="container">
<VideoPlayer class="video_id-video-player" video={video} />

@ -1 +1 @@
Subproject commit caa79dee9eca5b9b6959e6f5a721245202423612
Subproject commit 63d17331a17a5ab7874599d1df4ecf6b45ac89f3

View File

@ -9,3 +9,14 @@ export const groupBy = <K, T>(array: T[], getKey: (item: T) => K): { key: K; val
return [...map.entries()].map(([key, values]) => ({ key, values }));
};
export const sortBy = <T, K>(array: T[], getKey: (item: T) => K, sort: K[]) =>
array.sort((a, b) => {
const aKey = getKey(a);
const bKey = getKey(b);
let aKeyIndex = sort.indexOf(aKey);
let bKeyIndex = sort.indexOf(bKey);
if (aKeyIndex === -1) aKeyIndex = array.length;
if (bKeyIndex === -1) bKeyIndex = array.length;
return aKeyIndex - bKeyIndex;
});