Chronology v2
This commit is contained in:
parent
a04f1b50c3
commit
ac38f1dae0
|
@ -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>
|
||||
);
|
|
@ -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, [
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 ╰──────────────────────────────────────
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -20,6 +20,13 @@ query getChronologyItems {
|
|||
}
|
||||
}
|
||||
translations(pagination: { limit: -1 }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
description
|
||||
note
|
||||
|
|
|
@ -7,6 +7,13 @@ query getEras {
|
|||
starting_year
|
||||
ending_year
|
||||
title(pagination: { limit: -1 }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
description
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ?? "",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{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}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue