Added timeline page

This commit is contained in:
DrMint 2024-03-22 15:55:18 +01:00
parent 606c3cc53f
commit 0c4d5e4007
21 changed files with 807 additions and 130 deletions

View File

@ -12,7 +12,6 @@
- [Collectibles] Create page for scans
- When the tags overflow, the tag group name should be align start (see http://localhost:12499/en/pages/magnitude-negative-chapter-1)
- [SDK] create a initPayload() that return a payload sdk (and stop hard wirring to ENV or node-cache)
- [Payload] Compare current package.json with fresh install of create-payload-app
## Long term
@ -21,7 +20,6 @@
- Grid view (all files)
- Web archives
- Videos
- Timeline page
- Contact page
- About us page
- Global search function

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

View File

@ -48,7 +48,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(
<Icon name="material-symbols:history" />
<p>{"Changelog"}</p>
</a>
<a href="/timeline" class="DEV_TODO">
<a href="/timeline">
<Icon name="material-symbols:calendar-month" />
<p>{t("footer.links.timeline.title")}</p>
</a>

View File

@ -499,6 +499,10 @@ const { currentTheme } = Astro.locals;
font-size: 24px;
}
> h4 {
font-size: 18px;
}
> h2,
> h3,
> h4,
@ -548,3 +552,17 @@ const { currentTheme } = Astro.locals;
Array.from(newDocument.querySelectorAll(".when-no-js")).forEach((node) => node.remove());
});
</script>
<script is:inline data-astro-rerun>
document.querySelectorAll("a").forEach((element) => {
const href = element.getAttribute("href");
if (!href || !href.startsWith("#")) return;
const heading = document.getElementById(href.substring(1));
if (!heading) return;
element.addEventListener("click", (event) => {
heading.scrollIntoView({ behavior: "smooth" });
event.preventDefault();
});
});
</script>

View File

@ -0,0 +1,71 @@
---
import type { EndpointRecorder } from "src/shared/payload/payload-sdk";
import { getI18n } from "src/i18n/i18n";
interface Props {
translators?: EndpointRecorder[] | undefined;
transcribers?: EndpointRecorder[] | undefined;
proofreaders?: EndpointRecorder[] | undefined;
}
const { translators = [], transcribers = [], proofreaders = [] } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
const tagGroups = [];
if (translators.length > 0) {
tagGroups.push({
name: t("global.credits.translators"),
values: translators,
});
}
if (transcribers.length > 0) {
tagGroups.push({
name: t("global.credits.transcribers"),
values: transcribers,
});
}
if (proofreaders.length > 0) {
tagGroups.push({
name: t("global.credits.proofreaders"),
values: proofreaders,
});
}
---
{/* ------------------------------------------- HTML ------------------------------------------- */}
<div id="tags">
{
tagGroups.map(({ name, values }) => (
<div>
<p>{name}</p>
{values.map(({ username }) => username).join(", ")}
</div>
))
}
</div>
{/* ------------------------------------------- CSS -------------------------------------------- */}
<style>
#tags {
margin-top: 0.5em;
display: flex;
flex-direction: column;
gap: 0.5em 1.5em;
& > div {
font-weight: 400;
font-size: 80%;
display: flex;
gap: 0.5em;
& > p {
color: var(--color-base-750);
}
}
}
</style>

View File

@ -28,7 +28,7 @@ const title = (() => {
{/* ------------------------------------------- HTML ------------------------------------------- */}
<li data-prefix={entry.prefix}>
<a href={`#${entry.prefix}`} class="pressable-link table-of-content-item">{title}</a>
<a href={`#${entry.prefix}`} class="pressable-link">{title}</a>
{
entry.children.length > 0 && (
<ol>
@ -61,19 +61,3 @@ const title = (() => {
line-height: 125%;
}
</style>
{/* ------------------------------------------- JS --------------------------------------------- */}
<script>
document.querySelectorAll(".table-of-content-item").forEach((element) => {
const href = element.getAttribute("href")?.substring(1);
if (!href) return;
const heading = document.getElementById(href);
if (!heading) return;
element.addEventListener("click", (event) => {
heading.scrollIntoView({ behavior: "smooth" });
event.preventDefault();
});
});
</script>

37
src/dataConfig.ts Normal file
View File

@ -0,0 +1,37 @@
import type { WordingKey } from "src/i18n/wordings-keys";
const timelineEras: { name: WordingKey; start: number; end: number }[] = [
{ name: "timeline.eras.cataclysm", start: 856, end: 856 },
{
name: "timeline.eras.drakengard3",
start: 997,
end: 1000,
},
{
name: "timeline.eras.drakengard",
start: 1001,
end: 1099,
},
{
name: "timeline.eras.drakengard2",
start: 1100,
end: 1117,
},
{
name: "timeline.eras.nier",
start: 2003,
end: 5012,
},
{
name: "timeline.eras.nierAutomata",
start: 5012,
end: 12543,
},
];
export const dataConfig = {
timeline: {
yearsWithABreakBefore: [856, 997, 1001, 1118, 1957, 2003, 2049, 2050, 3361, 3463],
eras: timelineEras,
},
};

View File

@ -1,5 +1,7 @@
import type { WordingKey } from "src/i18n/wordings-keys";
import type { ChronologyEvent } from "src/shared/payload/payload-sdk";
import { cache } from "src/utils/cachedPayload";
import { capitalize } from "src/utils/format";
export const defaultLocale = "en";
@ -105,9 +107,7 @@ export const getI18n = async (locale: string) => {
return template;
};
const getLocalizedMatch = <T extends { language: string }>(
options: T[]
): Omit<T, "language"> & { language?: string } =>
const getLocalizedMatch = <T extends { language: string }>(options: T[]): T =>
options.find(({ language }) => language === locale) ??
options.find(({ language }) => language === defaultLocale) ??
options[0]!; // We will consider that there will always be at least one option.
@ -129,8 +129,10 @@ export const getI18n = async (locale: string) => {
const formatPrice = (price: { amount: number; currency: string }): string =>
price.amount.toLocaleString(locale, { style: "currency", currency: price.currency });
const formatDate = (date: Date): string =>
date.toLocaleDateString(locale, { dateStyle: "medium" });
const formatDate = (
date: Date,
options: Intl.DateTimeFormatOptions | undefined = { dateStyle: "medium" }
): string => date.toLocaleDateString(locale, options);
const formatInches = (sizeInMm: number): string => {
return (
@ -156,6 +158,21 @@ export const getI18n = async (locale: string) => {
return number.toLocaleString(locale, options);
};
const formatTimelineDate = ({ year, month, day }: ChronologyEvent["date"]): string => {
const date = new Date();
date.setFullYear(year);
if (month) date.setMonth(month - 1);
if (day) date.setDate(day);
return capitalize(
formatDate(date, {
year: "numeric",
month: month ? "long" : undefined,
day: day ? "numeric" : undefined,
})
);
};
return {
t,
getLocalizedMatch,
@ -167,5 +184,6 @@ export const getI18n = async (locale: string) => {
formatGrams,
formatMillimeters,
formatNumber,
formatTimelineDate,
};
};

View File

@ -87,4 +87,21 @@ export type WordingKey =
| "global.loading"
| "pages.tableOfContent.sceneBreak"
| "pages.tableOfContent.break"
| "global.languageOverride.availableLanguages";
| "global.languageOverride.availableLanguages"
| "timeline.title"
| "timeline.eras.drakengard3"
| "timeline.eras.drakengard"
| "timeline.eras.drakengard2"
| "timeline.eras.nier"
| "timeline.eras.nierAutomata"
| "timeline.eras.cataclysm"
| "timeline.description"
| "timeline.notes.title"
| "timeline.notes.content"
| "timeline.priorCataclysmNote.title"
| "timeline.priorCataclysmNote.content"
| "timeline.jumpTo"
| "timeline.year.during"
| "timeline.eventFooter.languages"
| "timeline.eventFooter.sources"
| "timeline.eventFooter.note";

View File

@ -0,0 +1,64 @@
---
import MasoTarget from "components/Maso/MasoTarget.astro";
import TimelineEventTranslation from "pages/[locale]/timeline/_components/TimelineEventTranslation.astro";
import TimelineLanguageOverride from "pages/[locale]/timeline/_components/TimelineLanguageOverride.astro";
import TimelineNote from "pages/[locale]/timeline/_components/TimelineNote.astro";
import TimelineSourcesButton from "pages/[locale]/timeline/_components/TimelineSourcesButton.astro";
import { getI18n } from "src/i18n/i18n";
import { payload, type EndpointChronologyEvent } from "src/shared/payload/payload-sdk";
export const partial = true;
interface Props {
lang: string;
event: EndpointChronologyEvent;
id: string;
index: number;
}
const reqUrl = new URL(Astro.request.url);
const lang = Astro.props.lang ?? reqUrl.searchParams.get("lang")!;
const id = Astro.props.id ?? reqUrl.searchParams.get("id")!;
const index = Astro.props.index ?? parseInt(reqUrl.searchParams.get("index")!);
const event = Astro.props.event ?? (await payload.getChronologyEventByID(id));
const { sources, translations } = event.events[index]!;
const { getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
const { getLocalizedMatch } = await getI18n(lang);
const { title, description, notes, proofreaders, transcribers, translators } =
getLocalizedMatch(translations);
---
<MasoTarget>
<div class="event">
<TimelineEventTranslation title={title} description={description} />
<div id="bottom" class="when-js when-no-print">
{sources.length > 0 && <TimelineSourcesButton sources={sources} />}
{notes && <TimelineNote notes={notes} />}
<TimelineLanguageOverride
availableLanguages={translations.map(({ language }) => language)}
currentLang={lang}
proofreaders={proofreaders}
transcribers={transcribers}
translators={translators}
getPartialUrl={(locale) =>
getLocalizedUrl(`/api/timeline/partial?id=${id}&index=${index}&lang=${locale}`)}
/>
</div>
</div>
</MasoTarget>
<style>
.event {
display: flex;
flex-direction: column;
gap: 1em;
& > #bottom {
display: flex;
place-items: start;
gap: 0.3em;
font-size: 85%;
}
}
</style>

View File

@ -6,6 +6,7 @@ import LibraryGrid from "./_components/LibraryGrid.astro";
import ChronicleCard from "./_components/ChronicleCard.astro";
import LinkCard from "./_components/LinkCard.astro";
import { getI18n } from "src/i18n/i18n";
import { dataConfig } from "src/dataConfig";
const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
---
@ -113,15 +114,6 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
<p set:html={t("home.moreSection.description")} />
<div class="grid">
<div class="DEV_TODO">
<LinkCard
icon="material-symbols:calendar-month-outline"
title={t("footer.links.timeline.title")}
subtitle={t("footer.links.timeline.subtitle", {
eraCount: 8,
eventCount: 358,
})}
href={getLocalizedUrl("/timeline")}
/>
<LinkCard
icon="material-symbols:movie-outline"
title={t("footer.links.videos.title")}
@ -136,6 +128,16 @@ const { t, getLocalizedUrl } = await getI18n(Astro.locals.currentLocale);
/>
</div>
<LinkCard
icon="material-symbols:calendar-month-outline"
title={t("footer.links.timeline.title")}
subtitle={t("footer.links.timeline.subtitle", {
eraCount: dataConfig.timeline.eras.length,
eventCount: 358,
})}
href={getLocalizedUrl("/timeline")}
/>
<LinkCard
icon="material-symbols:perm-media-outline"
title={t("footer.links.gallery.title")}

View File

@ -0,0 +1,59 @@
---
import { getI18n } from "src/i18n/i18n";
import type { EndpointChronologyEvent } from "src/shared/payload/payload-sdk";
import TimelineEventPartial from "../../api/timeline/partial.astro";
interface Props {
event: EndpointChronologyEvent;
displayDate: boolean;
}
const { event, displayDate } = Astro.props;
const { formatTimelineDate, t } = await getI18n(Astro.locals.currentLocale);
const displayedDate =
!event.date.month && !event.date.day
? t("timeline.year.during", { year: formatTimelineDate(event.date) })
: formatTimelineDate(event.date);
---
<div class="event-container">
{displayDate && <h3>{displayedDate}</h3>}
<div>
{
event.events.map((_, index) => (
<TimelineEventPartial
event={event}
index={index}
id={event.id}
lang={Astro.locals.currentLocale}
/>
))
}
</div>
</div>
<style>
.event-container {
&:has(h3) > div {
border-left: 1px solid var(--color-base-600);
padding-left: 1em;
padding-block: 1em;
}
& > div {
display: flex;
flex-direction: column;
gap: 2em;
}
& > h3 {
padding-bottom: 0.3em;
padding-inline: 0.2em;
color: var(--color-base-700);
border-bottom: 1px solid var(--color-base-600);
width: fit-content;
}
}
</style>

View File

@ -0,0 +1,29 @@
---
import RichText from "components/RichText/RichText.astro";
import type { RichTextContent } from "src/shared/payload/payload-sdk";
interface Props {
title?: string | undefined;
description?: RichTextContent | undefined;
}
const { title, description } = Astro.props;
---
<div>
{title && <h4>{title}</h4>}
{description && <RichText content={description} />}
</div>
<style>
div {
display: flex;
flex-direction: column;
gap: 0.5em;
& > h4 {
font-size: 120%;
max-width: 35rem;
}
}
</style>

View File

@ -0,0 +1,66 @@
---
import { Icon } from "astro-icon/components";
import InlineCredits from "components/InlineCredits.astro";
import MasoActor from "components/Maso/MasoActor.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointRecorder } from "src/shared/payload/payload-sdk";
import { formatLocale } from "src/utils/format";
interface Props {
currentLang: string;
availableLanguages: string[];
getPartialUrl: (locale: string) => string;
transcribers: EndpointRecorder[];
translators: EndpointRecorder[];
proofreaders: EndpointRecorder[];
}
const { availableLanguages, transcribers, proofreaders, translators, getPartialUrl, currentLang } =
Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
{
availableLanguages.map((id) => (
<MasoActor href={getPartialUrl(id)}>
<p class:list={{ current: id === currentLang, "pressable-link": true }}>
{formatLocale(id)}
</p>
</MasoActor>
))
}
<InlineCredits
translators={translators}
transcribers={transcribers}
proofreaders={proofreaders}
/>
</div>
<div class="pressable-label">
<Icon name="material-symbols:translate" />
<p>
{
t("timeline.eventFooter.languages", {
count: availableLanguages.length,
})
}
</p>
</div>
</Tooltip>
<style>
#tooltip-content {
display: grid;
gap: 0.5em;
font-size: 1rem;
& .current {
color: var(--color-base-750);
text-decoration: underline 0.08em var(--color-base-650);
}
}
</style>

View File

@ -0,0 +1,25 @@
---
import { Icon } from "astro-icon/components";
import RichText from "components/RichText/RichText.astro";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import type { RichTextContent } from "src/shared/payload/payload-sdk";
interface Props {
notes: RichTextContent;
}
const { notes } = Astro.props;
const { t } = await getI18n(Astro.locals.currentLocale);
---
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
<RichText content={notes} />
</div>
<div class="pressable-label">
<Icon name="material-symbols:comment-outline" />
<p>{t("timeline.eventFooter.note")}</p>
</div>
</Tooltip>

View File

@ -0,0 +1,53 @@
---
import { Icon } from "astro-icon/components";
import Tooltip from "components/Tooltip.astro";
import { getI18n } from "src/i18n/i18n";
import type { EndpointSource } from "src/shared/payload/payload-sdk";
import { formatInlineTitle } from "src/utils/format";
interface Props {
sources: EndpointSource[];
}
const { sources } = Astro.props;
const { getLocalizedUrl, getLocalizedMatch, t } = await getI18n(Astro.locals.currentLocale);
---
<Tooltip trigger="click">
<div id="tooltip-content" slot="tooltip-content">
{
sources.map((source) =>
source.type === "url" ? (
<a class="pressable-link" href={source.url} target={"_blank"} rel={"noopener noreferrer"}>
{source.label}
</a>
) : source.type === "collectible" ? (
<a
class="pressable-link"
href={getLocalizedUrl(`/collectibles/${source.collectible.slug}`)}>
{formatInlineTitle(getLocalizedMatch(source.collectible.translations))}
</a>
) : (
<a class="pressable-link" href={getLocalizedUrl(`/pages/${source.page.slug}`)}>
{formatInlineTitle(getLocalizedMatch(source.page.translations))}
</a>
)
)
}
</div>
<div class="pressable-label">
<Icon name="material-symbols:edit-note" />
<p>
{t("timeline.eventFooter.sources", { count: sources.length })}
</p>
</div>
</Tooltip>
<style>
#tooltip-content {
display: grid;
gap: 0.5em;
font-size: 1rem;
}
</style>

View File

@ -0,0 +1,53 @@
---
import type { EndpointChronologyEvent } from "src/shared/payload/payload-sdk";
import TimelineEvent from "./TimelineEvent.astro";
import { getI18n } from "src/i18n/i18n";
import { dataConfig } from "src/dataConfig";
interface Props {
year: number;
events: EndpointChronologyEvent[];
}
const { year, events } = Astro.props;
const { formatTimelineDate } = await getI18n(Astro.locals.currentLocale);
---
{dataConfig.timeline.yearsWithABreakBefore.includes(year) && <hr id={`hr-${year}`} />}
<h2 id={year.toString()}>
{formatTimelineDate(events.length === 1 ? events[0]!.date : { year })}
</h2>
<div class="year-container">
{events.map((event) => <TimelineEvent event={event} displayDate={events.length > 1} />)}
</div>
<style>
hr {
border: none;
border-top: 3px dashed var(--color-base-500);
margin-block: 5em;
scroll-margin-block: 5em;
}
.year-container {
border-left: 1px solid var(--color-base-600);
padding-left: 1em;
padding-block: 1em;
margin-bottom: 3em;
display: flex;
flex-direction: column;
gap: 2em;
}
h2 {
padding-bottom: 0.2em;
padding-inline: 0.2em;
color: var(--color-base-700);
border-bottom: 1px solid var(--color-base-600);
scroll-margin-block: 1em;
width: fit-content;
}
</style>

View File

@ -0,0 +1,110 @@
---
import { payload } from "src/shared/payload/payload-sdk";
import { groupBy } from "src/utils/array";
import TimelineYear from "./_components/TimelineYear.astro";
import AppEmptyLayout from "components/AppLayout/AppEmptyLayout.astro";
import AppLayoutTitle from "components/AppLayout/components/AppLayoutTitle.astro";
import Card from "components/Card.astro";
import { getI18n } from "src/i18n/i18n";
import AppLayoutBackgroundImg from "components/AppLayout/components/AppLayoutBackgroundImg.astro";
import { dataConfig } from "src/dataConfig";
const events = await payload.getChronologyEvents();
const groupedEvents = groupBy(events, (event) => event.date.year);
const { getLocalizedUrl, t, formatTimelineDate } = await getI18n(Astro.locals.currentLocale);
---
<AppEmptyLayout>
<AppLayoutBackgroundImg
img={{
url: "/img/timeline-background.webp",
filename: "timeline-background",
width: 2478,
height: 4110,
mimeType: "image/webp",
}}
/>
<AppLayoutTitle title={t("timeline.title")} />
<div id="summary" class="prose">
<p>
{t("timeline.description")}
</p>
</div>
<div class="card-container">
<Card>
<div class="card-content prose">
<h3>{t("timeline.notes.title")}</h3>
<p
set:html={t("timeline.notes.content", {
worldInside: `<a href="${getLocalizedUrl(
"/collectibles/world-inside"
)}">World Inside</a>`,
})}
/>
</div>
</Card>
<Card>
<div class="card-content prose">
<h3>{t("timeline.priorCataclysmNote.title")}</h3>
<p
set:html={t("timeline.priorCataclysmNote.content", {
worldInside: `<a href="${getLocalizedUrl(
"/collectibles/world-inside"
)}">World Inside</a>`,
})}
/>
</div>
</Card>
<Card>
<div class="card-content prose jump-card">
<h3>{t("timeline.jumpTo")}</h3>
{
dataConfig.timeline.eras.map(({ name, start, end }) => (
<p
set:html={t(name, {
start: `<a href="#${start}">${formatTimelineDate({ year: start })}</a>`,
end: `<a href="#${end}">${formatTimelineDate({ year: end })}</a>`,
})}
/>
))
}
</div>
</Card>
</div>
{groupedEvents.map(({ key, values }) => <TimelineYear year={key} events={values} />)}
</AppEmptyLayout>
<style>
#summary {
backdrop-filter: blur(5px);
padding: 1.5em;
margin: -1.5em;
margin-block: 1em;
border-radius: 3em;
width: fit-content;
}
.card-content {
padding: clamp(1em, 4vw, 3em);
max-width: 35rem;
&.jump-card > p {
margin-block: 0.2em;
}
}
.card-container {
margin-top: 2em;
margin-bottom: 4em;
display: flex;
gap: 2em;
flex-wrap: wrap;
}
</style>

View File

@ -20,8 +20,8 @@ export type RecorderBiographies =
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -46,26 +46,25 @@ export interface Config {
pages: Page;
collectibles: Collectible;
folders: Folder;
'chronology-items': ChronologyItem;
'chronology-eras': ChronologyEra;
"chronology-events": ChronologyEvent;
notes: Note;
images: Image;
'background-images': BackgroundImage;
'recorders-thumbnails': RecordersThumbnail;
"background-images": BackgroundImage;
"recorders-thumbnails": RecordersThumbnail;
videos: Video;
'videos-channels': VideosChannel;
"videos-channels": VideosChannel;
tags: Tag;
'tags-groups': TagsGroup;
"tags-groups": TagsGroup;
recorders: Recorder;
languages: Language;
currencies: Currency;
wordings: Wording;
'generic-contents': GenericContent;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
"generic-contents": GenericContent;
"payload-preferences": PayloadPreference;
"payload-migrations": PayloadMigration;
};
globals: {
'home-folders': HomeFolder;
"home-folders": HomeFolder;
};
}
/**
@ -75,7 +74,7 @@ export interface Config {
export interface Page {
id: string;
slug: string;
type: 'Content' | 'Post' | 'Generic';
type: "Content" | "Post" | "Generic";
thumbnail?: string | Image | null;
backgroundImage?: string | BackgroundImage | null;
tags?: (string | Tag)[] | null;
@ -93,8 +92,8 @@ export interface Page {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -108,8 +107,8 @@ export interface Page {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -126,7 +125,7 @@ export interface Page {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -237,7 +236,7 @@ export interface Recorder {
avatar?: string | RecordersThumbnail | null;
languages?: (string | Language)[] | null;
biographies?: RecorderBiographies;
role?: ('Admin' | 'Recorder' | 'Api')[] | null;
role?: ("Admin" | "Recorder" | "Api")[] | null;
anonymize: boolean;
email: string;
resetPasswordToken?: string | null;
@ -301,8 +300,8 @@ export interface Folder {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -327,11 +326,11 @@ export interface Folder {
files?:
| (
| {
relationTo: 'collectibles';
relationTo: "collectibles";
value: string | Collectible;
}
| {
relationTo: 'pages';
relationTo: "pages";
value: string | Page;
}
)[]
@ -347,7 +346,7 @@ export interface Collectible {
id: string;
slug: string;
thumbnail?: string | Image | null;
nature: 'Physical' | 'Digital';
nature: "Physical" | "Digital";
languages?: (string | Language)[] | null;
tags?: (string | Tag)[] | null;
translations: {
@ -362,8 +361,8 @@ export interface Collectible {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -455,8 +454,8 @@ export interface Collectible {
pageInfoEnabled?: boolean | null;
pageInfo?: {
pageCount: number;
bindingType?: ('Paperback' | 'Hardcover') | null;
pageOrder?: ('Left to right' | 'Right to left') | null;
bindingType?: ("Paperback" | "Hardcover") | null;
pageOrder?: ("Left to right" | "Right to left") | null;
};
folders?: (string | Folder)[] | null;
parentItems?: (string | Collectible)[] | null;
@ -465,11 +464,11 @@ export interface Collectible {
| {
content:
| {
relationTo: 'pages';
relationTo: "pages";
value: string | Page;
}
| {
relationTo: 'generic-contents';
relationTo: "generic-contents";
value: string | GenericContent;
};
range?:
@ -479,14 +478,14 @@ export interface Collectible {
end: number;
id?: string | null;
blockName?: string | null;
blockType: 'pageRange';
blockType: "pageRange";
}
| {
start: string;
end: string;
id?: string | null;
blockName?: string | null;
blockType: 'timeRange';
blockType: "timeRange";
}
| {
translations?:
@ -499,8 +498,8 @@ export interface Collectible {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -512,7 +511,7 @@ export interface Collectible {
| null;
id?: string | null;
blockName?: string | null;
blockType: 'other';
blockType: "other";
}
)[]
| null;
@ -522,7 +521,7 @@ export interface Collectible {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -548,9 +547,9 @@ export interface GenericContent {
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "chronology-items".
* via the `definition` "chronology-events".
*/
export interface ChronologyItem {
export interface ChronologyEvent {
id: string;
name?: string | null;
date: {
@ -559,6 +558,7 @@ export interface ChronologyItem {
day?: number | null;
};
events: {
sources?: (UrlBlock | CollectibleBlock | PageBlock)[] | null;
translations: {
language: string | Language;
sourceLanguage: string | Language;
@ -570,8 +570,8 @@ export interface ChronologyItem {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -585,8 +585,8 @@ export interface ChronologyItem {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -603,42 +603,63 @@ export interface ChronologyItem {
updatedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
_status?: ("draft" | "published") | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "chronology-eras".
* via the `definition` "UrlBlock".
*/
export interface ChronologyEra {
id: string;
slug: string;
startingYear: number;
endingYear: number;
translations?:
| {
language: string | Language;
title: string;
description?: {
root: {
children: {
type: string;
version: number;
[k: string]: unknown;
export interface UrlBlock {
url: string;
id?: string | null;
blockName?: string | null;
blockType: "urlBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "CollectibleBlock".
*/
export interface CollectibleBlock {
collectible: string | Collectible;
range?:
| (
| {
page: number;
id?: string | null;
blockName?: string | null;
blockType: "page";
}
| {
timestamp: string;
id?: string | null;
blockName?: string | null;
blockType: "timestamp";
}
| {
translations: {
language: string | Language;
note: string;
id?: string | null;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[]
id?: string | null;
blockName?: string | null;
blockType: "other";
}
)[]
| null;
events?: (string | ChronologyItem)[] | null;
updatedAt: string;
createdAt: string;
id?: string | null;
blockName?: string | null;
blockType: "collectibleBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "PageBlock".
*/
export interface PageBlock {
page: string | Page;
id?: string | null;
blockName?: string | null;
blockType: "pageBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -653,8 +674,8 @@ export interface Note {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -672,7 +693,7 @@ export interface Video {
id: string;
uid: string;
gone: boolean;
source: 'YouTube' | 'NicoNico' | 'Tumblr';
source: "YouTube" | "NicoNico" | "Tumblr";
title: string;
description?: string | null;
likes?: number | null;
@ -708,7 +729,7 @@ export interface Wording {
export interface PayloadPreference {
id: string;
user: {
relationTo: 'recorders';
relationTo: "recorders";
value: string | Recorder;
};
key?: string | null;
@ -764,15 +785,15 @@ export interface LineBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
blockType: 'lineBlock';
blockType: "lineBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -786,15 +807,15 @@ export interface CueBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
};
[k: string]: unknown;
};
blockType: 'cueBlock';
blockType: "cueBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -804,17 +825,17 @@ export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string | null;
blockName?: string | null;
blockType: 'transcriptBlock';
blockType: "transcriptBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "BreakBlock".
*/
export interface BreakBlock {
type: 'Scene break' | 'Empty space' | 'Solid line' | 'Dotted line';
type: "Scene break" | "Empty space" | "Solid line" | "Dotted line";
id?: string | null;
blockName?: string | null;
blockType: 'breakBlock';
blockType: "breakBlock";
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@ -828,8 +849,8 @@ export interface SectionBlock {
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
direction: ("ltr" | "rtl") | null;
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
indent: number;
type: string;
version: number;
@ -838,17 +859,18 @@ export interface SectionBlock {
};
id?: string | null;
blockName?: string | null;
blockType: 'sectionBlock';
blockType: "sectionBlock";
}
declare module "payload" {
export interface GeneratedTypes extends Config {}
}
/////////////// CONSTANTS ///////////////
export enum Collections {
ChronologyEras = "chronology-eras",
ChronologyItems = "chronology-items",
ChronologyEvents = "chronology-events",
Currencies = "currencies",
Files = "files",
Languages = "languages",
@ -1334,7 +1356,6 @@ export type EndpointPagePreview = {
title: string;
subtitle?: string;
}[];
status: "draft" | "published";
};
export type EndpointPage = EndpointPagePreview & {
@ -1368,7 +1389,6 @@ export type EndpointCollectiblePreview = {
description?: RichTextContent;
}[];
tagGroups: EndpointTagsGroup[];
status: "draft" | "published";
releaseDate?: string;
languages: string[];
};
@ -1441,6 +1461,40 @@ export type TableOfContentEntry = {
children: TableOfContentEntry[];
};
export type EndpointChronologyEvent = {
id: string;
date: {
year: number;
month?: number;
day?: number;
};
events: {
sources: EndpointSource[];
translations: {
language: string;
sourceLanguage: string;
title?: string;
description?: RichTextContent;
notes?: RichTextContent;
transcribers: EndpointRecorder[];
translators: EndpointRecorder[];
proofreaders: EndpointRecorder[];
}[];
}[];
};
export type EndpointSource =
| { type: "url"; url: string; label: string }
| {
type: "collectible";
collectible: EndpointCollectiblePreview;
range?:
| { type: "page"; page: number }
| { type: "timestamp"; timestamp: string }
| { type: "custom"; translations: { language: string; note: string }[] };
}
| { type: "page"; page: EndpointPagePreview };
export type PayloadImage = {
url: string;
width: number;
@ -1450,8 +1504,6 @@ export type PayloadImage = {
};
export const payload = {
getEras: async (): Promise<EndpointEra[]> =>
await (await request(payloadApiUrl(Collections.ChronologyEras, `all`))).json(),
getHomeFolders: async (): Promise<EndpointHomeFolder[]> =>
await (await request(payloadApiUrl(Collections.HomeFolders, `all`, true))).json(),
getFolder: async (slug: string): Promise<EndpointFolder> =>
@ -1468,4 +1520,8 @@ export const payload = {
await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(),
getCollectible: async (slug: string): Promise<EndpointCollectible> =>
await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}`))).json(),
getChronologyEvents: async (): Promise<EndpointChronologyEvent[]> =>
await (await request(payloadApiUrl(Collections.ChronologyEvents, `all`))).json(),
getChronologyEventByID: async (id: string): Promise<EndpointChronologyEvent> =>
await (await request(payloadApiUrl(Collections.ChronologyEvents, id))).json(),
};

11
src/utils/array.ts Normal file
View File

@ -0,0 +1,11 @@
export const groupBy = <K, T>(array: T[], getKey: (item: T) => K): { key: K; values: T[] }[] => {
const map = new Map<K, T[]>();
array.forEach((item) => {
const key = getKey(item);
const currentValueInMap = map.get(key) ?? [];
currentValueInMap.push(item);
map.set(key, currentValueInMap);
});
return [...map.entries()].map(([key, values]) => ({ key, values }));
};

View File

@ -47,3 +47,9 @@ export const formatRichTextToString = (content: RichTextContent): string => {
return content.root.children.map(formatNode).join("\n\n");
};
export const capitalize = (string: string): string => {
const [firstLetter, ...otherLetters] = string;
if (firstLetter === undefined) return "";
return [firstLetter.toUpperCase(), ...otherLetters].join("");
};