import { AppLayout } from "components/AppLayout"; import { Chip } from "components/Chip"; import { Select } from "components/Inputs/Select"; import { Switch } from "components/Inputs/Switch"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { ContentPanel, ContentPanelWidthSizes, } from "components/Panels/ContentPanel"; import { SubPanel } from "components/Panels/SubPanel"; import { TranslatedPreviewCard } from "components/PreviewCard"; import { GetContentsQuery } from "graphql/generated"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { getReadySdk } from "graphql/sdk"; import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { Immutable } from "helpers/types"; import { GetStaticPropsContext } from "next"; import { Fragment, useEffect, useState } from "react"; import { Icon } from "components/Ico"; import { WithLabel } from "components/Inputs/WithLabel"; import { Button } from "components/Inputs/Button"; import { TextInput } from "components/Inputs/TextInput"; interface Props extends AppStaticProps { contents: NonNullable<GetContentsQuery["contents"]>["data"]; } type GroupContentItems = Map<string, Immutable<Props["contents"]>>; const defaultFiltersState = { groupingMethod: -1, keepInfoVisible: false, combineRelatedContent: true, searchName: "", }; export default function Contents(props: Immutable<Props>): JSX.Element { const { langui, contents, languages } = props; const [groupingMethod, setGroupingMethod] = useState<number>( defaultFiltersState.groupingMethod ); const [keepInfoVisible, setKeepInfoVisible] = useState( defaultFiltersState.keepInfoVisible ); const [combineRelatedContent, setCombineRelatedContent] = useState( defaultFiltersState.combineRelatedContent ); const [searchName, setSearchName] = useState(defaultFiltersState.searchName); const [effectiveCombineRelatedContent, setEffectiveCombineRelatedContent] = useState(true); const [filteredItems, setFilteredItems] = useState( filterContents(contents, combineRelatedContent, searchName) ); const [groups, setGroups] = useState<GroupContentItems>( getGroups(langui, groupingMethod, filteredItems) ); useEffect(() => { if (searchName.length > 1) { setEffectiveCombineRelatedContent(false); } else { setEffectiveCombineRelatedContent(combineRelatedContent); } setFilteredItems( filterContents(contents, effectiveCombineRelatedContent, searchName) ); }, [ effectiveCombineRelatedContent, contents, searchName, combineRelatedContent, ]); useEffect(() => { setGroups(getGroups(langui, groupingMethod, filteredItems)); }, [langui, groupingMethod, filteredItems]); const subPanel = ( <SubPanel> <PanelHeader icon={Icon.Workspaces} title={langui.contents} description={langui.contents_description} /> <TextInput className="mb-6 w-full" placeholder={langui.search_title ?? undefined} state={searchName} setState={setSearchName} /> <WithLabel label={langui.group_by} input={ <Select className="w-full" options={[langui.category ?? "", langui.type ?? ""]} state={groupingMethod} setState={setGroupingMethod} allowEmpty /> } /> <WithLabel label={langui.combine_related_contents} disabled={searchName.length > 1} input={ <Switch setState={setCombineRelatedContent} state={effectiveCombineRelatedContent} /> } /> <WithLabel label={langui.always_show_info} input={<Switch setState={setKeepInfoVisible} state={keepInfoVisible} />} /> <Button className="mt-8" text={langui.reset_all_filters} icon={Icon.Replay} onClick={() => { setSearchName(defaultFiltersState.searchName); setGroupingMethod(defaultFiltersState.groupingMethod); setKeepInfoVisible(defaultFiltersState.keepInfoVisible); setCombineRelatedContent(defaultFiltersState.combineRelatedContent); }} /> </SubPanel> ); const contentPanel = ( <ContentPanel width={ContentPanelWidthSizes.Large}> {[...groups].map(([name, items]) => ( <Fragment key={name}> {items.length > 0 && ( <Fragment> {name && ( <h2 key={`h2${name}`} className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl first-of-type:pt-0" > {name} <Chip>{`${items.reduce((currentSum, item) => { if (effectiveCombineRelatedContent) { if (item.attributes?.group?.data?.attributes?.combine) { return ( currentSum + (item.attributes.group.data.attributes.contents?.data .length ?? 1) ); } } return currentSum + 1; }, 0)} ${ items.length <= 1 ? langui.result?.toLowerCase() ?? "" : langui.results?.toLowerCase() ?? "" }`}</Chip> </h2> )} <div key={`items${name}`} className="grid grid-cols-2 items-end gap-8 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4" > {items.map((item) => ( <Fragment key={item.id}> {item.attributes && ( <TranslatedPreviewCard href={`/contents/${item.attributes.slug}`} translations={item.attributes.translations?.map( (translation) => ({ pre_title: translation?.pre_title, title: translation?.title, subtitle: translation?.subtitle, language: translation?.language?.data?.attributes?.code, }) )} slug={item.attributes.slug} languages={languages} thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2" stackNumber={ effectiveCombineRelatedContent && item.attributes.group?.data?.attributes?.combine ? item.attributes.group.data.attributes.contents ?.data.length : 0 } topChips={ item.attributes.type?.data?.attributes ? [ item.attributes.type.data.attributes.titles?.[0] ? item.attributes.type.data.attributes .titles[0]?.title : prettySlug( item.attributes.type.data.attributes.slug ), ] : undefined } bottomChips={item.attributes.categories?.data.map( (category) => category.attributes?.short ?? "" )} keepInfoVisible={keepInfoVisible} /> )} </Fragment> ))} </div> </Fragment> )} </Fragment> ))} </ContentPanel> ); return ( <AppLayout navTitle={langui.contents} subPanel={subPanel} contentPanel={contentPanel} subPanelIcon={Icon.Search} {...props} /> ); } export async function getStaticProps( context: GetStaticPropsContext ): Promise<{ notFound: boolean } | { props: Props }> { const sdk = getReadySdk(); const contents = await sdk.getContents({ language_code: context.locale ?? "en", }); if (!contents.contents) return { notFound: true }; contents.contents.data.sort((a, b) => { const titleA = a.attributes?.translations?.[0] ? prettyinlineTitle( a.attributes.translations[0].pre_title, a.attributes.translations[0].title, a.attributes.translations[0].subtitle ) : a.attributes?.slug ?? ""; const titleB = b.attributes?.translations?.[0] ? prettyinlineTitle( b.attributes.translations[0].pre_title, b.attributes.translations[0].title, b.attributes.translations[0].subtitle ) : b.attributes?.slug ?? ""; return titleA.localeCompare(titleB); }); const props: Props = { ...(await getAppStaticProps(context)), contents: contents.contents.data, }; return { props: props, }; } function getGroups( langui: AppStaticProps["langui"], groupByType: number, items: Immutable<Props["contents"]> ): GroupContentItems { switch (groupByType) { case 0: { const group = new Map(); group.set("Drakengard 1", []); group.set("Drakengard 1.3", []); group.set("Drakengard 2", []); group.set("Drakengard 3", []); group.set("Drakengard 4", []); group.set("NieR Gestalt", []); group.set("NieR Replicant", []); group.set("NieR Replicant ver.1.22474487139...", []); group.set("NieR:Automata", []); group.set("NieR Re[in]carnation", []); group.set("SINoALICE", []); group.set("Voice of Cards", []); group.set("Final Fantasy XIV", []); group.set("Thou Shalt Not Die", []); group.set("Bakuken", []); group.set("YoRHa", []); group.set("YoRHa Boys", []); group.set(langui.no_category, []); items.map((item) => { if (item.attributes?.categories?.data.length === 0) { group.get(langui.no_category)?.push(item); } else { item.attributes?.categories?.data.map((category) => { group.get(category.attributes?.name)?.push(item); }); } }); return group; } case 1: { const group = new Map(); items.map((item) => { const type = item.attributes?.type?.data?.attributes?.titles?.[0]?.title ?? item.attributes?.type?.data?.attributes?.slug ? prettySlug(item.attributes.type.data.attributes.slug) : langui.no_type; if (!group.has(type)) group.set(type, []); group.get(type)?.push(item); }); return group; } default: { const group: GroupContentItems = new Map(); group.set("", items); return group; } } } function filterContents( contents: Immutable<Props["contents"]>, combineRelatedContent: boolean, searchName: string ): Immutable<Props["contents"]> { return contents.filter((content) => { if ( combineRelatedContent && content.attributes?.group?.data?.attributes?.combine && content.attributes.group.data.attributes.contents?.data[0].id !== content.id ) { return false; } if (searchName.length > 1) { if ( content.attributes?.translations?.find((translation) => prettyinlineTitle( translation?.pre_title, translation?.title, translation?.subtitle ) .toLowerCase() .includes(searchName.toLowerCase()) ) ) { return true; } return false; } return true; }); }