From f2f628add0dfe24dec7838cf0433583142cd53af Mon Sep 17 00:00:00 2001 From: Radiquum Date: Fri, 22 Aug 2025 04:01:44 +0500 Subject: [PATCH] feat: Refactor: search #12 --- api-prox/index.ts | 2 +- api-prox/shared.ts | 2 +- app/api/config.ts | 11 +- app/pages/Search.tsx | 555 ++++++++++++++----------------------------- 4 files changed, 192 insertions(+), 378 deletions(-) diff --git a/api-prox/index.ts b/api-prox/index.ts index b653ef0..7bcd03c 100644 --- a/api-prox/index.ts +++ b/api-prox/index.ts @@ -18,7 +18,7 @@ app.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", req.headers.origin || "*"); res.header( "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent" + "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent, Api-Version" ); res.header("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"); next(); diff --git a/api-prox/shared.ts b/api-prox/shared.ts index 36144e0..f241fd3 100644 --- a/api-prox/shared.ts +++ b/api-prox/shared.ts @@ -1,7 +1,7 @@ export const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": - "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent", + "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent, Api-Version", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Cache-Control": "no-cache", }; diff --git a/app/api/config.ts b/app/api/config.ts index 957a4c9..317ea1f 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -52,11 +52,12 @@ export const ENDPOINTS = { }, filter: `${API_PREFIX}/filter`, search: { - profileList: `${API_PREFIX}/search/profile/list/`, - profileHistory: `${API_PREFIX}/search/history/`, - profileFavoriteCollection: `${API_PREFIX}/search/favoriteCollections/`, - profiles: `${API_PREFIX}/search/profiles/`, - releases: `${API_PREFIX}/search/releases/`, + profileList: `${API_PREFIX}/search/profile/list`, + profileHistory: `${API_PREFIX}/search/history`, + profileFavoriteCollection: `${API_PREFIX}/search/favoriteCollections`, + profileFavorites: `${API_PREFIX}/search/favorites`, + profiles: `${API_PREFIX}/search/profiles`, + releases: `${API_PREFIX}/search/releases`, }, statistic: { addHistory: `${API_PREFIX}/history/add`, diff --git a/app/pages/Search.tsx b/app/pages/Search.tsx index 8f880b9..47d61d3 100644 --- a/app/pages/Search.tsx +++ b/app/pages/Search.tsx @@ -4,16 +4,35 @@ import { useState, useEffect } from "react"; import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; -import { - Button, - Dropdown, - DropdownItem, - Modal, - ModalBody, - ModalFooter, - ModalHeader, -} from "flowbite-react"; +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 = [ { @@ -112,7 +131,9 @@ export function SearchPage() { const userStore = useUserStore(); const [query, setQuery] = useState(searchParams.get("query") || ""); const [params, setParams] = useState(null); - const [filtersModalOpen, setFiltersModalOpen] = useState(false); + const [content, setContent] = useState(null); + + const [HeaderH, setHeaderH] = useState(null); useEffect(() => { const queryParams = searchParams.get("params"); @@ -132,6 +153,11 @@ export function SearchPage() { searchBy: "name", }); } + + if (window) { + setHeaderH(document.querySelector("header").clientHeight); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -139,17 +165,115 @@ export function SearchPage() { 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 (
-
-
+
+
-
-
+
+
item.id == params.where).label}`} color="light" + className="w-full" > {whereMapping.map((item) => { return item.auth && !userStore.isAuth ? @@ -217,17 +336,18 @@ export function SearchPage() {
{searchByMapping[params.where] ? -
+
item.id == params.searchBy - ).label}` - } + ).label + }`} color="light" + className="w-full" > {searchByMapping[params.where].map((item) => { return ( @@ -251,350 +371,43 @@ export function SearchPage() {
-

query: {query}

-

params: {JSON.stringify(params)}

+
+ {error ? +
+
+

Произошла ошибка поиска

+
+
+ : <>} + + {data && data[0].related && } + {content ? + content.length > 0 ? + params.where == "profiles" ? + + : params.where == "collections" ? + + : + :
+ +

Странно, аниме не найдено, попробуйте другой запрос...

+
+ + : <>} + + {!content && !isLoading && !query && ( +
+ +

Введите ваш запрос что-бы найти любимый тайтл

+
+ )} + + {isLoading ? +
+ +
+ : ""} +
); } - -// import useSWRInfinite from "swr/infinite"; -// import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection"; -// import { RelatedSection } from "#/components/RelatedSection/RelatedSection"; -// import { Spinner } from "#/components/Spinner/Spinner"; -// import { useScrollPosition } from "#/hooks/useScrollPosition"; -// import { useUserStore } from "../store/auth"; -// import { Button, Dropdown, DropdownItem, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react"; -// import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection"; -// import { UserSection } from "#/components/UserSection/UserSection"; -// import { useSWRfetcher } from "#/api/utils"; - -// export function SearchPage() { -// const router = useRouter(); - -// const [searchVal, setSearchVal] = useState(searchParams.get("q") || ""); -// const [where, setWhere] = useState(searchParams.get("where") || "releases"); - -// const [list, setList] = useState(searchParams.get("list") || "watching"); -// const [filtersModalOpen, setFiltersModalOpen] = useState(false); - -// - -// const getKey = (pageIndex: number, previousPageData: any) => { -// if (where == "releases") { -// if (previousPageData && !previousPageData.releases.length) return null; -// } else { -// if (previousPageData && !previousPageData.content.length) return null; -// } - -// const url = new URL("/api/search", window.location.origin); -// url.searchParams.set("page", pageIndex.toString()); - -// if (userStore.token) { -// url.searchParams.set("token", userStore.token); -// } - -// if (where) { -// url.searchParams.set("where", where); -// } - -// if (where == "list" && list && ListsMapping.hasOwnProperty(list)) { -// url.searchParams.set("list", ListsMapping[list].id); -// } - -// url.searchParams.set("searchBy", TagMapping[searchBy].id); - -// if (query) { -// url.searchParams.set("q", query); -// return url.toString(); -// } -// return; -// }; - -// const { data, error, isLoading, size, setSize } = useSWRInfinite( -// getKey, -// useSWRfetcher, -// { initialSize: 2, revalidateFirstPage: false } -// ); - -// const [content, setContent] = useState(null); -// useEffect(() => { -// if (data) { -// let allReleases = []; -// if (where == "releases") { -// for (let i = 0; i < data.length; i++) { -// allReleases.push(...data[i].releases); -// } -// } else { -// for (let i = 0; i < data.length; i++) { -// allReleases.push(...data[i].content); -// } -// } -// setContent(allReleases); -// } -// // 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]); - -// function _executeSearch(value: string) { -// const Params = new URLSearchParams(window.location.search); -// Params.set("q", value); -// const url = new URL(`/search?${Params.toString()}`, window.location.origin); -// setContent(null); -// setQuery(value); -// router.push(url.toString()); -// } - -// useEffect(() => { -// if (searchVal && searchVal.length % 4 == 1) { -// _executeSearch(searchVal.trim()); -// } -// // eslint-disable-next-line react-hooks/exhaustive-deps -// }, [searchVal]); - -// if (error) -// return ( -//
-//
-//

Ошибка

-//

-// Произошла ошибка поиска. Попробуйте обновить страницу или зайдите -// позже. -//

-//
-//
-// ); - -// return ( -// <> -//
-//
{ -// e.preventDefault(); -// _executeSearch(searchVal.trim()); -// }} -// > - -//
-// -//
-//
-// {data && data[0].related && } -// {content ? -// content.length > 0 ? -// <> -// {where == "collections" ? -// -// : where == "profiles" ? -// -// : -// } -// -// :
-// -//

Странно, аниме не найдено, попробуйте другой запрос...

-//
-// : isLoading && ( -//
-// -//
-// ) -// } -// {!content && !isLoading && !query && ( -//
-// -//

Введите ваш запрос что-бы найти любимый тайтл

-//
-// )} -//
-// {( -// data && -// data.length > 1 && -// (where == "releases" ? -// data[data.length - 1].releases.length == 25 -// : data[data.length - 1].content.length == 25) -// ) ? -// -// : ""} -// -// -// ); -// } - -// const FiltersModal = (props: { -// isOpen: boolean; -// setIsOpen: any; -// where: string; -// setWhere: any; -// list: string; -// setList: any; -// isAuth: boolean; -// searchBy: string; -// setSearchBy: any; -// setContent: any; -// }) => { -// const router = useRouter(); -// const [where, setWhere] = useState(props.where); -// const [list, setList] = useState(props.list); -// const [searchBy, setSearchBy] = useState(props.searchBy); - -// function _cancel() { -// setWhere(props.where); -// setList(props.list); -// setSearchBy(props.searchBy); -// props.setIsOpen(false); -// } - -// function _accept() { -// const Params = new URLSearchParams(window.location.search); - -// if (props.where != where) { -// Params.set("where", where); -// props.setWhere(where); -// } - -// if (where == "list") { -// Params.set("list", list); -// props.setList(list); -// } else { -// Params.delete("list"); -// } - -// if (!["profiles", "collections"].includes(where)) { -// Params.set("searchBy", searchBy); -// props.setSearchBy(searchBy); -// } else { -// Params.delete("searchBy"); -// props.setSearchBy("name"); -// } - -// props.setContent(null); - -// const url = new URL(`/search?${Params.toString()}`, window.location.origin); -// router.push(url.toString()); -// } - -// return ( -// _cancel()}> -// Фильтры -// -//
-//
-//

Искать в

-// -// {Object.keys(WhereMapping).map((item) => { -// if ( -// ["list", "history", "collections", "favorites"].includes( -// item -// ) && -// !props.isAuth -// ) { -// return <>; -// } else { -// return ( -// setWhere(item)} -// key={`where--${item}`} -// > -// {WhereMapping[item]} -// -// ); -// } -// })} -// -//
-//
-// {props.isAuth && where == "list" && ListsMapping.hasOwnProperty(list) ? -//
-//
-//

Список

-// -// {Object.keys(ListsMapping).map((item) => { -// return ( -// setList(item)} -// key={`list--${item}`} -// > -// {ListsMapping[item].name} -// -// ); -// })} -// -//
-//
-// : ""} -// {!["profiles", "collections"].includes(where) ? -//
-//
-//

Искать по

-// -// {Object.keys(TagMapping).map((item) => { -// return ( -// setSearchBy(item)} -// key={`tag--${item}`} -// > -// {TagMapping[item].name} -// -// ); -// })} -// -//
-//
-// : ""} -//
-// -//
-// -// -//
-//
-//
-// ); -// };