mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-03 21:15:35 +05:00
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
"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>
|
||
);
|
||
}
|