Merge pull request #16 from Radiquum/refactor__search
Some checks failed
Build and Publish 'anix-api-prox' to Docker Hub / publish (push) Waiting to run
Build and Publish 'anix' to Docker Hub / publish (push) Waiting to run
Build and Publish 'anix-player-parsers' to Docker Hub / publish (push) Has been cancelled

v3.8.0
This commit is contained in:
Kentai Radiquum 2025-08-22 04:50:02 +05:00 committed by GitHub
commit e64118a2a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 402 additions and 440 deletions

View file

@ -18,7 +18,7 @@ app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin || "*"); res.header("Access-Control-Allow-Origin", req.headers.origin || "*");
res.header( res.header(
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Sign" "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent, Api-Version"
); );
res.header("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"); res.header("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS");
next(); next();

View file

@ -1,7 +1,7 @@
export const corsHeaders = { export const corsHeaders = {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Access-Control-Allow-Headers":
"Origin, X-Requested-With, Content-Type, Accept, Sign", "Origin, X-Requested-With, Content-Type, Accept, Sign, Allow, User-Agent, Api-Version",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
}; };

View file

@ -1,4 +1,4 @@
export const CURRENT_APP_VERSION = "3.7.0"; export const CURRENT_APP_VERSION = "3.8.0";
import { env } from "next-runtime-env"; import { env } from "next-runtime-env";
const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null; const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null;
@ -51,7 +51,14 @@ export const ENDPOINTS = {
} }
}, },
filter: `${API_PREFIX}/filter`, filter: `${API_PREFIX}/filter`,
search: `${API_URL}/search`, search: {
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: { statistic: {
addHistory: `${API_PREFIX}/history/add`, addHistory: `${API_PREFIX}/history/add`,
markWatched: `${API_PREFIX}/episode/watch`, markWatched: `${API_PREFIX}/episode/watch`,

View file

@ -1,74 +0,0 @@
import { NextResponse } from "next/server";
import { NextRequest } from "next/server";
import { fetchDataViaPost } from "../utils";
import { ENDPOINTS } from "../config";
export async function GET(request: NextRequest) {
const page = parseInt(request.nextUrl.searchParams.get("page")) || 0;
const query = decodeURI(request.nextUrl.searchParams.get("q")) || null;
const token = request.nextUrl.searchParams.get("token") || null;
const where = request.nextUrl.searchParams.get("where") || "releases";
const searchBy = parseInt(request.nextUrl.searchParams.get("searchBy")) || 0;
const list = parseInt(request.nextUrl.searchParams.get("list")) || null;
let url: URL;
if (where == "list") {
if (!list) {
return NextResponse.json(
{ message: "List ID required" },
{ status: 400 }
);
}
if (!token) {
return NextResponse.json({ message: "token required" }, { status: 400 });
}
url = new URL(`${ENDPOINTS.search}/profile/list/${list}/${page}`);
} else if (where == "history") {
if (!token) {
return NextResponse.json({ message: "token required" }, { status: 400 });
}
url = new URL(`${ENDPOINTS.search}/history/${page}`);
} else if (where == "favorites") {
if (!token) {
return NextResponse.json({ message: "token required" }, { status: 400 });
}
url = new URL(`${ENDPOINTS.search}/favorites/${page}`);
} else if (where == "collections") {
if (!token) {
return NextResponse.json({ message: "token required" }, { status: 400 });
}
url = new URL(`${ENDPOINTS.search}/favoriteCollections/${page}`);
} else if (where == "profiles") {
url = new URL(`${ENDPOINTS.search}/profiles/${page}`);
} else {
url = new URL(`${ENDPOINTS.search}/releases/${page}`);
}
if (token) {
url.searchParams.set("token", token);
}
const body = { query, searchBy };
const { data, error } = await fetchDataViaPost(
url.toString(),
JSON.stringify(body),
true
);
if (error) {
return new Response(JSON.stringify(error), {
status: 500,
headers: {
"Content-Type": "application/json",
},
});
}
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}

View file

@ -2,6 +2,8 @@ export const metadata = {
title: "Закладки", title: "Закладки",
}; };
export const dynamic = "force-static";
import { BookmarksPage } from "#/pages/Bookmarks"; import { BookmarksPage } from "#/pages/Bookmarks";
export default function Index() { export default function Index() {
return <BookmarksPage />; return <BookmarksPage />;

View file

@ -1,7 +1,7 @@
import { ViewCollectionPage } from "#/pages/ViewCollection"; import { ViewCollectionPage } from "#/pages/ViewCollection";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/collection/${id}` `${API_URL}/collection/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;

View file

@ -5,6 +5,8 @@ export const metadata = {
description: "Создание новой коллекции", description: "Создание новой коллекции",
}; };
export const dynamic = "force-static";
export default function Collections() { export default function Collections() {
return <CreateCollectionPage />; return <CreateCollectionPage />;
} }

View file

@ -5,6 +5,8 @@ export const metadata = {
description: "Просмотр избранных коллекций", description: "Просмотр избранных коллекций",
}; };
export const dynamic = "force-static";
export default function Collections() { export default function Collections() {
return <CollectionsFullPage type="favorites" title="Избранные коллекции" />; return <CollectionsFullPage type="favorites" title="Избранные коллекции" />;
} }

View file

@ -5,6 +5,8 @@ export const metadata = {
description: "Просмотр и управление коллекциями", description: "Просмотр и управление коллекциями",
} }
export const dynamic = "force-static";
export default function Collections() { export default function Collections() {
return <CollectionsPage />; return <CollectionsPage />;
} }

View file

@ -15,7 +15,7 @@ export const RelatedSection = (props: any) => {
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
{props.images.map((item, index) => { {props.images.map((item, index) => {
return ( return (
<div key={`related-img-${index}`} className="w-[100px] lg:w-[300px] aspect-[9/12] even:scale-110 shadow-md even:shadow-lg even:z-30 origin-center first:[transform:translateX(25%)] last:[transform:translateX(-25%)]"> <div key={`related-img-${index}`} className="w-[100px] lg:w-[300px] aspect-[9/12] even:scale-110 shadow-md even:[box-shadow:_0px_0px_16px_black;] even:z-30 origin-center first:[transform:translateX(25%)] last:[transform:translateX(-25%)] rounded-lg overflow-hidden">
<Image <Image
fill={true} fill={true}
src={item} src={item}

View file

@ -4,6 +4,8 @@ export const metadata = {
import { FavoritesPage } from "#/pages/Favorites"; import { FavoritesPage } from "#/pages/Favorites";
export const dynamic = "force-static";
export default function Index() { export default function Index() {
return <FavoritesPage />; return <FavoritesPage />;
} }

View file

@ -4,6 +4,8 @@ export const metadata = {
import { HistoryPage } from "#/pages/History"; import { HistoryPage } from "#/pages/History";
export const dynamic = "force-static";
export default function Index() { export default function Index() {
return <HistoryPage />; return <HistoryPage />;
} }

View file

@ -5,6 +5,8 @@ export const metadata = {
description: "Вход в аккаунт anixart", description: "Вход в аккаунт anixart",
} }
export const dynamic = "force-static";
export default function Login() { export default function Login() {
return <LoginPage />; return <LoginPage />;
} }

View file

@ -4,6 +4,8 @@ export const metadata = {
import { MenuPage } from "#/pages/MobileMenuPage"; import { MenuPage } from "#/pages/MobileMenuPage";
export const dynamic = "force-static";
export default function Index() { export default function Index() {
return <MenuPage />; return <MenuPage />;
} }

View file

@ -1,5 +1,7 @@
import { IndexPage } from "./pages/Index"; import { IndexPage } from "./pages/Index";
export const dynamic = "force-static";
export default function Index() { export default function Index() {
return <IndexPage />; return <IndexPage />;
} }

View file

@ -1,138 +1,258 @@
"use client"; "use client";
import useSWRInfinite from "swr/infinite";
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
import { RelatedSection } from "#/components/RelatedSection/RelatedSection";
import { Spinner } from "#/components/Spinner/Spinner";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useUserStore } from "../store/auth"; import { useRouter } from "next/navigation";
import { Button, Dropdown, DropdownItem, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection"; 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 { UserSection } from "#/components/UserSection/UserSection";
import { useSWRfetcher } from "#/api/utils"; import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { RelatedSection } from "#/components/RelatedSection/RelatedSection";
const ListsMapping = { const postFetcher = async (url: string, payload: string) => {
watching: { const { data, error } = await tryCatchAPI(
name: "Смотрю", fetch(url, {
id: 1, method: "POST",
}, headers: {
planned: { "Api-Version": "v2",
name: "В планах", "Content-Type": "application/json",
id: 2, },
}, body: payload,
watched: { })
name: "Просмотрено", );
id: 3,
}, if (error) {
delayed: { throw error;
name: "Отложено", }
id: 4, return data;
},
abandoned: {
name: "Заброшено",
id: 5,
},
}; };
const TagMapping = { const whereMapping = [
name: { {
name: "Названию", id: "releases",
id: 0, label: "Релизах",
auth: false,
}, },
studio: { {
name: "Студии", id: "profiles",
id: 1, label: "Профилях",
auth: false,
}, },
director: { {
name: "Режиссёру", id: "list",
id: 2, label: "Списках",
auth: true,
}, },
author: { {
name: "Автору", id: "history",
id: 3, label: "Истории",
auth: true,
}, },
tag: { {
name: "Тегу", id: "favorites",
id: 4, label: "Избранном",
auth: true,
}, },
}; {
id: "collections",
label: "Коллекциях",
auth: true,
},
];
const WhereMapping = { const searchByMapping = {
releases: "Релизах", releases: [
list: "Списках", {
history: "Истории", id: "name",
favorites: "Избранном", label: "Названию",
collections: "Коллекциях", value: 0,
profiles: "Профилях", },
{
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() { export function SearchPage() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get("q") || "");
const [searchVal, setSearchVal] = useState(searchParams.get("q") || "");
const [where, setWhere] = useState(searchParams.get("where") || "releases");
const [searchBy, setSearchBy] = useState(
searchParams.get("searchBy") || "name"
);
const [list, setList] = useState(searchParams.get("list") || "watching");
const [filtersModalOpen, setFiltersModalOpen] = useState(false);
const userStore = useUserStore(); 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) => { const getKey = (pageIndex: number, previousPageData: any) => {
if (where == "releases") { if (!params) return null;
if (previousPageData && !previousPageData.releases.length) return null; if (!query) return null;
} else {
if (previousPageData && !previousPageData.content.length) return null; if (previousPageData) {
if (params.where == "releases") {
if (!previousPageData.releases.length) return null;
} else {
if (!previousPageData.content.length) return null;
}
} }
const url = new URL("/api/search", window.location.origin); let url = null;
url.searchParams.set("page", pageIndex.toString()); 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) { if (userStore.token) {
url.searchParams.set("token", userStore.token); url += `?token=${userStore.token}`;
} }
if (where) { let searchBy = null;
url.searchParams.set("where", where); const _sbym = searchByMapping[params.where];
if (_sbym) {
searchBy = _sbym.find((item) => item.id == params.searchBy).value;
} else {
searchBy = searchByMapping["none"][0].value;
} }
if (where == "list" && list && ListsMapping.hasOwnProperty(list)) { return [url, JSON.stringify({ query, searchBy })];
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( const { data, error, isLoading, size, setSize, mutate } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, ([url, payload]) => postFetcher(url, payload),
{ initialSize: 2, revalidateFirstPage: false } { initialSize: 2 }
); );
const [content, setContent] = useState(null);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
let allReleases = []; let _content = [];
if (where == "releases") { if (params.where == "releases") {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
allReleases.push(...data[i].releases); _content.push(...data[i].releases);
} }
} else { } else {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
allReleases.push(...data[i].content); _content.push(...data[i].content);
} }
} }
setContent(allReleases); setContent(_content);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]); }, [data]);
@ -145,52 +265,22 @@ export function SearchPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]); }, [scrollPosition]);
function _executeSearch(value: string) { if (!params) return <></>;
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 (
<main className="flex items-center justify-center min-h-screen">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold">Ошибка</h1>
<p className="text-lg">
Произошла ошибка поиска. Попробуйте обновить страницу или зайдите
позже.
</p>
</div>
</main>
);
return ( return (
<> <div>
<div className="flex flex-wrap items-center gap-2"> <div
<form 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"
className="flex-1 max-w-full mx-auto" style={{ "--header-height": `${HeaderH}px` } as React.CSSProperties}
onSubmit={(e) => { >
e.preventDefault(); <div className="flex flex-col flex-1 w-full lg:flex-row">
_executeSearch(searchVal.trim());
}}
>
<label <label
htmlFor="default-search" htmlFor="default-search"
className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white" className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
> >
Search Search
</label> </label>
<div className="relative"> <div className="relative w-full">
<div className="absolute inset-y-0 flex items-center pointer-events-none start-0 ps-3"> <div className="absolute inset-y-0 flex items-center pointer-events-none start-0 ps-3">
<svg <svg
className="w-4 h-4 text-gray-500 dark:text-gray-400" className="w-4 h-4 text-gray-500 dark:text-gray-400"
@ -214,229 +304,110 @@ export function SearchPage() {
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" 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="Поиск аниме..." placeholder="Поиск аниме..."
required required
value={searchVal} value={query}
onChange={(e) => setSearchVal(e.target.value)} onChange={(e) => setQuery(e.target.value)}
/> />
<button
type="submit"
className="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Поиск
</button>
</div> </div>
</form> <div className="flex gap-2 mt-2 lg:ml-2 lg:mt-0">
<Button <div className="flex justify-between flex-1 gap-4">
color="light" <Dropdown
size="xl" size="xl"
onClick={() => setFiltersModalOpen(true)} label={`Искать в: ${whereMapping.find((item) => item.id == params.where).label}`}
> color="light"
Фильтры className="w-full lg:w-fit"
</Button> >
{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>
<div className="mt-2">
<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} />} {data && data[0].related && <RelatedSection {...data[0].related} />}
{content ? {content ?
content.length > 0 ? content.length > 0 ?
<> params.where == "profiles" ?
{where == "collections" ? <UserSection content={content} />
<CollectionsSection : params.where == "collections" ?
sectionTitle="Найденные Коллекции" <CollectionsSection content={content} />
content={content} : <ReleaseSection content={content} />
/>
: where == "profiles" ?
<UserSection
sectionTitle="Найденные Пользователи"
content={content}
/>
: <ReleaseSection
sectionTitle="Найденные релизы"
content={content}
/>
}
</>
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl"> : <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> <span className="w-24 h-24 iconify-color twemoji--crying-cat"></span>
<p>Странно, аниме не найдено, попробуйте другой запрос...</p> <p>Странно, аниме не найдено, попробуйте другой запрос...</p>
</div> </div>
: isLoading && (
<div className="flex items-center justify-center min-w-full min-h-screen"> : <></>}
<Spinner />
</div>
)
}
{!content && !isLoading && !query && ( {!content && !isLoading && !query && (
<div className="flex flex-col items-center justify-center min-w-full gap-2 mt-12 text-xl"> <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> <span className="w-16 h-16 iconify mdi--arrow-up animate-bounce"></span>
<p>Введите ваш запрос что-бы найти любимый тайтл</p> <p>Введите ваш запрос что-бы найти любимый тайтл</p>
</div> </div>
)} )}
</div>
{( {isLoading ?
data && <div className="flex items-center justify-center w-full h-16">
data.length > 1 && <Spinner />
(where == "releases" ?
data[data.length - 1].releases.length == 25
: data[data.length - 1].content.length == 25)
) ?
<Button
className="w-full"
color={"light"}
onClick={() => setSize(size + 1)}
>
<div className="flex items-center gap-2">
<span className="w-6 h-6 iconify mdi--plus-circle "></span>
<span className="text-lg">Загрузить ещё</span>
</div> </div>
</Button> : ""}
: ""} </div>
<FiltersModal </div>
isOpen={filtersModalOpen}
setIsOpen={setFiltersModalOpen}
where={where}
setWhere={setWhere}
list={list}
setList={setList}
isAuth={userStore.isAuth}
searchBy={searchBy}
setSearchBy={setSearchBy}
setContent={setContent}
/>
</>
); );
} }
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 (
<Modal show={props.isOpen} onClose={() => _cancel()}>
<ModalHeader>Фильтры</ModalHeader>
<ModalBody>
<div className="my-4">
<div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Искать в</p>
<Dropdown label={WhereMapping[where]} color="blue">
{Object.keys(WhereMapping).map((item) => {
if (
["list", "history", "collections", "favorites"].includes(
item
) &&
!props.isAuth
) {
return <></>;
} else {
return (
<DropdownItem
onClick={() => setWhere(item)}
key={`where--${item}`}
>
{WhereMapping[item]}
</DropdownItem>
);
}
})}
</Dropdown>
</div>
</div>
{props.isAuth && where == "list" && ListsMapping.hasOwnProperty(list) ?
<div className="my-4">
<div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Список</p>
<Dropdown label={ListsMapping[list].name} color="blue">
{Object.keys(ListsMapping).map((item) => {
return (
<DropdownItem
onClick={() => setList(item)}
key={`list--${item}`}
>
{ListsMapping[item].name}
</DropdownItem>
);
})}
</Dropdown>
</div>
</div>
: ""}
{!["profiles", "collections"].includes(where) ?
<div className="my-4">
<div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Искать по</p>
<Dropdown label={TagMapping[searchBy].name} color="blue">
{Object.keys(TagMapping).map((item) => {
return (
<DropdownItem
onClick={() => setSearchBy(item)}
key={`tag--${item}`}
>
{TagMapping[item].name}
</DropdownItem>
);
})}
</Dropdown>
</div>
</div>
: ""}
</ModalBody>
<ModalFooter>
<div className="flex justify-end w-full gap-2">
<Button color="red" onClick={() => _cancel()}>
Отменить
</Button>
<Button color="blue" onClick={() => _accept()}>
Применить
</Button>
</div>
</ModalFooter>
</Modal>
);
};

View file

@ -1,7 +1,7 @@
import { BookmarksCategoryPage } from "#/pages/BookmarksCategory"; import { BookmarksCategoryPage } from "#/pages/BookmarksCategory";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = 'force-static'; import { API_URL } from "#/api/config";
const SectionTitleMapping = { const SectionTitleMapping = {
watching: "Смотрю", watching: "Смотрю",
@ -17,7 +17,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `${API_URL}/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;

View file

@ -1,7 +1,7 @@
import { BookmarksPage } from "#/pages/Bookmarks"; import { BookmarksPage } from "#/pages/Bookmarks";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `${API_URL}/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;

View file

@ -1,7 +1,7 @@
import { CollectionsFullPage } from "#/pages/CollectionsFull"; import { CollectionsFullPage } from "#/pages/CollectionsFull";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `${API_URL}/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
@ -38,7 +38,7 @@ export async function generateMetadata(
export default async function Collections({ params }) { export default async function Collections({ params }) {
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${params.id}` `${API_URL}/profile/${params.id}`
); );
if (error) { if (error) {

View file

@ -1,7 +1,7 @@
import { ProfilePage } from "#/pages/Profile"; import { ProfilePage } from "#/pages/Profile";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `${API_URL}/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;

View file

@ -1,7 +1,7 @@
import { RelatedPage } from "#/pages/Related"; import { RelatedPage } from "#/pages/Related";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = 'force-static'; import { API_URL } from "#/api/config";
const _getData = async (url: string) => { const _getData = async (url: string) => {
const { data, error } = await fetchDataViaGet(url); const { data, error } = await fetchDataViaGet(url);
@ -12,7 +12,7 @@ export async function generateMetadata({ params }, parent: ResolvingMetadata): P
const id:string = params.id; const id:string = params.id;
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
const [ related, relatedError ] = await _getData(`https://api.anixart.tv/related/${id}/0`); const [ related, relatedError ] = await _getData(`${API_URL}/related/${id}/0`);
if (relatedError || related.content.length == 0) { if (relatedError || related.content.length == 0) {
return { return {
title: "Ошибка", title: "Ошибка",
@ -20,7 +20,7 @@ export async function generateMetadata({ params }, parent: ResolvingMetadata): P
}; };
}; };
const [ firstRelease, firstReleaseError ] = await _getData(`https://api.anixart.tv/release/${related.content[0].id}`); const [ firstRelease, firstReleaseError ] = await _getData(`${API_URL}/release/${related.content[0].id}`);
if (firstReleaseError) { if (firstReleaseError) {
return { return {
title: "Ошибка", title: "Ошибка",
@ -46,7 +46,7 @@ export async function generateMetadata({ params }, parent: ResolvingMetadata): P
export default async function Related({ params }) { export default async function Related({ params }) {
const id: string = params.id; const id: string = params.id;
const [ related, relatedError ] = await _getData(`https://api.anixart.tv/related/${id}/0`); const [ related, relatedError ] = await _getData(`${API_URL}/related/${id}/0`);
if (relatedError || related.content.length == 0) { if (relatedError || related.content.length == 0) {
return <main className="flex items-center justify-center min-h-screen"> return <main className="flex items-center justify-center min-h-screen">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -56,7 +56,7 @@ export default async function Related({ params }) {
</main> </main>
}; };
const [ firstRelease, firstReleaseError ] = await _getData(`https://api.anixart.tv/release/${related.content[0].id}`); const [ firstRelease, firstReleaseError ] = await _getData(`${API_URL}/release/${related.content[0].id}`);
if (firstReleaseError) { if (firstReleaseError) {
return <main className="flex items-center justify-center min-h-screen"> return <main className="flex items-center justify-center min-h-screen">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View file

@ -1,7 +1,7 @@
import { CollectionsFullPage } from "#/pages/CollectionsFull"; import { CollectionsFullPage } from "#/pages/CollectionsFull";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/release/${id}` `${API_URL}/release/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
@ -38,7 +38,7 @@ export async function generateMetadata(
export default async function Collections({ params }) { export default async function Collections({ params }) {
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/release/${params.id}` `${API_URL}/release/${params.id}`
); );
if (error) { if (error) {

View file

@ -1,7 +1,7 @@
import { ReleasePage } from "#/pages/Release"; import { ReleasePage } from "#/pages/Release";
import { fetchDataViaGet } from "#/api/utils"; import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = "force-static"; import { API_URL } from "#/api/config";
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
@ -9,7 +9,7 @@ export async function generateMetadata(
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/release/${id}` `${API_URL}/release/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;

View file

@ -5,6 +5,8 @@ export const metadata = {
description: "Поиск аниме релизов", description: "Поиск аниме релизов",
}; };
export const dynamic = "force-static";
export default function Search() { export default function Search() {
return <SearchPage />; return <SearchPage />;
} }

View file

@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"dev-with-services": "node ./run-all.dev.js",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",

View file

@ -9,7 +9,7 @@ app.use(function (req, res, next) {
res.header("Access-Control-Allow-Origin", req.headers.origin || "*"); res.header("Access-Control-Allow-Origin", req.headers.origin || "*");
res.header( res.header(
"Access-Control-Allow-Headers", "Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept" "Origin, X-Requested-With, Content-Type, Accept, Allow, User-Agent"
); );
res.header("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"); res.header("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS");
next(); next();

View file

@ -1,6 +1,6 @@
export const corsHeaders = { export const corsHeaders = {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept", "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Allow, User-Agent",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
}; };

View file

@ -0,0 +1,8 @@
# 3.8.0
## Изменено
- Фильтры на странице поиска были перемещены рядом с полем ввода
- ТЕХ: механизм запросов поиска стал работать так-же как и остальные запросы
- Динамическая метадата (пред просмотра) снова включена
- ТЕХ: генераторы метадаты (пред просмотра) используют ссылку API (API_URL) с конфига

29
run-all.dev.js Normal file
View file

@ -0,0 +1,29 @@
const { spawn } = require("child_process");
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
const client = spawn(npm, ["run", "dev"], { shell: true });
const parser = spawn(npm, ["run", "serve"], { shell: true, cwd: "./player-parser" });
const proxy = spawn(npm, ["run", "serve"], { shell: true, cwd: "./api-prox" });
const clientInfo = "\x1b[36m[client]\x1b[0m";
const parserInfo = "\x1b[33m[parser]\x1b[0m";
const proxyInfo = "\x1b[31m[proxy]\x1b[0m";
console.log(`${clientInfo} CMD: ${client.spawnargs.toString()}`);
console.log(`${clientInfo} PID: ${client.pid}`);
console.log(`${parserInfo} CMD: ${parser.spawnargs.toString()}`);
console.log(`${parserInfo} PID: ${parser.pid}`);
console.log(`${proxyInfo} CMD: ${proxy.spawnargs.toString()}`);
console.log(`${proxyInfo} PID: ${proxy.pid}`);
console.log(`\n`);
client.stdout.on("data", (data) =>
console.log(clientInfo, data.toString())
);
parser.stdout.on("data", (data) =>
console.log(parserInfo, data.toString())
);
proxy.stdout.on("data", (data) =>
console.log(proxyInfo, data.toString())
);