Chronology v2

This commit is contained in:
DrMint 2022-07-23 22:56:48 +02:00
parent a04f1b50c3
commit ac38f1dae0
16 changed files with 497 additions and 346 deletions

View File

@ -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 => (
<ToolTip
content={"Copy anchor link"}
trigger="mouseenter"
className="text-sm"
>
{/* TODO: Langui Copied! */}
<ToolTip content={"Copied! 👍"} trigger="click" className="text-sm">
<Ico
icon={Icon.Link}
className="transition-color cursor-pointer hover:text-dark"
onClick={() => {
navigator.clipboard.writeText(
`${
process.env.NEXT_PUBLIC_URL_SELF + window.location.pathname
}#${id}`
);
}}
/>
</ToolTip>
</ToolTip>
);

View File

@ -47,7 +47,7 @@ export const ChroniclesList = ({
</div> </div>
</div> </div>
<div <div
className="grid gap-4 overflow-hidden transition-[max-height]" className="grid gap-4 overflow-hidden transition-[max-height] duration-500"
style={{ maxHeight: isOpen ? `${8 * chronicles.length}rem` : 0 }} style={{ maxHeight: isOpen ? `${8 * chronicles.length}rem` : 0 }}
> >
{filterHasAttributes(chronicles, [ {filterHasAttributes(chronicles, [

View File

@ -19,6 +19,7 @@ interface Props {
localesIndex: number | undefined; localesIndex: number | undefined;
onLanguageChanged: (index: number) => void; onLanguageChanged: (index: number) => void;
size?: Parameters<typeof Button>[0]["size"]; size?: Parameters<typeof Button>[0]["size"];
showBadge?: boolean;
} }
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
@ -30,6 +31,7 @@ export const LanguageSwitcher = ({
languages, languages,
size, size,
onLanguageChanged, onLanguageChanged,
showBadge = true,
}: Props): JSX.Element => ( }: Props): JSX.Element => (
<ToolTip <ToolTip
content={ content={
@ -47,7 +49,7 @@ export const LanguageSwitcher = ({
} }
> >
<Button <Button
badgeNumber={locales.size > 1 ? locales.size : undefined} badgeNumber={showBadge && locales.size > 1 ? locales.size : undefined}
icon={Icon.Translate} icon={Icon.Translate}
size={size} size={size}
/> />

View File

@ -3,10 +3,8 @@ import { useRouter } from "next/router";
import React, { Fragment, useMemo } from "react"; import React, { Fragment, useMemo } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Ico, Icon } from "components/Ico";
import { Img } from "components/Img"; import { Img } from "components/Img";
import { InsetBox } from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import { ToolTip } from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
@ -14,6 +12,7 @@ import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others"; import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { AnchorShare } from "components/AnchorShare";
/* /*
* *
@ -86,7 +85,7 @@ export const Markdawn = ({
}) => ( }) => (
<h1 id={compProps.id} style={compProps.style}> <h1 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h1> </h1>
), ),
}, },
@ -99,7 +98,7 @@ export const Markdawn = ({
}) => ( }) => (
<h2 id={compProps.id} style={compProps.style}> <h2 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h2> </h2>
), ),
}, },
@ -112,7 +111,7 @@ export const Markdawn = ({
}) => ( }) => (
<h3 id={compProps.id} style={compProps.style}> <h3 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h3> </h3>
), ),
}, },
@ -125,7 +124,7 @@ export const Markdawn = ({
}) => ( }) => (
<h4 id={compProps.id} style={compProps.style}> <h4 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h4> </h4>
), ),
}, },
@ -138,7 +137,7 @@ export const Markdawn = ({
}) => ( }) => (
<h5 id={compProps.id} style={compProps.style}> <h5 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h5> </h5>
), ),
}, },
@ -151,7 +150,7 @@ export const Markdawn = ({
}) => ( }) => (
<h6 id={compProps.id} style={compProps.style}> <h6 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <AnchorShare id={compProps.id} />
</h6> </h6>
), ),
}, },
@ -383,33 +382,6 @@ const TocLevel = ({
); );
}; };
/*
*
* PRIVATE METHODS
*/
const HeaderToolTip = (props: { id: string }): JSX.Element => (
<ToolTip
content={"Copy anchor link"}
trigger="mouseenter"
className="text-sm"
>
<ToolTip content={"Copied! 👍"} trigger="click" className="text-sm">
<Ico
icon={Icon.Link}
className="transition-color cursor-pointer hover:text-dark"
onClick={() => {
navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_URL_SELF + window.location.pathname}#${
props.id
}`
);
}}
/>
</ToolTip>
</ToolTip>
);
/* /*
* *
* PRIVATE COMPONENTS * PRIVATE COMPONENTS

View File

@ -6,7 +6,7 @@ import { ChroniclePreview } from "./Chronicles/ChroniclePreview";
import { ChroniclesList } from "./Chronicles/ChroniclesList"; import { ChroniclesList } from "./Chronicles/ChroniclesList";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
type TranslatedProps<P, K extends keyof P> = Omit<P, K> & { export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & {
translations: (Pick<P, K> & { language: string })[]; translations: (Pick<P, K> & { language: string })[];
fallback: Pick<P, K>; fallback: Pick<P, K>;
}; };

View File

@ -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<GetChronologyItemsQuery["chronologyItems"]>["data"][number];
displayYear: boolean;
langui: AppStaticProps["langui"];
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const ChronologyItemComponent = ({
langui,
item,
displayYear,
}: Props): JSX.Element => (
<>
{isDefined(item.attributes) && (
<div
className="grid grid-cols-[4em] grid-rows-[auto_1fr] place-content-start
rounded-2xl py-4 px-8 target:my-4 target:bg-mid target:py-8"
id={generateAnchor(
item.attributes.year,
item.attributes.month,
item.attributes.day
)}
>
{displayYear && (
<p className="mt-[-.2em] text-lg font-bold">
{generateYear(item.attributes.displayed_date, item.attributes.year)}
</p>
)}
<p className="col-start-1 text-sm text-dark">
{generateDate(item.attributes.month, item.attributes.day)}
</p>
<div className="col-start-2 row-span-2 row-start-1 grid gap-4">
{item.attributes.events &&
filterHasAttributes(item.attributes.events, [
"id",
"translations",
] as const).map((event) => (
<Fragment key={event.id}>
<div className="m-0">
{filterDefined(event.translations).map(
(translation, translationIndex) => (
<Fragment key={translationIndex}>
<Fragment>
<div
className="grid
grid-flow-col place-content-start place-items-start gap-2"
>
{translation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && (
<ToolTip
content={getStatusDescription(
translation.status,
langui
)}
maxWidth={"20rem"}
>
<Chip text={translation.status} />
</ToolTip>
)}
{translation.title ? (
<h3>{translation.title}</h3>
) : (
""
)}
</div>
{translation.description && (
<p
className={
event.translations.length > 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}
</p>
)}
{translation.note ? (
<em>{`${langui.notes}: ${translation.note}`}</em>
) : (
""
)}
</Fragment>
</Fragment>
)
)}
<p className="mt-1 grid grid-flow-col gap-1 place-self-start text-xs text-dark">
{event.source?.data ? (
`(${event.source.data.attributes?.name})`
) : (
<div className="flex items-center gap-1">
<Ico icon={Icon.Warning} className="!text-sm" />
{langui.no_source_warning}
</div>
)}
</p>
</div>
</Fragment>
))}
</div>
</div>
)}
</>
);
/*
*
* 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;
};

View File

@ -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 => (
<div
className="rounded-2xl target:my-4 target:bg-mid target:py-4"
id={items.length > 1 ? year.toString() : undefined}
>
{items.map((item, index) => (
<ChronologyItemComponent
key={index}
item={item}
displayYear={index === 0}
langui={langui}
/>
))}
</div>
);

View File

@ -20,6 +20,13 @@ query getChronologyItems {
} }
} }
translations(pagination: { limit: -1 }) { translations(pagination: { limit: -1 }) {
language {
data {
attributes {
code
}
}
}
title title
description description
note note

View File

@ -7,6 +7,13 @@ query getEras {
starting_year starting_year
ending_year ending_year
title(pagination: { limit: -1 }) { title(pagination: { limit: -1 }) {
language {
data {
attributes {
code
}
}
}
title title
description description
} }

View File

@ -8,7 +8,12 @@ export const compareDate = (
if (isUndefined(a) || isUndefined(b)) { if (isUndefined(a) || isUndefined(b)) {
return 0; return 0;
} }
const dateA = (a.year ?? 99999) * 365 + (a.month ?? 12) * 31 + (a.day ?? 31); const dateA =
const dateB = (b.year ?? 99999) * 365 + (b.month ?? 12) * 31 + (b.day ?? 31); (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; return dateA - dateB;
}; };
export const datePickerToDate = (date: DatePickerFragment): Date =>
new Date(date.year ?? 0, date.month ? date.month - 1 : 0, date.day ?? 1);

View File

@ -1,18 +1,14 @@
import { AppStaticProps } from "../graphql/getAppStaticProps"; import { AppStaticProps } from "../graphql/getAppStaticProps";
import { convertPrice } from "./numbers"; import { convertPrice } from "./numbers";
import { isDefinedAndNotEmpty, isUndefined } from "./others"; import { isDefinedAndNotEmpty, isUndefined } from "./others";
import { datePickerToDate } from "./date";
import { DatePickerFragment, PricePickerFragment } from "graphql/generated"; import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
export const prettyDate = ( export const prettyDate = (
datePicker: DatePickerFragment, datePicker: DatePickerFragment,
locale = "en", locale = "en",
dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium"
): string => ): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle });
new Date(
datePicker.year ?? 0,
datePicker.month ?? 0,
datePicker.day ?? 1
).toLocaleString(locale, { dateStyle });
export const prettyPrice = ( export const prettyPrice = (
pricePicker: PricePickerFragment, pricePicker: PricePickerFragment,

View File

@ -25,11 +25,11 @@ export interface OpenGraph {
export const getOpenGraph = ( export const getOpenGraph = (
langui: AppStaticProps["langui"], langui: AppStaticProps["langui"],
title: string, title?: string | null | undefined,
description?: string | null | undefined, description?: string | null | undefined,
thumbnail?: UploadImageFragment | null | undefined thumbnail?: UploadImageFragment | null | undefined
): OpenGraph => ({ ): OpenGraph => ({
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) && ` - ${title}`}`, title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? ` - ${title}` : ""}`,
description: isDefinedAndNotEmpty(description) description: isDefinedAndNotEmpty(description)
? description ? description
: langui.default_description ?? "", : langui.default_description ?? "",

View File

@ -2,6 +2,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import getConfig from "next/config"; import getConfig from "next/config";
type RequestProps = type RequestProps =
| HookChronicle
| HookChronicleChapter
| HookChronology | HookChronology
| HookContent | HookContent
| HookContentGroup | 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 = { type ResponseMailProps = {
message: string; message: string;
revalidated: boolean; revalidated: boolean;
@ -208,6 +226,30 @@ const Revalidate = (
break; 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": { case "custom": {
paths.push(`${body.url}`); paths.push(`${body.url}`);
break; break;

View File

@ -3,15 +3,18 @@ import {
getPostStaticProps, getPostStaticProps,
PostStaticProps, PostStaticProps,
} from "graphql/getPostStaticProps"; } from "graphql/getPostStaticProps";
import { getOpenGraph } from "helpers/openGraph";
/* /*
* *
* PAGE * PAGE
*/ */
const Home = (props: PostStaticProps): JSX.Element => ( const Home = ({ langui, ...otherProps }: PostStaticProps): JSX.Element => (
<PostPage <PostPage
{...props} {...otherProps}
openGraph={getOpenGraph(langui)}
langui={langui}
prependBody={ prependBody={
<div className="grid w-full place-content-center place-items-center gap-5 text-center"> <div className="grid w-full place-content-center place-items-center gap-5 text-center">
<div <div

View File

@ -165,10 +165,10 @@ const Library = ({
case 1: { case 1: {
const priceA = a.attributes.price const priceA = a.attributes.price
? convertPrice(a.attributes.price, currencies[0]) ? convertPrice(a.attributes.price, currencies[0])
: 99999; : Infinity;
const priceB = b.attributes.price const priceB = b.attributes.price
? convertPrice(b.attributes.price, currencies[0]) ? convertPrice(b.attributes.price, currencies[0])
: 99999; : Infinity;
return priceA - priceB; return priceA - priceB;
} }
case 2: { case 2: {

View File

@ -1,21 +1,36 @@
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { Fragment, useMemo } from "react"; import { Fragment, useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { InsetBox } from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import { NavOption } from "components/PanelComponents/NavOption";
import { import {
ReturnButton, ReturnButton,
ReturnButtonType, ReturnButtonType,
} from "components/PanelComponents/ReturnButton"; } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Panels/ContentPanel"; import { ContentPanel } from "components/Panels/ContentPanel";
import { SubPanel } from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { ChronologyYearComponent } from "components/Wiki/Chronology/ChronologyYearComponent"; import {
import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated"; Enum_Componenttranslationschronologyitem_Status,
GetChronologyItemsQuery,
GetErasQuery,
} from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/others"; import {
filterHasAttributes,
getStatusDescription,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/others";
import { getOpenGraph } from "helpers/openGraph"; import { getOpenGraph } from "helpers/openGraph";
import { TranslatedNavOption, TranslatedProps } from "components/Translated";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { ToolTip } from "components/ToolTip";
import { Chip } from "components/Chip";
import { Ico, Icon } from "components/Ico";
import { AnchorShare } from "components/AnchorShare";
import { datePickerToDate } from "helpers/date";
/* /*
* *
@ -33,37 +48,9 @@ const Chronology = ({
chronologyItems, chronologyItems,
chronologyEras, chronologyEras,
langui, langui,
languages,
...otherProps ...otherProps
}: Props): JSX.Element => { }: Props): 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( const subPanel = useMemo(
() => ( () => (
<SubPanel> <SubPanel>
@ -78,16 +65,19 @@ const Chronology = ({
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map( {filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map(
(era) => ( (era) => (
<Fragment key={era.id}> <Fragment key={era.id}>
<NavOption <TranslatedNavOption
translations={filterHasAttributes(era.attributes.title, [
"language.data.attributes.code",
] as const).map((translation) => ({
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}`} 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 border
/> />
</Fragment> </Fragment>
@ -109,39 +99,34 @@ const Chronology = ({
className="mb-10" className="mb-10"
/> />
{chronologyItemYearGroups.map((era, eraIndex) => ( {filterHasAttributes(chronologyEras, ["attributes"] as const).map(
<Fragment key={eraIndex}> (era) => (
<InsetBox <TranslatedChronologyEra
id={chronologyEras[eraIndex].attributes?.slug} key={era.attributes.slug}
className="my-8 grid gap-4 text-center" id={era.attributes.slug}
> translations={filterHasAttributes(era.attributes.title, [
<h2 className="text-2xl"> "language.data.attributes.code",
{chronologyEras[eraIndex].attributes?.title?.[0] ] as const).map((translation) => ({
? chronologyEras[eraIndex].attributes?.title?.[0]?.title language: translation.language.data.attributes.code,
: prettySlug(chronologyEras[eraIndex].attributes?.slug)} title: translation.title,
</h2> description: translation.description,
<p className="whitespace-pre-line "> }))}
{chronologyEras[eraIndex].attributes?.title?.[0] fallback={{ title: prettySlug(era.attributes.slug) }}
? chronologyEras[eraIndex].attributes?.title?.[0]?.description chronologyItems={filterHasAttributes(chronologyItems, [
: ""} "attributes",
</p> ] as const).filter(
</InsetBox> (item) =>
{era.map((items, index) => ( item.attributes.year >= era.attributes.starting_year &&
<Fragment key={index}> item.attributes.year < era.attributes.ending_year
{items[0].attributes && isDefined(items[0].attributes.year) && ( )}
<ChronologyYearComponent langui={langui}
year={items[0].attributes.year} languages={languages}
items={items} />
langui={langui} )
/> )}
)}
</Fragment>
))}
</Fragment>
))}
</ContentPanel> </ContentPanel>
), ),
[chronologyEras, chronologyItemYearGroups, langui] [chronologyEras, chronologyItems, languages, langui]
); );
return ( return (
@ -149,6 +134,7 @@ const Chronology = ({
contentPanel={contentPanel} contentPanel={contentPanel}
subPanel={subPanel} subPanel={subPanel}
langui={langui} langui={langui}
languages={languages}
{...otherProps} {...otherProps}
/> />
); );
@ -180,3 +166,317 @@ export const getStaticProps: GetStaticProps = async (context) => {
props: props, 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 (
<div id={id}>
<InsetBox className="my-8 grid gap-4 text-center">
<h2 className="flex place-content-center gap-3 text-2xl">
{title}
<AnchorShare id={id} />
</h2>
{isDefinedAndNotEmpty(description) && (
<p className="whitespace-pre-line ">{description}</p>
)}
</InsetBox>
<div>
{yearGroups.map((item, index) => (
<ChronologyYear
key={index}
items={item}
langui={langui}
languages={languages}
/>
))}
</div>
</div>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const TranslatedChronologyEra = ({
translations,
fallback,
...otherProps
}: TranslatedProps<
Parameters<typeof ChronologyEra>[0],
"description" | "title"
>): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languageExtractor: (item: { language: string }): string => item.language,
});
return (
<ChronologyEra
title={selectedTranslation?.title ?? fallback.title}
description={selectedTranslation?.description ?? fallback.description}
{...otherProps}
/>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyYearProps {
items: NonNullable<Props["chronologyItems"]>;
langui: AppStaticProps["langui"];
languages: AppStaticProps["languages"];
}
const ChronologyYear = ({ items, langui, languages }: ChronologyYearProps) => (
<div
className="rounded-2xl target:my-4 target:bg-mid target:py-4"
id={generateAnchor(items[0].attributes?.year)}
>
{filterHasAttributes(items, ["attributes.events"] as const).map(
(item, index) => (
<ChronologyDate
key={index}
langui={langui}
languages={languages}
date={{
year: item.attributes.year,
month: item.attributes.month,
day: item.attributes.day,
displayYear: index === 0,
overwriteYear: item.attributes.displayed_date,
}}
events={item.attributes.events}
/>
)
)}
</div>
);
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyDateProps {
date: {
year: number;
month: number | null | undefined;
day: number | null | undefined;
displayYear: boolean;
overwriteYear?: string | null | undefined;
};
events: NonNullable<
NonNullable<
NonNullable<Props["chronologyItems"]>[number]["attributes"]
>["events"]
>;
langui: AppStaticProps["langui"];
languages: AppStaticProps["languages"];
}
export const ChronologyDate = ({
date,
events,
langui,
languages,
}: ChronologyDateProps): JSX.Element => {
const router = useRouter();
return (
<div
className="grid grid-cols-[4em] grid-rows-[auto_1fr]
gap-x-8 rounded-2xl py-4 px-8 target:my-4 target:bg-mid target:py-8"
id={generateAnchor(date.year, date.month, date.day)}
>
{date.displayYear && (
<p className="mt-5 text-right text-lg font-bold">
{isDefinedAndNotEmpty(date.overwriteYear)
? date.overwriteYear
: date.year}
</p>
)}
<p className="col-start-1 text-right text-sm text-dark">
{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",
})
: ""}
</p>
<div className="col-start-2 row-span-2 row-start-1 grid gap-4">
{filterHasAttributes(events, ["id", "translations"] as const).map(
(event) => (
<ChronologyEvent
id={generateAnchor(date.year, date.month, date.day)}
key={event.id}
event={event}
langui={langui}
languages={languages}
/>
)
)}
</div>
</div>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyEventProps {
event: NonNullable<
NonNullable<
NonNullable<
NonNullable<Props["chronologyItems"]>[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 (
<div>
{selectedTranslation && (
<>
<div className="mr-2 flex place-items-center gap-x-2">
<LanguageSwitcher
{...languageSwitcherProps}
size="small"
showBadge={false}
/>
{selectedTranslation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && (
<ToolTip
content={getStatusDescription(
selectedTranslation.status,
langui
)}
maxWidth={"20rem"}
>
<Chip text={selectedTranslation.status} />
</ToolTip>
)}
<p className="mt-[0.2rem] grid grid-flow-col gap-1 place-self-start text-xs text-dark">
{event.source?.data ? (
`(${event.source.data.attributes?.name})`
) : (
<div className="flex items-center gap-1">
<Ico icon={Icon.Warning} className="!text-sm" />
{langui.no_source_warning}
</div>
)}
</p>
<span className="flex-shrink">
<AnchorShare id={id} />
</span>
</div>
{selectedTranslation.title && (
<div className="mt-1 flex place-content-start place-items-start gap-2">
<h3 className="font-headers font-bold">
{selectedTranslation.title}
</h3>
</div>
)}
{selectedTranslation.description && (
<p className="whitespace-pre-line">
{selectedTranslation.description}
</p>
)}
{selectedTranslation.note && (
<em>{`${langui.notes}: ${selectedTranslation.note}`}</em>
)}
</>
)}
</div>
);
};
/*
*
* 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;
};