AniX/app/pages/Search.tsx

413 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { Dropdown, DropdownItem } from "flowbite-react";
import { useUserStore } from "#/store/auth";
import { ENDPOINTS } from "#/api/config";
import { tryCatchAPI } from "#/api/utils";
import useSWRInfinite from "swr/infinite";
import { Spinner } from "#/components/Spinner/Spinner";
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
import { UserSection } from "#/components/UserSection/UserSection";
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { RelatedSection } from "#/components/RelatedSection/RelatedSection";
const postFetcher = async (url: string, payload: string) => {
const { data, error } = await tryCatchAPI(
fetch(url, {
method: "POST",
headers: {
"Api-Version": "v2",
"Content-Type": "application/json",
},
body: payload,
})
);
if (error) {
throw error;
}
return data;
};
const whereMapping = [
{
id: "releases",
label: "Релизах",
auth: false,
},
{
id: "profiles",
label: "Профилях",
auth: false,
},
{
id: "list",
label: "Списках",
auth: true,
},
{
id: "history",
label: "Истории",
auth: true,
},
{
id: "favorites",
label: "Избранном",
auth: true,
},
{
id: "collections",
label: "Коллекциях",
auth: true,
},
];
const searchByMapping = {
releases: [
{
id: "name",
label: "Названию",
value: 0,
},
{
id: "studio",
label: "Студии",
value: 1,
},
{
id: "director",
label: "Режиссёру",
value: 2,
},
{
id: "author",
label: "Автору",
value: 3,
},
{
id: "tag",
label: "Тегу",
value: 4,
},
],
list: [
{
id: "watching",
label: "Смотрю",
value: 1,
},
{
id: "planned",
label: "В планах",
value: 2,
},
{
id: "watched",
label: "Просмотрено",
value: 3,
},
{
id: "delayed",
label: "Отложено",
value: 4,
},
{
id: "abandoned",
label: "Заброшено",
value: 5,
},
],
none: [{ id: "none", label: "Нет", value: 0 }],
};
export function SearchPage() {
const router = useRouter();
const searchParams = useSearchParams();
const userStore = useUserStore();
const [query, setQuery] = useState(searchParams.get("query") || "");
const [params, setParams] = useState(null);
const [content, setContent] = useState(null);
const [HeaderH, setHeaderH] = useState(null);
useEffect(() => {
const queryParams = searchParams.get("params");
if (queryParams) {
try {
setParams(JSON.parse(queryParams));
} catch (e) {
setParams({
where: "releases",
searchBy: "name",
});
}
} else {
setParams({
where: "releases",
searchBy: "name",
});
}
if (window) {
setHeaderH(document.querySelector("header").clientHeight);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!params) return;
const url = new URL(`/search`, window.location.origin);
url.searchParams.set("query", query);
url.searchParams.set("params", JSON.stringify(params));
router.replace(url.toString());
setContent(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);
useEffect(() => {
setContent(null);
const url = new URL(`/search`, window.location.origin);
url.searchParams.set("query", query);
url.searchParams.set("params", JSON.stringify(params));
router.replace(url.toString());
}, [query]);
const getKey = (pageIndex: number, previousPageData: any) => {
if (!params) return null;
if (!query) return null;
if (previousPageData) {
if (params.where == "releases") {
if (!previousPageData.releases.length) return null;
} else {
if (!previousPageData.content.length) return null;
}
}
let url = null;
switch (params.where) {
case "releases":
url = `${ENDPOINTS.search.releases}/${pageIndex}`;
break;
case "profiles":
url = `${ENDPOINTS.search.profiles}/${pageIndex}`;
break;
case "list":
const list = searchByMapping[params.where].find(
(item) => item.id == params.searchBy
);
if (!list) break;
url = `${ENDPOINTS.search.profileList}/${list.value}/${pageIndex}`;
break;
case "history":
url = `${ENDPOINTS.search.profileHistory}/${pageIndex}`;
break;
case "favorites":
url = `${ENDPOINTS.search.profileFavorites}/${pageIndex}`;
break;
case "collections":
url = `${ENDPOINTS.search.profileFavoriteCollection}/${pageIndex}`;
break;
}
if (userStore.token) {
url += `?token=${userStore.token}`;
}
let searchBy = null;
const _sbym = searchByMapping[params.where];
if (_sbym) {
searchBy = _sbym.find((item) => item.id == params.searchBy).value;
} else {
searchBy = searchByMapping["none"][0].value;
}
return [url, JSON.stringify({ query, searchBy })];
};
const { data, error, isLoading, size, setSize, mutate } = useSWRInfinite(
getKey,
([url, payload]) => postFetcher(url, payload),
{ initialSize: 2 }
);
useEffect(() => {
if (data) {
let _content = [];
if (params.where == "releases") {
for (let i = 0; i < data.length; i++) {
_content.push(...data[i].releases);
}
} else {
for (let i = 0; i < data.length; i++) {
_content.push(...data[i].content);
}
}
setContent(_content);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const scrollPosition = useScrollPosition();
useEffect(() => {
if (scrollPosition >= 98 && scrollPosition <= 99) {
setSize(size + 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]);
if (!params) return <></>;
return (
<div>
<div
className="sticky top-0 sm:top-[var(--header-height)] z-50 flex flex-wrap w-full gap-2 bg-black bg-opacity-25 py-2 px-2 rounded-lg backdrop-blur-sm"
style={{ "--header-height": `${HeaderH}px` } as React.CSSProperties}
>
<div className="flex flex-col flex-1 w-full lg:flex-row">
<label
htmlFor="default-search"
className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
>
Search
</label>
<div className="relative w-full">
<div className="absolute inset-y-0 flex items-center pointer-events-none start-0 ps-3">
<svg
className="w-4 h-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<input
type="search"
id="default-search"
className="block w-full p-4 text-sm text-gray-900 border border-gray-300 rounded-lg ps-10 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Поиск аниме..."
required
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="flex gap-2 mt-2 lg:ml-2 lg:mt-0">
<div className="flex justify-between flex-1 gap-4">
<Dropdown
size="xl"
label={`Искать в: ${whereMapping.find((item) => item.id == params.where).label}`}
color="light"
className="w-full lg:w-fit"
>
{whereMapping.map((item) => {
return item.auth && !userStore.isAuth ?
<></>
: <DropdownItem
onClick={() =>
searchByMapping[item.id] ?
setParams({
where: item.id,
searchBy: searchByMapping[item.id][0].id,
})
: setParams({ where: item.id, searchBy: "none" })
}
key={`filter--where--${item.id}`}
>
{item.label}
</DropdownItem>;
})}
</Dropdown>
</div>
{searchByMapping[params.where] ?
<div className="flex justify-between flex-1 gap-4">
<Dropdown
size="xl"
label={`Искать по: ${
params.searchBy == "none" ?
searchByMapping.none[0].label
: searchByMapping[params.where].find(
(item) => item.id == params.searchBy
).label
}`}
color="light"
className="w-full lg:w-fit"
>
{searchByMapping[params.where].map((item) => {
return (
<DropdownItem
onClick={() =>
setParams({
where: params.where,
searchBy: item.id,
})
}
key={`filter--where--${params.where}--searchBy--${item.id}`}
>
{item.label}
</DropdownItem>
);
})}
</Dropdown>
</div>
: <></>}
</div>
</div>
</div>
<div>
{error ?
<div className="flex flex-col justify-between w-full p-4 border border-red-200 rounded-md md:flex-row bg-red-50 dark:bg-red-700 dark:border-red-600">
<div className="mb-4 md:mb-0 md:me-4">
<p>Произошла ошибка поиска</p>
</div>
</div>
: <></>}
{data && data[0].related && <RelatedSection {...data[0].related} />}
{content ?
content.length > 0 ?
params.where == "profiles" ?
<UserSection content={content} />
: params.where == "collections" ?
<CollectionsSection content={content} />
: <ReleaseSection content={content} />
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
<span className="w-24 h-24 iconify-color twemoji--crying-cat"></span>
<p>Странно, аниме не найдено, попробуйте другой запрос...</p>
</div>
: <></>}
{!content && !isLoading && !query && (
<div className="flex flex-col items-center justify-center min-w-full gap-2 mt-12 text-xl">
<span className="w-16 h-16 iconify mdi--arrow-up animate-bounce"></span>
<p>Введите ваш запрос что-бы найти любимый тайтл</p>
</div>
)}
{isLoading ?
<div className="flex items-center justify-center w-full h-16">
<Spinner />
</div>
: ""}
</div>
</div>
);
}