diff --git a/src/graphql/fragments/reinCostume.graphql b/src/graphql/fragments/reinCostume.graphql
new file mode 100644
index 0000000..475ecbe
--- /dev/null
+++ b/src/graphql/fragments/reinCostume.graphql
@@ -0,0 +1,27 @@
+fragment reinCostume on ReinCostume {
+ slug
+ sprite {
+ data {
+ attributes {
+ ...uploadImage
+ }
+ }
+ }
+ translations {
+ id
+ language {
+ data {
+ attributes {
+ code
+ }
+ }
+ }
+ name
+ description
+ }
+ emblem {
+ data {
+ id
+ }
+ }
+}
diff --git a/src/graphql/operations/getReinCostumes.graphql b/src/graphql/operations/getReinCostumes.graphql
new file mode 100644
index 0000000..81a2f56
--- /dev/null
+++ b/src/graphql/operations/getReinCostumes.graphql
@@ -0,0 +1,34 @@
+query getReinCostumes {
+ reinCostumes(pagination: { limit: -1 }) {
+ data {
+ id
+ attributes {
+ slug
+ sprite {
+ data {
+ attributes {
+ ...uploadImage
+ }
+ }
+ }
+ translations {
+ id
+ language {
+ data {
+ attributes {
+ code
+ }
+ }
+ }
+ name
+ description
+ }
+ emblem {
+ data {
+ id
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/graphql/operations/getReinCostumesWithoutEmblem.graphql b/src/graphql/operations/getReinCostumesWithoutEmblem.graphql
new file mode 100644
index 0000000..2f70d11
--- /dev/null
+++ b/src/graphql/operations/getReinCostumesWithoutEmblem.graphql
@@ -0,0 +1,10 @@
+query getReinCostumesWithoutEmblem {
+ reinCostumes(filters: { emblem: { id: { null: true } } }, pagination: { limit: -1 }) {
+ data {
+ id
+ attributes {
+ ...reinCostume
+ }
+ }
+ }
+}
diff --git a/src/graphql/operations/getReinEmblem.graphql b/src/graphql/operations/getReinEmblem.graphql
new file mode 100644
index 0000000..7fe4082
--- /dev/null
+++ b/src/graphql/operations/getReinEmblem.graphql
@@ -0,0 +1,30 @@
+query getReinEmblem($slug: String) {
+ reinEmblems(filters: { slug: { eq: $slug } }) {
+ data {
+ id
+ attributes {
+ slug
+ translations {
+ id
+ name
+ description
+ language {
+ data {
+ attributes {
+ code
+ }
+ }
+ }
+ }
+ costumes {
+ data {
+ id
+ attributes {
+ ...reinCostume
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/graphql/operations/getReinEmblems.graphql b/src/graphql/operations/getReinEmblems.graphql
new file mode 100644
index 0000000..f423dfa
--- /dev/null
+++ b/src/graphql/operations/getReinEmblems.graphql
@@ -0,0 +1,22 @@
+query getReinEmblems {
+ reinEmblems(pagination: { limit: -1 }) {
+ data {
+ id
+ attributes {
+ slug
+ translations {
+ id
+ name
+ description
+ language {
+ data {
+ attributes {
+ code
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/graphql/operations/getReinEmblemsSlugs.graphql b/src/graphql/operations/getReinEmblemsSlugs.graphql
new file mode 100644
index 0000000..e662ef7
--- /dev/null
+++ b/src/graphql/operations/getReinEmblemsSlugs.graphql
@@ -0,0 +1,10 @@
+query getReinEmblemsSlugs {
+ reinEmblems(pagination: { limit: -1 }) {
+ data {
+ id
+ attributes {
+ slug
+ }
+ }
+ }
+}
diff --git a/src/pages/wiki/index.tsx b/src/pages/wiki/index.tsx
index d1e8e94..ad41755 100644
--- a/src/pages/wiki/index.tsx
+++ b/src/pages/wiki/index.tsx
@@ -167,6 +167,13 @@ const Wiki = (props: Props): JSX.Element => {
onClick={closeSubPanel}
border
/>
+
);
diff --git a/src/pages/wiki/rein/costumes.tsx b/src/pages/wiki/rein/costumes.tsx
new file mode 100644
index 0000000..db5286d
--- /dev/null
+++ b/src/pages/wiki/rein/costumes.tsx
@@ -0,0 +1,206 @@
+import { GetStaticProps } from "next";
+import { useCallback } from "react";
+import Markdown from "markdown-to-jsx";
+import { AppLayout, AppLayoutRequired } from "components/AppLayout";
+import { getFormat } from "helpers/i18n";
+import { getOpenGraph } from "helpers/openGraph";
+import { ReinCostume, ReinEmblemCostume } from "types/types";
+import { getReadySdk } from "graphql/sdk";
+import { filterHasAttributes, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
+import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
+import { TranslatedProps } from "types/TranslatedProps";
+import { useSmartLanguage } from "hooks/useSmartLanguage";
+import { prettySlug } from "helpers/formatters";
+import { ElementsSeparator } from "helpers/component";
+import { SubPanel } from "components/Containers/SubPanel";
+import { ReturnButton } from "components/PanelComponents/ReturnButton";
+import { useFormat } from "hooks/useFormat";
+import { PanelHeader } from "components/PanelComponents/PanelHeader";
+import { TranslatedNavOption } from "components/PanelComponents/NavOption";
+import { Img } from "components/Img";
+import { TranslatedPreviewCard } from "components/PreviewCard";
+
+/*
+ * ╭────────╮
+ * ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
+ */
+
+interface Props extends AppLayoutRequired {
+ emblems: ReinEmblemCostume[];
+}
+
+const ReincarnationCostumes = ({ emblems, ...otherProps }: Props): JSX.Element => {
+ const { format } = useFormat();
+
+ const subPanel = (
+
+
+ {[
+ <>
+
+
+
+ >,
+
+ <>
+ {emblems.map((emblem) => (
+ ({
+ language: translation.language.data.attributes.code,
+ title: translation.name,
+ }))}
+ fallback={{ title: prettySlug(emblem.attributes.slug) }}
+ url={`#${emblem.attributes.slug}`}
+ />
+ ))}
+ >,
+ ]}
+
+
+ );
+
+ const contentPanel = (
+
+
+
+ {emblems.map((emblem) => (
+ ({
+ language: translation.language.data.attributes.code,
+ title: translation.name,
+ description: translation.description,
+ }))}
+ fallback={{ title: prettySlug(emblem.attributes.slug) }}
+ slug={emblem.attributes.slug}
+ costumes={emblem.attributes.costumes}
+ />
+ ))}
+
+
+
+ );
+
+ return ;
+};
+export default ReincarnationCostumes;
+
+/*
+ * ╭──────────────────────╮
+ * ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
+ */
+
+export const getStaticProps: GetStaticProps = async (context) => {
+ const sdk = getReadySdk();
+ const { format } = getFormat(context.locale);
+
+ const emblems = (await sdk.getReinEmblems()).reinEmblems?.data;
+ const costumes = (await sdk.getReinCostumes()).reinCostumes?.data;
+
+ if (isUndefined(emblems) || isUndefined(costumes)) {
+ return { notFound: true };
+ }
+
+ const processedEmblems: ReinEmblemCostume[] = [];
+
+ filterHasAttributes(emblems, ["id", "attributes"] as const).forEach(({ id, attributes }) => {
+ const costumesOfCurrentEmblem = costumes.filter(
+ (costume) => costume.attributes?.emblem?.data?.id === id
+ );
+ const emblemCostume: ReinEmblemCostume = {
+ id,
+ attributes: { ...attributes, costumes: costumesOfCurrentEmblem },
+ };
+ processedEmblems.push(emblemCostume);
+ });
+
+ const costumesWithoutEmblem = costumes.filter((costume) =>
+ isUndefined(costume.attributes?.emblem?.data)
+ );
+
+ processedEmblems.push({
+ id: "others",
+ attributes: { slug: "others", costumes: costumesWithoutEmblem },
+ });
+
+ const props: Props = {
+ emblems: processedEmblems,
+ openGraph: getOpenGraph(format, format("costume", { count: Infinity })),
+ };
+ return {
+ props: props,
+ };
+};
+
+/*
+ * ╭──────────────────────╮
+ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
+ */
+
+interface EmblemCostumeProps {
+ slug: string;
+ title: string;
+ description?: string;
+ costumes: ReinCostume[];
+}
+
+const EmblemCostume = ({ slug, title, description, costumes }: EmblemCostumeProps): JSX.Element => (
+
+
+ {title}
+
+ {isDefinedAndNotEmpty(description) &&
{description}}
+
+ {costumes.map((costume) => (
+ ({
+ language: translation.language.data.attributes.code,
+ title: translation.name,
+ }))}
+ thumbnail={costume.attributes?.sprite?.data?.attributes}
+ keepInfoVisible
+ thumbnailAspectRatio="1/1"
+ thumbnailForceAspectRatio
+ thumbnailFitMethod="contain"
+ fallback={{ title: prettySlug(costume.attributes?.slug) }}
+ href="#"
+ />
+ ))}
+
+
+);
+
+export const TranslatedEmblemCostume = ({
+ translations,
+ fallback,
+ ...otherProps
+}: TranslatedProps): JSX.Element => {
+ const [selectedTranslation] = useSmartLanguage({
+ items: translations,
+ languageExtractor: useCallback((item: { language: string }): string => item.language, []),
+ });
+ return (
+
+ );
+};
diff --git a/src/pages/wiki/rein/emblems/[slug].tsx b/src/pages/wiki/rein/emblems/[slug].tsx
new file mode 100644
index 0000000..884d541
--- /dev/null
+++ b/src/pages/wiki/rein/emblems/[slug].tsx
@@ -0,0 +1,148 @@
+import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
+import Markdown from "markdown-to-jsx";
+import { useCallback } from "react";
+import { getReadySdk } from "graphql/sdk";
+import { filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
+import { AppLayout, AppLayoutRequired } from "components/AppLayout";
+import { useFormat } from "hooks/useFormat";
+import { GetReinEmblemQuery, ReinCostumeFragment } from "graphql/generated";
+import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
+import { Img } from "components/Img";
+import { TranslatedProps } from "types/TranslatedProps";
+import { useSmartLanguage } from "hooks/useSmartLanguage";
+import { prettySlug } from "helpers/formatters";
+import { getFormat } from "helpers/i18n";
+import { getOpenGraph } from "helpers/openGraph";
+
+/*
+ * ╭────────╮
+ * ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
+ */
+
+interface Props extends AppLayoutRequired {
+ emblem: NonNullable<
+ NonNullable["data"][number]>["attributes"]
+ >;
+}
+
+const ReinEmblem = ({ emblem, ...otherProps }: Props): JSX.Element => {
+ const { format } = useFormat();
+
+ const contentPanel = (
+
+
+ ({
+ language: translation.language.data.attributes.code,
+ title: translation.name,
+ description: translation.description,
+ }))}
+ fallback={{ title: prettySlug(emblem.slug) }}
+ slug={emblem.slug}
+ costumes={filterHasAttributes(emblem.costumes?.data, ["attributes"] as const).map(
+ (costume) => costume.attributes
+ )}
+ />
+
+
+ );
+
+ return ;
+};
+export default ReinEmblem;
+
+/*
+ * ╭──────────────────────╮
+ * ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
+ */
+
+export const getStaticProps: GetStaticProps = async (context) => {
+ const sdk = getReadySdk();
+ const { format } = getFormat(context.locale);
+
+ const slug =
+ context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
+ const emblem = (await sdk.getReinEmblem({ slug })).reinEmblems?.data?.[0]?.attributes;
+
+ if (isUndefined(emblem)) {
+ return { notFound: true };
+ }
+
+ const props: Props = {
+ emblem,
+ openGraph: getOpenGraph(format, format("costume", { count: Infinity })),
+ };
+ return {
+ props: props,
+ };
+};
+
+// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
+
+export const getStaticPaths: GetStaticPaths = async (context) => {
+ const sdk = getReadySdk();
+ const contents = await sdk.getReinEmblemsSlugs();
+ const paths: GetStaticPathsResult["paths"] = [];
+ filterHasAttributes(contents.reinEmblems?.data, ["attributes"] as const).map((emblem) => {
+ context.locales?.map((local) =>
+ paths.push({
+ params: { slug: emblem.attributes.slug },
+ locale: local,
+ })
+ );
+ });
+ return {
+ paths,
+ fallback: "blocking",
+ };
+};
+
+/*
+ * ╭──────────────────────╮
+ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
+ */
+
+interface EmblemCostumeProps {
+ slug: string;
+ title: string;
+ description?: string;
+ costumes: ReinCostumeFragment[];
+}
+
+const EmblemCostume = ({ slug, title, description, costumes }: EmblemCostumeProps): JSX.Element => (
+
+
+ {title}
+
+ {isDefinedAndNotEmpty(description) &&
{description}}
+
+ {costumes.map((costume) => (
+
+ {costume.sprite?.data?.attributes &&
}
+
{costume.translations?.[0]?.name}
+
{costume.translations?.[0]?.description ?? ""}
+
+ ))}
+
+
+);
+
+export const TranslatedEmblemCostume = ({
+ translations,
+ fallback,
+ ...otherProps
+}: TranslatedProps): JSX.Element => {
+ const [selectedTranslation] = useSmartLanguage({
+ items: translations,
+ languageExtractor: useCallback((item: { language: string }): string => item.language, []),
+ });
+ return (
+
+ );
+};
diff --git a/src/types/types.ts b/src/types/types.ts
index 9a41097..e6da9aa 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -2,6 +2,8 @@ import {
GetChronicleQuery,
GetContentTextQuery,
GetPostQuery,
+ GetReinCostumesQuery,
+ GetReinEmblemsQuery,
GetWeaponQuery,
GetWikiPageQuery,
} from "graphql/generated";
@@ -68,6 +70,17 @@ export type WeaponGroupPreview = NonNullable<
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
+type ReinEmblem = NonNullable["data"][number];
+
+export type ReinCostume = NonNullable["data"][number];
+
+export type ReinEmblemCostume = {
+ id: ReinEmblem["id"];
+ attributes: NonNullable & { costumes: ReinCostume[] };
+};
+
+// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
+
export enum LibraryItemUserStatus {
None = 0,
Want = 1,