diff --git a/src/components/AnchorShare.tsx b/src/components/AnchorShare.tsx new file mode 100644 index 0000000..a367f1d --- /dev/null +++ b/src/components/AnchorShare.tsx @@ -0,0 +1,37 @@ +import { useRouter } from "next/router"; +import { Ico, Icon } from "./Ico"; +import { ToolTip } from "./ToolTip"; + +/* + * ╭─────────────╮ + * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── + */ + +interface Props { + id: string; +} + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +export const AnchorShare = ({ id }: Props): JSX.Element => ( + + {/* TODO: Langui Copied! */} + + { + navigator.clipboard.writeText( + `${ + process.env.NEXT_PUBLIC_URL_SELF + window.location.pathname + }#${id}` + ); + }} + /> + + +); diff --git a/src/components/Chronicles/ChroniclesList.tsx b/src/components/Chronicles/ChroniclesList.tsx index 1ba2bb1..c439e4f 100644 --- a/src/components/Chronicles/ChroniclesList.tsx +++ b/src/components/Chronicles/ChroniclesList.tsx @@ -47,7 +47,7 @@ export const ChroniclesList = ({
= Omit
& { +export type TranslatedProps
& { translations: (Pick
& { language: string })[]; fallback: Pick
; }; diff --git a/src/components/Wiki/Chronology/ChronologyItemComponent.tsx b/src/components/Wiki/Chronology/ChronologyItemComponent.tsx deleted file mode 100644 index ad1dbb5..0000000 --- a/src/components/Wiki/Chronology/ChronologyItemComponent.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Fragment } from "react"; -import { Chip } from "components/Chip"; -import { Ico, Icon } from "components/Ico"; -import { ToolTip } from "components/ToolTip"; -import { - Enum_Componenttranslationschronologyitem_Status, - GetChronologyItemsQuery, -} from "graphql/generated"; -import { AppStaticProps } from "graphql/getAppStaticProps"; -import { - filterDefined, - filterHasAttributes, - getStatusDescription, - isDefined, -} from "helpers/others"; - -/* - * ╭─────────────╮ - * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── - */ - -interface Props { - item: NonNullable["data"][number]; - displayYear: boolean; - langui: AppStaticProps["langui"]; -} - -// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ - -export const ChronologyItemComponent = ({ - langui, - item, - displayYear, -}: Props): JSX.Element => ( - <> - {isDefined(item.attributes) && ( - - {displayYear && ( - - {generateYear(item.attributes.displayed_date, item.attributes.year)} - - )} - - - {generateDate(item.attributes.month, item.attributes.day)} - - - - {item.attributes.events && - filterHasAttributes(item.attributes.events, [ - "id", - "translations", - ] as const).map((event) => ( - - - {filterDefined(event.translations).map( - (translation, translationIndex) => ( - - - - {translation.status !== - Enum_Componenttranslationschronologyitem_Status.Done && ( - - - - )} - {translation.title ? ( - {translation.title} - ) : ( - "" - )} - - - {translation.description && ( - 1 - ? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block - before:w-4 before:text-dark before:content-['-']` - : "whitespace-pre-line" - } - > - {translation.description} - - )} - {translation.note ? ( - {`${langui.notes}: ${translation.note}`} - ) : ( - "" - )} - - - ) - )} - - - {event.source?.data ? ( - `(${event.source.data.attributes?.name})` - ) : ( - - - {langui.no_source_warning} - - )} - - - - ))} - - - )} - > -); - -/* - * ╭───────────────────╮ - * ─────────────────────────────────────╯ PRIVATE METHODS ╰─────────────────────────────────────── - */ - -const generateAnchor = ( - year: number | undefined, - month: number | null | undefined, - day: number | null | undefined -): string => { - let result = ""; - if (year) result += year; - if (month) result += `- ${month.toString().padStart(2, "0")}`; - if (day) result += `- ${day.toString().padStart(2, "0")}`; - return result; -}; - -const generateYear = ( - displayed_date: string | null | undefined, - year: number | undefined -): string => displayed_date ?? year?.toString() ?? ""; - -const generateDate = ( - month: number | null | undefined, - day: number | null | undefined -): string => { - const lut = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ]; - - let result = ""; - if (month && month >= 1 && month <= 12) { - result += lut[month - 1]; - if (day) { - result += ` ${day}`; - } - } - - return result; -}; diff --git a/src/components/Wiki/Chronology/ChronologyYearComponent.tsx b/src/components/Wiki/Chronology/ChronologyYearComponent.tsx deleted file mode 100644 index e9777bc..0000000 --- a/src/components/Wiki/Chronology/ChronologyYearComponent.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ChronologyItemComponent } from "components/Wiki/Chronology/ChronologyItemComponent"; -import { GetChronologyItemsQuery } from "graphql/generated"; -import { AppStaticProps } from "graphql/getAppStaticProps"; - -/* - * ╭─────────────╮ - * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── - */ - -interface Props { - year: number; - items: NonNullable< - GetChronologyItemsQuery["chronologyItems"] - >["data"][number][]; - langui: AppStaticProps["langui"]; -} - -// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ - -export const ChronologyYearComponent = ({ - langui, - year, - items, -}: Props): JSX.Element => ( - 1 ? year.toString() : undefined} - > - {items.map((item, index) => ( - - ))} - -); diff --git a/src/graphql/operations/getChronologyItems.graphql b/src/graphql/operations/getChronologyItems.graphql index fb8ff20..ae4a126 100644 --- a/src/graphql/operations/getChronologyItems.graphql +++ b/src/graphql/operations/getChronologyItems.graphql @@ -20,6 +20,13 @@ query getChronologyItems { } } translations(pagination: { limit: -1 }) { + language { + data { + attributes { + code + } + } + } title description note diff --git a/src/graphql/operations/getEras.graphql b/src/graphql/operations/getEras.graphql index f2a0f36..e3ba249 100644 --- a/src/graphql/operations/getEras.graphql +++ b/src/graphql/operations/getEras.graphql @@ -7,6 +7,13 @@ query getEras { starting_year ending_year title(pagination: { limit: -1 }) { + language { + data { + attributes { + code + } + } + } title description } diff --git a/src/helpers/date.ts b/src/helpers/date.ts index 7d77f4c..de12ef7 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -8,7 +8,12 @@ export const compareDate = ( if (isUndefined(a) || isUndefined(b)) { return 0; } - const dateA = (a.year ?? 99999) * 365 + (a.month ?? 12) * 31 + (a.day ?? 31); - const dateB = (b.year ?? 99999) * 365 + (b.month ?? 12) * 31 + (b.day ?? 31); + const dateA = + (a.year ?? Infinity) * 365 + (a.month ?? 12) * 31 + (a.day ?? 31); + const dateB = + (b.year ?? Infinity) * 365 + (b.month ?? 12) * 31 + (b.day ?? 31); return dateA - dateB; }; + +export const datePickerToDate = (date: DatePickerFragment): Date => + new Date(date.year ?? 0, date.month ? date.month - 1 : 0, date.day ?? 1); diff --git a/src/helpers/formatters.ts b/src/helpers/formatters.ts index 7345426..3bbf1f1 100644 --- a/src/helpers/formatters.ts +++ b/src/helpers/formatters.ts @@ -1,18 +1,14 @@ import { AppStaticProps } from "../graphql/getAppStaticProps"; import { convertPrice } from "./numbers"; import { isDefinedAndNotEmpty, isUndefined } from "./others"; +import { datePickerToDate } from "./date"; import { DatePickerFragment, PricePickerFragment } from "graphql/generated"; export const prettyDate = ( datePicker: DatePickerFragment, locale = "en", dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" -): string => - new Date( - datePicker.year ?? 0, - datePicker.month ?? 0, - datePicker.day ?? 1 - ).toLocaleString(locale, { dateStyle }); +): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle }); export const prettyPrice = ( pricePicker: PricePickerFragment, diff --git a/src/helpers/openGraph.ts b/src/helpers/openGraph.ts index 3687f86..717ea1f 100644 --- a/src/helpers/openGraph.ts +++ b/src/helpers/openGraph.ts @@ -25,11 +25,11 @@ export interface OpenGraph { export const getOpenGraph = ( langui: AppStaticProps["langui"], - title: string, + title?: string | null | undefined, description?: string | null | undefined, thumbnail?: UploadImageFragment | null | undefined ): OpenGraph => ({ - title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) && ` - ${title}`}`, + title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? ` - ${title}` : ""}`, description: isDefinedAndNotEmpty(description) ? description : langui.default_description ?? "", diff --git a/src/pages/api/revalidate.ts b/src/pages/api/revalidate.ts index 4dbc839..ff16706 100644 --- a/src/pages/api/revalidate.ts +++ b/src/pages/api/revalidate.ts @@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; import getConfig from "next/config"; type RequestProps = + | HookChronicle + | HookChronicleChapter | HookChronology | HookContent | HookContentGroup @@ -83,6 +85,22 @@ type HookWiki = { }; }; +type HookChronicle = { + event: "entry.create" | "entry.delete" | "entry.update"; + model: "chronicle"; + entry: { + slug: string; + }; +}; + +type HookChronicleChapter = { + event: "entry.create" | "entry.delete" | "entry.update"; + model: "chronicles-chapter"; + entry: { + chronicles: { slug: string }[]; + }; +}; + type ResponseMailProps = { message: string; revalidated: boolean; @@ -208,6 +226,30 @@ const Revalidate = ( break; } + case "chronicle": { + paths.push(`/chronicles`); + paths.push(`/chronicles/${body.entry.slug}`); + serverRuntimeConfig.locales?.map((locale: string) => { + paths.push(`/${locale}/chronicles`); + paths.push(`/${locale}/chronicles/${body.entry.slug}`); + }); + break; + } + + case "chronicles-chapter": { + paths.push(`/chronicles`); + serverRuntimeConfig.locales?.map((locale: string) => { + paths.push(`/${locale}/chronicles`); + }); + body.entry.chronicles.map((chronicle) => { + paths.push(`/chronicles/${chronicle.slug}`); + serverRuntimeConfig.locales?.map((locale: string) => { + paths.push(`/${locale}/chronicles/${chronicle.slug}`); + }); + }); + break; + } + case "custom": { paths.push(`${body.url}`); break; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0574986..b87b32b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,15 +3,18 @@ import { getPostStaticProps, PostStaticProps, } from "graphql/getPostStaticProps"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -const Home = (props: PostStaticProps): JSX.Element => ( +const Home = ({ langui, ...otherProps }: PostStaticProps): JSX.Element => ( { - // Group by year the Chronology items - const chronologyItemYearGroups = useMemo(() => { - const memo: Props["chronologyItems"][number][][][] = []; - chronologyEras.map(() => { - memo.push([]); - }); - - let currentChronologyEraIndex = 0; - chronologyItems.map((item) => { - if (item.attributes) { - if ( - item.attributes.year > - (chronologyEras[currentChronologyEraIndex].attributes?.ending_year ?? - 999999) - ) { - currentChronologyEraIndex++; - } - if ( - Object.hasOwn(memo[currentChronologyEraIndex], item.attributes.year) - ) { - memo[currentChronologyEraIndex][item.attributes.year].push(item); - } else { - memo[currentChronologyEraIndex][item.attributes.year] = [item]; - } - } - }); - return memo; - }, [chronologyEras, chronologyItems]); - const subPanel = useMemo( () => ( @@ -78,16 +65,19 @@ const Chronology = ({ {filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map( (era) => ( - ({ + language: translation.language.data.attributes.code, + title: translation.title, + subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`, + }))} + fallback={{ + title: prettySlug(era.attributes.slug), + subtitle: `${era.attributes.starting_year} → ${era.attributes.ending_year}`, + }} url={`#${era.attributes.slug}`} - title={ - era.attributes.title && - era.attributes.title.length > 0 && - era.attributes.title[0] - ? era.attributes.title[0].title - : prettySlug(era.attributes.slug) - } - subtitle={`${era.attributes.starting_year} → ${era.attributes.ending_year}`} border /> @@ -109,39 +99,34 @@ const Chronology = ({ className="mb-10" /> - {chronologyItemYearGroups.map((era, eraIndex) => ( - - - - {chronologyEras[eraIndex].attributes?.title?.[0] - ? chronologyEras[eraIndex].attributes?.title?.[0]?.title - : prettySlug(chronologyEras[eraIndex].attributes?.slug)} - - - {chronologyEras[eraIndex].attributes?.title?.[0] - ? chronologyEras[eraIndex].attributes?.title?.[0]?.description - : ""} - - - {era.map((items, index) => ( - - {items[0].attributes && isDefined(items[0].attributes.year) && ( - - )} - - ))} - - ))} + {filterHasAttributes(chronologyEras, ["attributes"] as const).map( + (era) => ( + ({ + language: translation.language.data.attributes.code, + title: translation.title, + description: translation.description, + }))} + fallback={{ title: prettySlug(era.attributes.slug) }} + chronologyItems={filterHasAttributes(chronologyItems, [ + "attributes", + ] as const).filter( + (item) => + item.attributes.year >= era.attributes.starting_year && + item.attributes.year < era.attributes.ending_year + )} + langui={langui} + languages={languages} + /> + ) + )} ), - [chronologyEras, chronologyItemYearGroups, langui] + [chronologyEras, chronologyItems, languages, langui] ); return ( @@ -149,6 +134,7 @@ const Chronology = ({ contentPanel={contentPanel} subPanel={subPanel} langui={langui} + languages={languages} {...otherProps} /> ); @@ -180,3 +166,317 @@ export const getStaticProps: GetStaticProps = async (context) => { props: props, }; }; + +/* + * ╭──────────────────────╮ + * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── + */ + +interface ChronologyEraProps { + id: string; + title: string; + description?: string | null | undefined; + chronologyItems: Props["chronologyItems"]; + langui: AppStaticProps["langui"]; + languages: AppStaticProps["languages"]; +} + +const ChronologyEra = ({ + id, + title, + description, + chronologyItems, + langui, + languages, +}: ChronologyEraProps) => { + const yearGroups = useMemo(() => { + const memo: Props["chronologyItems"][] = []; + let currentYear = -Infinity; + filterHasAttributes(chronologyItems, ["attributes"] as const).forEach( + (item) => { + if (currentYear === item.attributes.year) { + memo[memo.length - 1].push(item); + } else { + currentYear = item.attributes.year; + memo.push([item]); + } + } + ); + return memo; + }, [chronologyItems]); + + return ( + + + + {title} + + + + {isDefinedAndNotEmpty(description) && ( + {description} + )} + + + {yearGroups.map((item, index) => ( + + ))} + + + ); +}; + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +const TranslatedChronologyEra = ({ + translations, + fallback, + ...otherProps +}: TranslatedProps< + Parameters[0], + "description" | "title" +>): JSX.Element => { + const [selectedTranslation] = useSmartLanguage({ + items: translations, + languageExtractor: (item: { language: string }): string => item.language, + }); + + return ( + + ); +}; + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +interface ChronologyYearProps { + items: NonNullable; + langui: AppStaticProps["langui"]; + languages: AppStaticProps["languages"]; +} + +const ChronologyYear = ({ items, langui, languages }: ChronologyYearProps) => ( + + {filterHasAttributes(items, ["attributes.events"] as const).map( + (item, index) => ( + + ) + )} + +); + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +interface ChronologyDateProps { + date: { + year: number; + month: number | null | undefined; + day: number | null | undefined; + displayYear: boolean; + overwriteYear?: string | null | undefined; + }; + events: NonNullable< + NonNullable< + NonNullable[number]["attributes"] + >["events"] + >; + langui: AppStaticProps["langui"]; + languages: AppStaticProps["languages"]; +} + +export const ChronologyDate = ({ + date, + events, + langui, + languages, +}: ChronologyDateProps): JSX.Element => { + const router = useRouter(); + return ( + + {date.displayYear && ( + + {isDefinedAndNotEmpty(date.overwriteYear) + ? date.overwriteYear + : date.year} + + )} + + + {isDefined(date.month) + ? isDefined(date.day) + ? datePickerToDate({ + year: date.year, + month: date.month, + day: date.day, + }).toLocaleDateString(router.locale, { + month: "short", + day: "numeric", + }) + : datePickerToDate({ + year: date.year, + month: date.month, + day: date.day, + }).toLocaleDateString(router.locale, { + month: "short", + }) + : ""} + + + + {filterHasAttributes(events, ["id", "translations"] as const).map( + (event) => ( + + ) + )} + + + ); +}; + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +interface ChronologyEventProps { + event: NonNullable< + NonNullable< + NonNullable< + NonNullable[number]["attributes"] + >["events"] + >[number] + >; + langui: AppStaticProps["langui"]; + languages: AppStaticProps["languages"]; + id: string; +} + +export const ChronologyEvent = ({ + event, + langui, + languages, + id, +}: ChronologyEventProps): JSX.Element => { + const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = + useSmartLanguage({ + items: event.translations ?? [], + languageExtractor: useCallback( + ( + item: NonNullable< + ChronologyEventProps["event"]["translations"] + >[number] + ) => item?.language?.data?.attributes?.code, + [] + ), + languages: languages, + }); + + return ( + + {selectedTranslation && ( + <> + + + + {selectedTranslation.status !== + Enum_Componenttranslationschronologyitem_Status.Done && ( + + + + )} + + + {event.source?.data ? ( + `(${event.source.data.attributes?.name})` + ) : ( + + + {langui.no_source_warning} + + )} + + + + + + + + {selectedTranslation.title && ( + + + {selectedTranslation.title} + + + )} + + {selectedTranslation.description && ( + + {selectedTranslation.description} + + )} + + {selectedTranslation.note && ( + {`${langui.notes}: ${selectedTranslation.note}`} + )} + > + )} + + ); +}; + +/* + * ╭───────────────────╮ + * ─────────────────────────────────────╯ PRIVATE METHODS ╰─────────────────────────────────────── + */ + +const generateAnchor = ( + year: number | undefined, + month?: number | null | undefined, + day?: number | null | undefined +): string => { + let result = ""; + if (isDefined(year)) result += year; + if (isDefined(month)) result += `-${month.toString().padStart(2, "0")}`; + if (isDefined(day)) result += `-${day.toString().padStart(2, "0")}`; + return result; +};
- {generateYear(item.attributes.displayed_date, item.attributes.year)} -
- {generateDate(item.attributes.month, item.attributes.day)} -
1 - ? `mt-2 whitespace-pre-line before:ml-[-1em] before:inline-block - before:w-4 before:text-dark before:content-['-']` - : "whitespace-pre-line" - } - > - {translation.description} -
- {event.source?.data ? ( - `(${event.source.data.attributes?.name})` - ) : ( -
- {chronologyEras[eraIndex].attributes?.title?.[0] - ? chronologyEras[eraIndex].attributes?.title?.[0]?.description - : ""} -
{description}
+ {isDefinedAndNotEmpty(date.overwriteYear) + ? date.overwriteYear + : date.year} +
+ {isDefined(date.month) + ? isDefined(date.day) + ? datePickerToDate({ + year: date.year, + month: date.month, + day: date.day, + }).toLocaleDateString(router.locale, { + month: "short", + day: "numeric", + }) + : datePickerToDate({ + year: date.year, + month: date.month, + day: date.day, + }).toLocaleDateString(router.locale, { + month: "short", + }) + : ""} +
+ {event.source?.data ? ( + `(${event.source.data.attributes?.name})` + ) : ( +
+ {selectedTranslation.description} +