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
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 }}
>
{filterHasAttributes(chronicles, [

View File

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

View File

@ -3,10 +3,8 @@ import { useRouter } from "next/router";
import React, { Fragment, useMemo } from "react";
import ReactDOMServer from "react-dom/server";
import { HorizontalLine } from "components/HorizontalLine";
import { Ico, Icon } from "components/Ico";
import { Img } from "components/Img";
import { InsetBox } from "components/InsetBox";
import { ToolTip } from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className";
@ -14,6 +12,7 @@ import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { useLightBox } from "hooks/useLightBox";
import { AnchorShare } from "components/AnchorShare";
/*
*
@ -86,7 +85,7 @@ export const Markdawn = ({
}) => (
<h1 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</h1>
),
},
@ -99,7 +98,7 @@ export const Markdawn = ({
}) => (
<h2 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</h2>
),
},
@ -112,7 +111,7 @@ export const Markdawn = ({
}) => (
<h3 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</h3>
),
},
@ -125,7 +124,7 @@ export const Markdawn = ({
}) => (
<h4 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</h4>
),
},
@ -138,7 +137,7 @@ export const Markdawn = ({
}) => (
<h5 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</h5>
),
},
@ -151,7 +150,7 @@ export const Markdawn = ({
}) => (
<h6 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
<AnchorShare id={compProps.id} />
</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

View File

@ -6,7 +6,7 @@ import { ChroniclePreview } from "./Chronicles/ChroniclePreview";
import { ChroniclesList } from "./Chronicles/ChroniclesList";
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 })[];
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 }) {
language {
data {
attributes {
code
}
}
}
title
description
note

View File

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

View File

@ -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);

View File

@ -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,

View File

@ -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 ?? "",

View File

@ -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;

View File

@ -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 => (
<PostPage
{...props}
{...otherProps}
openGraph={getOpenGraph(langui)}
langui={langui}
prependBody={
<div className="grid w-full place-content-center place-items-center gap-5 text-center">
<div

View File

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

View File

@ -1,21 +1,36 @@
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 { InsetBox } from "components/InsetBox";
import { NavOption } from "components/PanelComponents/NavOption";
import {
ReturnButton,
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Panels/ContentPanel";
import { SubPanel } from "components/Panels/SubPanel";
import { ChronologyYearComponent } from "components/Wiki/Chronology/ChronologyYearComponent";
import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated";
import {
Enum_Componenttranslationschronologyitem_Status,
GetChronologyItemsQuery,
GetErasQuery,
} from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk";
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 { 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,
chronologyEras,
langui,
languages,
...otherProps
}: 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(
() => (
<SubPanel>
@ -78,16 +65,19 @@ const Chronology = ({
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map(
(era) => (
<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}`}
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
/>
</Fragment>
@ -109,39 +99,34 @@ const Chronology = ({
className="mb-10"
/>
{chronologyItemYearGroups.map((era, eraIndex) => (
<Fragment key={eraIndex}>
<InsetBox
id={chronologyEras[eraIndex].attributes?.slug}
className="my-8 grid gap-4 text-center"
>
<h2 className="text-2xl">
{chronologyEras[eraIndex].attributes?.title?.[0]
? chronologyEras[eraIndex].attributes?.title?.[0]?.title
: prettySlug(chronologyEras[eraIndex].attributes?.slug)}
</h2>
<p className="whitespace-pre-line ">
{chronologyEras[eraIndex].attributes?.title?.[0]
? chronologyEras[eraIndex].attributes?.title?.[0]?.description
: ""}
</p>
</InsetBox>
{era.map((items, index) => (
<Fragment key={index}>
{items[0].attributes && isDefined(items[0].attributes.year) && (
<ChronologyYearComponent
year={items[0].attributes.year}
items={items}
langui={langui}
/>
{filterHasAttributes(chronologyEras, ["attributes"] as const).map(
(era) => (
<TranslatedChronologyEra
key={era.attributes.slug}
id={era.attributes.slug}
translations={filterHasAttributes(era.attributes.title, [
"language.data.attributes.code",
] as const).map((translation) => ({
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}
/>
)
)}
</Fragment>
))}
</Fragment>
))}
</ContentPanel>
),
[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 (
<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;
};