Added relations pages to collectibles and pages
This commit is contained in:
parent
1a6e0b315b
commit
403f7e087d
6
TODO.md
6
TODO.md
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -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"
|
||||
|
|
|
@ -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}`];
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ const metaAttributes = [
|
|||
description: description && formatRichTextToString(description),
|
||||
thumbnail: image,
|
||||
}}
|
||||
backlinks={backlinks}>
|
||||
topBar={{ relations: backlinks }}>
|
||||
<Lightbox
|
||||
image={image}
|
||||
pretitle={pretitle}
|
||||
|
|
|
@ -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>
|
||||
{
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
/>
|
|
@ -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)}
|
||||
|
|
|
@ -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} />}
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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 }} />}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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")} />
|
||||
|
|
|
@ -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>
|
|
@ -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} />
|
|
@ -86,7 +86,7 @@ const getSearchUrl = (newState: State): string => {
|
|||
|
||||
{/* ------------------------------------------- HTML ------------------------------------------- */}
|
||||
|
||||
<AppLayout hideSearchButton>
|
||||
<AppLayout topBar={{ hideSearchButton: true }}>
|
||||
<div class="center">
|
||||
<HomeTitle />
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue