import { GetStaticProps } from "next"; import { useMemo, useState } from "react"; import { useBoolean } from "usehooks-ts"; import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Switch } from "components/Inputs/Switch"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel"; import { SubPanel } from "components/Containers/SubPanel"; import { GetPostsPreviewQuery } from "graphql/generated"; import { getReadySdk } from "graphql/sdk"; import { prettySlug } from "helpers/formatters"; import { Icon } from "components/Ico"; import { WithLabel } from "components/Inputs/WithLabel"; import { TextInput } from "components/Inputs/TextInput"; import { Button } from "components/Inputs/Button"; import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others"; import { SmartList } from "components/SmartList"; import { getOpenGraph } from "helpers/openGraph"; import { compareDate } from "helpers/date"; import { TranslatedPreviewCard } from "components/PreviewCard"; import { HorizontalLine } from "components/HorizontalLine"; import { cIf } from "helpers/className"; import { getLangui } from "graphql/fetchLocalData"; import { sendAnalytics } from "helpers/analytics"; import { useIsTerminalMode } from "hooks/useIsTerminalMode"; import { Terminal } from "components/Cli/Terminal"; import { useLocalData } from "contexts/LocalDataContext"; import { useContainerQueries } from "contexts/ContainerQueriesContext"; /* * ╭─────────────╮ * ────────────────────────────────────────╯ CONSTANTS ╰────────────────────────────────────────── */ const DEFAULT_FILTERS_STATE = { searchName: "", keepInfoVisible: true, }; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ interface Props extends AppLayoutRequired { posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"]; } const News = ({ posts, ...otherProps }: Props): JSX.Element => { const { isContentPanelAtLeast4xl } = useContainerQueries(); const { langui } = useLocalData(); const hoverable = useDeviceSupportsHover(); const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName); const { value: keepInfoVisible, toggle: toggleKeepInfoVisible, setValue: setKeepInfoVisible, } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); const isTerminalMode = useIsTerminalMode(); const subPanel = useMemo( () => ( <SubPanel> <PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} /> <HorizontalLine /> <TextInput className="mb-6 w-full" placeholder={langui.search_title ?? "Search..."} value={searchName} onChange={(name) => { setSearchName(name); if (isDefinedAndNotEmpty(name)) { sendAnalytics("News", "Change search term"); } else { sendAnalytics("News", "Clear search term"); } }} /> {hoverable && ( <WithLabel label={langui.always_show_info}> <Switch value={keepInfoVisible} onClick={() => { toggleKeepInfoVisible(); sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`); }} /> </WithLabel> )} <Button className="mt-8" text={langui.reset_all_filters} icon={Icon.Replay} onClick={() => { setSearchName(DEFAULT_FILTERS_STATE.searchName); setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); sendAnalytics("News", "Reset all filters"); }} /> </SubPanel> ), [hoverable, keepInfoVisible, langui, searchName, setKeepInfoVisible, toggleKeepInfoVisible] ); const contentPanel = useMemo( () => ( <ContentPanel width={ContentPanelWidthSizes.Full}> <SmartList items={filterHasAttributes(posts, ["attributes", "id"] as const)} getItemId={(post) => post.id} renderItem={({ item: post }) => ( <TranslatedPreviewCard href={`/news/${post.attributes.slug}`} translations={filterHasAttributes(post.attributes.translations, [ "language.data.attributes.code", ] as const).map((translation) => ({ language: translation.language.data.attributes.code, title: translation.title, description: translation.excerpt, }))} fallback={{ title: prettySlug(post.attributes.slug) }} thumbnail={post.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2" thumbnailForceAspectRatio bottomChips={post.attributes.categories?.data.map( (category) => category.attributes?.short ?? "" )} keepInfoVisible={keepInfoVisible} metadata={{ releaseDate: post.attributes.date, releaseDateFormat: "long", position: "Top", }} /> )} className={cIf( isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-x-4 gap-y-6" )} searchingTerm={searchName} searchingBy={(post) => `${prettySlug(post.attributes.slug)} ${post.attributes.translations ?.map((translation) => translation?.title) .join(" ")}` } paginationItemPerPage={25} /> </ContentPanel> ), [keepInfoVisible, posts, searchName, isContentPanelAtLeast4xl] ); if (isTerminalMode) { return ( <Terminal parentPath="/" childrenPaths={filterHasAttributes(posts, ["attributes"] as const).map( (post) => post.attributes.slug )} /> ); } return ( <AppLayout subPanel={subPanel} contentPanel={contentPanel} subPanelIcon={Icon.Search} {...otherProps} /> ); }; export default News; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ NEXT DATA FETCHING ╰────────────────────────────────────── */ export const getStaticProps: GetStaticProps = async (context) => { const sdk = getReadySdk(); const langui = getLangui(context.locale); const posts = await sdk.getPostsPreview(); if (!posts.posts) return { notFound: true }; const props: Props = { posts: sortPosts(posts.posts.data), openGraph: getOpenGraph(langui, langui.news ?? "News"), }; return { props: props, }; }; /* * ╭───────────────────╮ * ─────────────────────────────────────╯ PRIVATE METHODS ╰─────────────────────────────────────── */ const sortPosts = (posts: Props["posts"]): Props["posts"] => posts.sort((a, b) => compareDate(a.attributes?.date, b.attributes?.date)).reverse();