280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
import { GetStaticProps } from "next";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useBoolean } from "usehooks-ts";
|
|
import { z } from "zod";
|
|
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
|
import { Switch } from "components/Inputs/Switch";
|
|
import { TextInput } from "components/Inputs/TextInput";
|
|
import { WithLabel } from "components/Inputs/WithLabel";
|
|
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
|
import { ReturnButton } from "components/PanelComponents/ReturnButton";
|
|
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
|
|
import { SubPanel } from "components/Containers/SubPanel";
|
|
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
|
import { getOpenGraph } from "helpers/openGraph";
|
|
import { HorizontalLine } from "components/HorizontalLine";
|
|
import { CustomSearchResponse, meiliSearch } from "helpers/search";
|
|
import { MeiliIndices, MeiliVideo } from "shared/meilisearch-graphql-typings/meiliTypes";
|
|
import { PreviewCard } from "components/PreviewCard";
|
|
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
|
import { getVideoThumbnailURL } from "helpers/videos";
|
|
import { useTypedRouter } from "hooks/useTypedRouter";
|
|
import { Select } from "components/Inputs/Select";
|
|
import { sendAnalytics } from "helpers/analytics";
|
|
import { Button } from "components/Inputs/Button";
|
|
import { Paginator } from "components/Containers/Paginator";
|
|
import { useFormat } from "hooks/useFormat";
|
|
import { getFormat } from "helpers/i18n";
|
|
|
|
/*
|
|
* ╭─────────────╮
|
|
* ────────────────────────────────────────╯ CONSTANTS ╰──────────────────────────────────────────
|
|
*/
|
|
|
|
const DEFAULT_FILTERS_STATE = {
|
|
searchName: "",
|
|
page: 1,
|
|
sortingMethod: 1,
|
|
onlyShowGone: false,
|
|
keepInfoVisible: true,
|
|
};
|
|
|
|
const queryParamSchema = z.object({
|
|
query: z.coerce.string().optional(),
|
|
page: z.coerce.number().positive().optional(),
|
|
sort: z.coerce.number().min(0).max(5).optional(),
|
|
gone: z.coerce.boolean().optional(),
|
|
});
|
|
|
|
/*
|
|
* ╭────────╮
|
|
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
|
*/
|
|
|
|
interface Props extends AppLayoutRequired {}
|
|
|
|
const Videos = ({ ...otherProps }: Props): JSX.Element => {
|
|
const { format } = useFormat();
|
|
const hoverable = useDeviceSupportsHover();
|
|
const router = useTypedRouter(queryParamSchema);
|
|
|
|
const sortingMethods = useMemo(
|
|
() => [
|
|
{ meiliAttribute: "sortable_published_date:asc", displayedName: format("oldest") },
|
|
{ meiliAttribute: "sortable_published_date:desc", displayedName: format("newest") },
|
|
{ meiliAttribute: "views:asc", displayedName: format("least_popular") },
|
|
{ meiliAttribute: "views:desc", displayedName: format("most_popular") },
|
|
{ meiliAttribute: "duration:asc", displayedName: format("shortest") },
|
|
{ meiliAttribute: "duration:desc", displayedName: format("longest") },
|
|
],
|
|
[format]
|
|
);
|
|
|
|
const {
|
|
value: keepInfoVisible,
|
|
toggle: toggleKeepInfoVisible,
|
|
setValue: setKeepInfoVisible,
|
|
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
|
|
|
const {
|
|
value: onlyShowGone,
|
|
toggle: toggleOnlyShowGone,
|
|
setValue: setOnlyShowGone,
|
|
} = useBoolean(router.query.gone ?? DEFAULT_FILTERS_STATE.onlyShowGone);
|
|
|
|
const [query, setQuery] = useState<string>(
|
|
router.query.query ?? DEFAULT_FILTERS_STATE.searchName
|
|
);
|
|
|
|
const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page);
|
|
|
|
const [sortingMethod, setSortingMethod] = useState<number>(
|
|
router.query.sort ?? DEFAULT_FILTERS_STATE.sortingMethod
|
|
);
|
|
|
|
const [videos, setVideos] = useState<CustomSearchResponse<MeiliVideo>>();
|
|
|
|
useEffect(() => {
|
|
const fetchVideos = async () => {
|
|
const currentSortingMethod = sortingMethods[sortingMethod];
|
|
const searchResult = await meiliSearch(MeiliIndices.VIDEOS, query, {
|
|
hitsPerPage: 25,
|
|
page,
|
|
attributesToRetrieve: [
|
|
"title",
|
|
"channel",
|
|
"uid",
|
|
"published_date",
|
|
"views",
|
|
"duration",
|
|
"description",
|
|
],
|
|
attributesToHighlight: ["title", "channel", "description"],
|
|
attributesToCrop: ["description"],
|
|
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
|
|
filter: onlyShowGone ? ["gone = true"] : undefined,
|
|
});
|
|
|
|
setVideos(searchResult);
|
|
};
|
|
fetchVideos();
|
|
}, [query, page, sortingMethod, onlyShowGone, sortingMethods]);
|
|
|
|
useEffect(() => {
|
|
if (router.isReady)
|
|
router.updateQuery({
|
|
page,
|
|
query,
|
|
sort: sortingMethod,
|
|
gone: onlyShowGone,
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [page, query, sortingMethod, onlyShowGone, router.isReady]);
|
|
|
|
useEffect(() => {
|
|
if (router.isReady) {
|
|
if (isDefined(router.query.page)) setPage(router.query.page);
|
|
if (isDefined(router.query.query)) setQuery(router.query.query);
|
|
if (isDefined(router.query.sort)) setSortingMethod(router.query.sort);
|
|
if (isDefined(router.query.gone)) setOnlyShowGone(router.query.gone);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [router.isReady]);
|
|
|
|
const subPanel = (
|
|
<SubPanel>
|
|
<ReturnButton
|
|
href="/archives/"
|
|
title={"Archives"}
|
|
displayOnlyOn={"3ColumnsLayout"}
|
|
className="mb-10"
|
|
/>
|
|
|
|
<PanelHeader
|
|
icon="movie"
|
|
title={format("videos")}
|
|
description={format("archives_description")}
|
|
/>
|
|
|
|
<HorizontalLine />
|
|
|
|
<TextInput
|
|
className="mb-6 w-full"
|
|
placeholder={format("search_title")}
|
|
value={query}
|
|
onChange={(newQuery) => {
|
|
setPage(1);
|
|
setQuery(newQuery);
|
|
if (isDefinedAndNotEmpty(newQuery)) {
|
|
sendAnalytics("Videos", "Change search term");
|
|
} else {
|
|
sendAnalytics("Videos", "Clear search term");
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<WithLabel label={format("order_by")}>
|
|
<Select
|
|
className="w-full"
|
|
options={sortingMethods.map((item) => item.displayedName)}
|
|
value={sortingMethod}
|
|
onChange={(newSort) => {
|
|
setPage(1);
|
|
setSortingMethod(newSort);
|
|
sendAnalytics(
|
|
"Videos",
|
|
`Change sorting method (${
|
|
sortingMethods.map((item) => item.meiliAttribute)[newSort]
|
|
})`
|
|
);
|
|
}}
|
|
/>
|
|
</WithLabel>
|
|
|
|
<WithLabel label={format("only_unavailable_videos")}>
|
|
<Switch
|
|
value={onlyShowGone}
|
|
onClick={() => {
|
|
toggleOnlyShowGone();
|
|
}}
|
|
/>
|
|
</WithLabel>
|
|
|
|
{hoverable && (
|
|
<WithLabel label={format("always_show_info")}>
|
|
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
|
</WithLabel>
|
|
)}
|
|
|
|
<Button
|
|
className="mt-8"
|
|
text={format("reset_all_filters")}
|
|
icon="settings_backup_restore"
|
|
onClick={() => {
|
|
setPage(1);
|
|
setOnlyShowGone(DEFAULT_FILTERS_STATE.onlyShowGone);
|
|
setPage(DEFAULT_FILTERS_STATE.page);
|
|
setQuery(DEFAULT_FILTERS_STATE.searchName);
|
|
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
|
|
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
|
sendAnalytics("Videos", "Reset all filters");
|
|
}}
|
|
/>
|
|
</SubPanel>
|
|
);
|
|
|
|
const contentPanel = (
|
|
<ContentPanel width={ContentPanelWidthSizes.Full}>
|
|
<Paginator page={page} onPageChange={setPage} totalNumberOfPages={videos?.totalPages}>
|
|
<div
|
|
className="grid grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-start
|
|
gap-x-6 gap-y-8">
|
|
{videos?.hits.map((item) => (
|
|
<PreviewCard
|
|
key={item.uid}
|
|
href={`/archives/videos/v/${item.uid}`}
|
|
title={item._formatted.title}
|
|
thumbnail={getVideoThumbnailURL(item.uid)}
|
|
thumbnailAspectRatio="16/9"
|
|
thumbnailForceAspectRatio
|
|
keepInfoVisible={keepInfoVisible}
|
|
metadata={{
|
|
releaseDate: item.published_date,
|
|
views: item.views,
|
|
author: item._formatted.channel?.data?.attributes?.title,
|
|
position: "Top",
|
|
}}
|
|
description={
|
|
item._matchesPosition.description && item._matchesPosition.description.length > 0
|
|
? item._formatted.description
|
|
: undefined
|
|
}
|
|
hoverlay={{
|
|
__typename: "Video",
|
|
duration: item.duration,
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Paginator>
|
|
</ContentPanel>
|
|
);
|
|
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
|
|
};
|
|
export default Videos;
|
|
|
|
/*
|
|
* ╭──────────────────────╮
|
|
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
|
*/
|
|
|
|
export const getStaticProps: GetStaticProps = (context) => {
|
|
const { format } = getFormat(context.locale);
|
|
const props: Props = {
|
|
openGraph: getOpenGraph(format, format("videos")),
|
|
};
|
|
return {
|
|
props: props,
|
|
};
|
|
};
|