diff --git a/TODO.md b/TODO.md index 15bb6d9..1c8e386 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/public/img/timeline-background.webp b/public/img/timeline-background.webp new file mode 100644 index 0000000..d7ef7b4 Binary files /dev/null and b/public/img/timeline-background.webp differ diff --git a/src/components/AppLayout/components/Footer.astro b/src/components/AppLayout/components/Footer.astro index 8395eb7..c32ea9d 100644 --- a/src/components/AppLayout/components/Footer.astro +++ b/src/components/AppLayout/components/Footer.astro @@ -48,7 +48,7 @@ const contactLabel = `${t("footer.socials.contact.title")} - ${t(

{"Changelog"}

- +

{t("footer.links.timeline.title")}

diff --git a/src/components/AppLayout/components/Html.astro b/src/components/AppLayout/components/Html.astro index b766dbf..37e5ce0 100644 --- a/src/components/AppLayout/components/Html.astro +++ b/src/components/AppLayout/components/Html.astro @@ -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()); }); + + diff --git a/src/components/InlineCredits.astro b/src/components/InlineCredits.astro new file mode 100644 index 0000000..fe4b4c4 --- /dev/null +++ b/src/components/InlineCredits.astro @@ -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 ------------------------------------------- */} + +
+ { + tagGroups.map(({ name, values }) => ( +
+

{name}

+ {values.map(({ username }) => username).join(", ")} +
+ )) + } +
+ +{/* ------------------------------------------- CSS -------------------------------------------- */} + + diff --git a/src/components/TableOfContent/components/TableOfContentItem.astro b/src/components/TableOfContent/components/TableOfContentItem.astro index 20d09ed..87f897e 100644 --- a/src/components/TableOfContent/components/TableOfContentItem.astro +++ b/src/components/TableOfContent/components/TableOfContentItem.astro @@ -28,7 +28,7 @@ const title = (() => { {/* ------------------------------------------- HTML ------------------------------------------- */}
  • - {title} + {title} { entry.children.length > 0 && (
      @@ -61,19 +61,3 @@ const title = (() => { line-height: 125%; } - -{/* ------------------------------------------- JS --------------------------------------------- */} - - diff --git a/src/dataConfig.ts b/src/dataConfig.ts new file mode 100644 index 0000000..fe115c3 --- /dev/null +++ b/src/dataConfig.ts @@ -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, + }, +}; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index fe1809b..739c50e 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -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 = ( - options: T[] - ): Omit & { language?: string } => + const getLocalizedMatch = (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, }; }; diff --git a/src/i18n/wordings-keys.ts b/src/i18n/wordings-keys.ts index 440b95c..7625391 100644 --- a/src/i18n/wordings-keys.ts +++ b/src/i18n/wordings-keys.ts @@ -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"; diff --git a/src/pages/[locale]/api/timeline/partial.astro b/src/pages/[locale]/api/timeline/partial.astro new file mode 100644 index 0000000..b78d166 --- /dev/null +++ b/src/pages/[locale]/api/timeline/partial.astro @@ -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); +--- + + +
      + +
      + {sources.length > 0 && } + {notes && } + language)} + currentLang={lang} + proofreaders={proofreaders} + transcribers={transcribers} + translators={translators} + getPartialUrl={(locale) => + getLocalizedUrl(`/api/timeline/partial?id=${id}&index=${index}&lang=${locale}`)} + /> +
      +
      +
      + + diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index 8aea91b..276c5ea 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -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);

      -
      + + + {displayDate &&

      {displayedDate}

      } + +
      + { + event.events.map((_, index) => ( + + )) + } +
      +
      + + diff --git a/src/pages/[locale]/timeline/_components/TimelineEventTranslation.astro b/src/pages/[locale]/timeline/_components/TimelineEventTranslation.astro new file mode 100644 index 0000000..d0b5d6e --- /dev/null +++ b/src/pages/[locale]/timeline/_components/TimelineEventTranslation.astro @@ -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; +--- + +
      + {title &&

      {title}

      } + {description && } +
      + + diff --git a/src/pages/[locale]/timeline/_components/TimelineLanguageOverride.astro b/src/pages/[locale]/timeline/_components/TimelineLanguageOverride.astro new file mode 100644 index 0000000..0be373a --- /dev/null +++ b/src/pages/[locale]/timeline/_components/TimelineLanguageOverride.astro @@ -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); +--- + + +
      + { + availableLanguages.map((id) => ( + +

      + {formatLocale(id)} +

      +
      + )) + } + + +
      +
      + +

      + { + t("timeline.eventFooter.languages", { + count: availableLanguages.length, + }) + } +

      +
      +
      + + diff --git a/src/pages/[locale]/timeline/_components/TimelineNote.astro b/src/pages/[locale]/timeline/_components/TimelineNote.astro new file mode 100644 index 0000000..79df44b --- /dev/null +++ b/src/pages/[locale]/timeline/_components/TimelineNote.astro @@ -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); +--- + + +
      + +
      +
      + +

      {t("timeline.eventFooter.note")}

      +
      +
      diff --git a/src/pages/[locale]/timeline/_components/TimelineSourcesButton.astro b/src/pages/[locale]/timeline/_components/TimelineSourcesButton.astro new file mode 100644 index 0000000..5f65c66 --- /dev/null +++ b/src/pages/[locale]/timeline/_components/TimelineSourcesButton.astro @@ -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); +--- + + +
      + { + sources.map((source) => + source.type === "url" ? ( + + {source.label} + + ) : source.type === "collectible" ? ( + + {formatInlineTitle(getLocalizedMatch(source.collectible.translations))} + + ) : ( + + {formatInlineTitle(getLocalizedMatch(source.page.translations))} + + ) + ) + } +
      +
      + +

      + {t("timeline.eventFooter.sources", { count: sources.length })} +

      +
      +
      + + diff --git a/src/pages/[locale]/timeline/_components/TimelineYear.astro b/src/pages/[locale]/timeline/_components/TimelineYear.astro new file mode 100644 index 0000000..81edb22 --- /dev/null +++ b/src/pages/[locale]/timeline/_components/TimelineYear.astro @@ -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) &&
      } + +

      + {formatTimelineDate(events.length === 1 ? events[0]!.date : { year })} +

      + +
      + {events.map((event) => 1} />)} +
      + + diff --git a/src/pages/[locale]/timeline/index.astro b/src/pages/[locale]/timeline/index.astro new file mode 100644 index 0000000..c9f8856 --- /dev/null +++ b/src/pages/[locale]/timeline/index.astro @@ -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); +--- + + + + + +
      +

      + {t("timeline.description")} +

      +
      + +
      + +
      +

      {t("timeline.notes.title")}

      + +

      World Inside`, + })} + /> +

      +
      + + +
      +

      {t("timeline.priorCataclysmNote.title")}

      + +

      World Inside`, + })} + /> +

      +
      + + +
      +

      {t("timeline.jumpTo")}

      + + { + dataConfig.timeline.eras.map(({ name, start, end }) => ( +

      ${formatTimelineDate({ year: start })}`, + end: `${formatTimelineDate({ year: end })}`, + })} + /> + )) + } +

      +
      +
      + {groupedEvents.map(({ key, values }) => )} +
      + + diff --git a/src/shared/payload/payload-sdk.ts b/src/shared/payload/payload-sdk.ts index 563197c..447b673 100644 --- a/src/shared/payload/payload-sdk.ts +++ b/src/shared/payload/payload-sdk.ts @@ -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 => - await (await request(payloadApiUrl(Collections.ChronologyEras, `all`))).json(), getHomeFolders: async (): Promise => await (await request(payloadApiUrl(Collections.HomeFolders, `all`, true))).json(), getFolder: async (slug: string): Promise => @@ -1468,4 +1520,8 @@ export const payload = { await (await request(payloadApiUrl(Collections.Pages, `slug/${slug}`))).json(), getCollectible: async (slug: string): Promise => await (await request(payloadApiUrl(Collections.Collectibles, `slug/${slug}`))).json(), + getChronologyEvents: async (): Promise => + await (await request(payloadApiUrl(Collections.ChronologyEvents, `all`))).json(), + getChronologyEventByID: async (id: string): Promise => + await (await request(payloadApiUrl(Collections.ChronologyEvents, id))).json(), }; diff --git a/src/utils/array.ts b/src/utils/array.ts new file mode 100644 index 0000000..c200515 --- /dev/null +++ b/src/utils/array.ts @@ -0,0 +1,11 @@ +export const groupBy = (array: T[], getKey: (item: T) => K): { key: K; values: T[] }[] => { + const map = new Map(); + 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 })); +}; diff --git a/src/utils/format.ts b/src/utils/format.ts index 10b3532..894dcb1 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -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(""); +};