208 lines
8.1 KiB
TypeScript
208 lines
8.1 KiB
TypeScript
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();
|