Compare commits

..

No commits in common. "b79c07f4c2399f6db310510c8a4623fc8f77b487" and "f2f03df1a0ceca020da195aa0fdcd4ec856f8690" have entirely different histories.

58 changed files with 1618 additions and 2339 deletions

View file

@ -6,10 +6,10 @@ AniX is an unofficial web client for the Android application Anixart. It allows
## Changelog [RU] ## Changelog [RU]
- [3.4.0](./public/changelog/3.4.0.md)
- [3.3.0](./public/changelog/3.3.0.md) - [3.3.0](./public/changelog/3.3.0.md)
- [3.2.3](./public/changelog/3.2.3.md) - [3.2.3](./public/changelog/3.2.3.md)
- [3.2.2](./public/changelog/3.2.2.md) - [3.2.2](./public/changelog/3.2.2.md)
- [3.2.1](./public/changelog/3.2.1.md)
[other versions](./public/changelog) [other versions](./public/changelog)

View file

@ -8,7 +8,6 @@ import { Button, Modal } from "flowbite-react";
import { Spinner } from "./components/Spinner/Spinner"; import { Spinner } from "./components/Spinner/Spinner";
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal"; import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
import PlausibleProvider from "next-plausible"; import PlausibleProvider from "next-plausible";
import { Bounce, ToastContainer } from "react-toastify";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -112,20 +111,6 @@ export const App = (props) => {
enabled={true} enabled={true}
/> />
)} )}
<ToastContainer
className={"mx-2 mb-20 sm:mb-0"}
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={true}
closeOnClick={true}
rtl={false}
pauseOnFocusLoss={false}
draggable={true}
pauseOnHover={true}
theme="colored"
transition={Bounce}
/>
</body> </body>
); );
}; };

View file

@ -1,4 +1,4 @@
export const CURRENT_APP_VERSION = "3.4.0"; export const CURRENT_APP_VERSION = "3.3.0";
export const API_URL = "https://api.anixart.tv"; export const API_URL = "https://api.anixart.tv";
export const API_PREFIX = "/api/proxy"; export const API_PREFIX = "/api/proxy";
@ -13,7 +13,6 @@ export const ENDPOINTS = {
licensed: `${API_PREFIX}/release/streaming/platform`, licensed: `${API_PREFIX}/release/streaming/platform`,
}, },
user: { user: {
auth: `${API_PREFIX}/auth/signIn`,
profile: `${API_PREFIX}/profile`, profile: `${API_PREFIX}/profile`,
bookmark: `${API_PREFIX}/profile/list`, bookmark: `${API_PREFIX}/profile/list`,
history: `${API_PREFIX}/history`, history: `${API_PREFIX}/history`,

View file

@ -0,0 +1,14 @@
import { NextResponse, NextRequest } from "next/server";
import { authorize } from "#/api/utils";
import { API_URL } from "#/api/config";
export async function POST(request: NextRequest) {
const response = await authorize(`${API_URL}/auth/signIn`, await request.json());
if (!response) {
return NextResponse.json({ message: "Server Error" }, { status: 500 });
}
if (!response.profile) {
return NextResponse.json({ message: "Profile not found" }, { status: 404 });
}
return NextResponse.json(response);
}

View file

@ -49,26 +49,16 @@ export async function GET(request: NextRequest) {
if (token) { if (token) {
url.searchParams.set("token", token); url.searchParams.set("token", token);
} }
const body = { query, searchBy }; const data = { query, searchBy };
const { data, error } = await fetchDataViaPost( const response = await fetchDataViaPost(
url.toString(), url.toString(),
JSON.stringify(body), JSON.stringify(data),
true true
); );
if (error) { if (!response) {
return new Response(JSON.stringify(error), { return NextResponse.json({ message: "Bad request" }, { status: 400 });
status: 500,
headers: {
"Content-Type": "application/json",
},
});
} }
return new Response(JSON.stringify(data), { return NextResponse.json(response);
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} }

View file

@ -4,159 +4,79 @@ export const HEADERS = {
"Content-Type": "application/json; charset=UTF-8", "Content-Type": "application/json; charset=UTF-8",
}; };
type Success<T> = {
data: T;
error: null;
};
type Failure<E> = {
data: null;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
export async function tryCatch<T, E = Error>(
promise: Promise<T>
): Promise<Result<T, E>> {
try {
const data = await promise;
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
export async function tryCatchPlayer<T, E = Error>(
promise: Promise<any>
): Promise<Result<any, any>> {
try {
const res: Awaited<Response> = await promise;
const data = await res.json();
if (!res.ok) {
if (data.message) {
return {
data: null,
error: {
message: data.message,
code: res.status,
},
};
} else if (data.detail) {
return {
data: null,
error: {
message: data.detail,
code: res.status,
},
};
} else {
return {
data: null,
error: {
message: res.statusText,
code: res.status,
},
};
}
}
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
export async function tryCatchAPI<T, E = Error>(
promise: Promise<any>
): Promise<Result<any, any>> {
try {
const res: Awaited<Response> = await promise;
// if (!res.ok) {
// return {
// data: null,
// error: {
// message: res.statusText,
// code: res.status,
// },
// };
// }
if (
res.headers.get("content-length") &&
Number(res.headers.get("content-length")) == 0
) {
return {
data: null,
error: {
message: "Not Found",
code: 404,
},
};
}
const data: Awaited<any> = await res.json();
if (data.code != 0) {
return {
data: null,
error: {
message: "API Returned an Error",
code: data.code || 500,
},
};
}
return { data, error: null };
} catch (error) {
return { data: null, error: error };
}
}
export const useSWRfetcher = async (url: string) => {
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
throw error;
}
return data;
};
export const fetchDataViaGet = async ( export const fetchDataViaGet = async (
url: string, url: string,
API_V2: string | boolean = false, API_V2: string | boolean = false
addHeaders?: Record<string, any>
) => { ) => {
if (API_V2) { if (API_V2) {
HEADERS["API-Version"] = "v2"; HEADERS["API-Version"] = "v2";
} }
try {
const { data, error } = await tryCatchAPI( const response = await fetch(url, {
fetch(url, { headers: HEADERS,
headers: { ...HEADERS, ...addHeaders }, });
}) if (response.status !== 200) {
); return null;
}
return { data, error }; const data = await response.json();
return data;
} catch (error) {
console.log(error);
}
}; };
export const fetchDataViaPost = async ( export const fetchDataViaPost = async (
url: string, url: string,
body: string, body: string,
API_V2: string | boolean = false, API_V2: string | boolean = false,
addHeaders?: Record<string, any> contentType: string = ""
) => { ) => {
if (API_V2) { if (API_V2) {
HEADERS["API-Version"] = "v2"; HEADERS["API-Version"] = "v2";
} }
if (contentType != "") {
HEADERS["Content-Type"] = contentType;
}
const { data, error } = await tryCatchAPI( try {
fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
headers: HEADERS,
body: body, body: body,
headers: { ...HEADERS, ...addHeaders }, });
}) if (response.status !== 200) {
); return null;
}
const data = await response.json();
return data;
} catch (error) {
console.log(error);
}
};
return { data, error }; export const authorize = async (
url: string,
data: { login: string; password: string }
) => {
try {
const response = await fetch(
`${url}?login=${data.login}&password=${data.password}`,
{
method: "POST",
headers: {
"User-Agent": USER_AGENT,
Sign: "9aa5c7af74e8cd70c86f7f9587bde23d",
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (response.status !== 200) {
throw new Error("Error authorizing user");
}
return await response.json();
} catch (error) {
return error;
}
}; };
export function setJWT(user_id: number | string, jwt: string) { export function setJWT(user_id: number | string, jwt: string) {

View file

@ -1,36 +1,28 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const collection = await fetchDataViaGet(
`https://api.anixart.tv/collection/${id}` `https://api.anixart.tv/collection/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Приватная коллекция", title: collection.collection
description: "Приватная коллекция", ? "коллекция - " + collection.collection.title
};
}
return {
title:
data.collection ?
"коллекция - " + data.collection.title
: "Приватная коллекция", : "Приватная коллекция",
description: data.collection && data.collection.description, description: collection.collection && collection.collection.description,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.collection && data.collection.image, // Must be an absolute URL url: collection.collection && collection.collection.image, // Must be an absolute URL
width: 600, width: 600,
height: 800, height: 800,
}, },

View file

@ -4,7 +4,6 @@ import { Modal, Accordion } from "flowbite-react";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Styles from "./ChangelogModal.module.css"; import Styles from "./ChangelogModal.module.css";
import { tryCatch } from "#/api/utils";
export const ChangelogModal = (props: { export const ChangelogModal = (props: {
isOpen: boolean; isOpen: boolean;
@ -18,20 +17,29 @@ export const ChangelogModal = (props: {
>({}); >({});
async function _fetchVersionChangelog(version: string) { async function _fetchVersionChangelog(version: string) {
const { data, error } = await tryCatch(fetch(`/changelog/${version}.md`)); const res = await fetch(`/changelog/${version}.md`);
if (error) { return await res.text();
return "Нет списка изменений";
}
return await data.text();
} }
useEffect(() => { useEffect(() => {
if (props.version != "" && currentVersionChangelog == "") { if (props.version != "" && currentVersionChangelog == "") {
setCurrentVersionChangelog("Загрузка ...");
_fetchVersionChangelog(props.version).then((data) => { _fetchVersionChangelog(props.version).then((data) => {
setCurrentVersionChangelog(data); setCurrentVersionChangelog(data);
}); });
} }
if (props.previousVersions.length > 0) {
props.previousVersions.forEach((version) => {
_fetchVersionChangelog(version).then((data) => {
setPreviousVersionsChangelog((prev) => {
return {
...prev,
[version]: data,
};
});
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.version]); }, [props.version]);
@ -42,38 +50,20 @@ export const ChangelogModal = (props: {
<Markdown className={Styles.markdown}> <Markdown className={Styles.markdown}>
{currentVersionChangelog} {currentVersionChangelog}
</Markdown> </Markdown>
{Object.keys(previousVersionsChangelog).length == props.previousVersions.length && (
<Accordion collapseAll={true} className="mt-4"> <Accordion collapseAll={true} className="mt-4">
{props.previousVersions.length > 0 && {props.previousVersions.map(
props.previousVersions.map((version) => { (version) => (
return (
<Accordion.Panel key={version}> <Accordion.Panel key={version}>
<Accordion.Title <Accordion.Title>Список изменений v{version}</Accordion.Title>
onClickCapture={(e) => {
if (!previousVersionsChangelog.hasOwnProperty(version)) {
_fetchVersionChangelog(version).then((data) => {
setPreviousVersionsChangelog((prev) => {
return {
...prev,
[version]: data,
};
});
});
}
}}
>
Список изменений v{version}
</Accordion.Title>
<Accordion.Content> <Accordion.Content>
{previousVersionsChangelog.hasOwnProperty(version) ? <Markdown className={Styles.markdown}>{previousVersionsChangelog[version]}</Markdown>
<Markdown className={Styles.markdown}>
{previousVersionsChangelog[version]}
</Markdown>
: <div>Загрузка ...</div>}
</Accordion.Content> </Accordion.Content>
</Accordion.Panel> </Accordion.Panel>
); )
})} )}
</Accordion> </Accordion>
)}
</Modal.Body> </Modal.Body>
</Modal> </Modal>
); );

View file

@ -1,11 +1,9 @@
"use client"; "use client";
import { Card, Button, useThemeMode } from "flowbite-react"; import { Card, Button } from "flowbite-react";
import { useState } from "react"; import { useState } from "react";
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { tryCatchAPI } from "#/api/utils";
import { toast } from "react-toastify";
export const CollectionInfoControls = (props: { export const CollectionInfoControls = (props: {
isFavorite: boolean; isFavorite: boolean;
@ -14,124 +12,36 @@ export const CollectionInfoControls = (props: {
isPrivate: boolean; isPrivate: boolean;
}) => { }) => {
const [isFavorite, setIsFavorite] = useState(props.isFavorite); const [isFavorite, setIsFavorite] = useState(props.isFavorite);
const [isUpdating, setIsUpdating] = useState(false);
const theme = useThemeMode();
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
async function _addToFavorite() { async function _addToFavorite() {
async function _FavCol(url: string) { if (userStore.user) {
setIsUpdating(true);
const tid = toast.loading(
isFavorite ?
"Удаляем коллекцию из избранного..."
: "Добавляем коллекцию в избранное...",
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render:
isFavorite ?
"Ошибка удаления коллекции из избранного"
: "Ошибка добавления коллекции в избранное",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsUpdating(false);
return;
}
toast.update(tid, {
render:
isFavorite ?
"Коллекция удалена из избранного"
: "Коллекция добавлена в избранное",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsUpdating(false);
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
}
if (userStore.token) {
let url = `${ENDPOINTS.collection.favoriteCollections}/add/${props.id}?token=${userStore.token}`;
if (isFavorite) { if (isFavorite) {
url = `${ENDPOINTS.collection.favoriteCollections}/delete/${props.id}?token=${userStore.token}`; fetch(
`${ENDPOINTS.collection.favoriteCollections}/delete/${props.id}?token=${userStore.token}`
);
} else {
fetch(
`${ENDPOINTS.collection.favoriteCollections}/add/${props.id}?token=${userStore.token}`
);
} }
_FavCol(url);
} }
} }
async function _deleteCollection() { async function _deleteCollection() {
async function _DelCol(url: string) { if (userStore.user) {
setIsUpdating(true); fetch(
const tid = toast.loading("Удаляем коллекцию...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: "Ошибка удаления коллекции",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsUpdating(false);
return;
}
toast.update(tid, {
render: `Коллекция удалена`,
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsUpdating(false);
router.push("/collections");
}
if (userStore.token) {
_DelCol(
`${ENDPOINTS.collection.delete}/${props.id}?token=${userStore.token}` `${ENDPOINTS.collection.delete}/${props.id}?token=${userStore.token}`
); );
router.push("/collections");
} }
} }
return ( return (
<Card className="w-full h-fit "> <Card className="w-full h-fit ">
<Button <Button color={"blue"} onClick={() => _addToFavorite()}>
color={"blue"}
onClick={() => _addToFavorite()}
disabled={isUpdating}
>
<span <span
className={`iconify w-6 h-6 mr-2 ${ className={`iconify w-6 h-6 mr-2 ${
isFavorite ? "mdi--heart" : "mdi--heart-outline" isFavorite ? "mdi--heart" : "mdi--heart-outline"
@ -150,7 +60,6 @@ export const CollectionInfoControls = (props: {
onClick={() => onClick={() =>
router.push("/collections/create?mode=edit&id=" + props.id) router.push("/collections/create?mode=edit&id=" + props.id)
} }
disabled={isUpdating}
> >
<span className="w-6 h-6 mr-2 iconify mdi--pencil"></span>{" "} <span className="w-6 h-6 mr-2 iconify mdi--pencil"></span>{" "}
Редактировать Редактировать
@ -159,7 +68,6 @@ export const CollectionInfoControls = (props: {
color={"red"} color={"red"}
className="w-full sm:max-w-64" className="w-full sm:max-w-64"
onClick={() => _deleteCollection()} onClick={() => _deleteCollection()}
disabled={isUpdating}
> >
<span className="w-6 h-6 mr-2 iconify mdi--trash"></span> Удалить <span className="w-6 h-6 mr-2 iconify mdi--trash"></span> Удалить
</Button> </Button>

View file

@ -15,7 +15,7 @@ export const CollectionLink = (props: any) => {
<Image <Image
src={props.image} src={props.image}
fill={true} fill={true}
alt={props.title || ""} alt={props.title}
className="-z-[1] object-cover" className="-z-[1] object-cover"
sizes=" sizes="
(max-width: 768px) 300px, (max-width: 768px) 300px,

View file

@ -4,7 +4,6 @@ import { useState, useEffect, useCallback } from "react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import useSWRInfinite from "swr/infinite"; import useSWRInfinite from "swr/infinite";
import { CommentsAddModal } from "./Comments.Add"; import { CommentsAddModal } from "./Comments.Add";
import { useSWRfetcher } from "#/api/utils";
export const CommentsMain = (props: { export const CommentsMain = (props: {
release_id: number; release_id: number;
@ -83,6 +82,20 @@ export const CommentsMain = (props: {
); );
}; };
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
const CommentsAllModal = (props: { const CommentsAllModal = (props: {
isOpen: boolean; isOpen: boolean;
setIsOpen: any; setIsOpen: any;
@ -90,6 +103,7 @@ const CommentsAllModal = (props: {
token: string | null; token: string | null;
type?: "release" | "collection"; type?: "release" | "collection";
}) => { }) => {
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const [currentRef, setCurrentRef] = useState<any>(null); const [currentRef, setCurrentRef] = useState<any>(null);
const modalRef = useCallback((ref) => { const modalRef = useCallback((ref) => {
setCurrentRef(ref); setCurrentRef(ref);
@ -113,7 +127,7 @@ const CommentsAllModal = (props: {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -125,6 +139,7 @@ const CommentsAllModal = (props: {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -155,7 +170,7 @@ const CommentsAllModal = (props: {
Все комментарии Все комментарии
</h2> </h2>
<p className="text-sm font-light text-gray-600 dark:text-gray-300"> <p className="text-sm font-light text-gray-600 dark:text-gray-300">
всего: {isLoading ? "загрузка..." : data[0].total_count} всего: {!isLoadingEnd ? "загрузка..." : data[0].total_count}
</p> </p>
</div> </div>
</Modal.Header> </Modal.Header>
@ -164,7 +179,7 @@ const CommentsAllModal = (props: {
onScroll={handleScroll} onScroll={handleScroll}
ref={modalRef} ref={modalRef}
> >
{isLoading ? ( {!isLoadingEnd ? (
<Spinner /> <Spinner />
) : content ? ( ) : content ? (
content.map((comment: any) => ( content.map((comment: any) => (

View file

@ -3,86 +3,56 @@ import Cropper, { ReactCropperElement } from "react-cropper";
import "cropperjs/dist/cropper.css"; import "cropperjs/dist/cropper.css";
import { Button, Modal } from "flowbite-react"; import { Button, Modal } from "flowbite-react";
type CropModalProps = { type Props = {
src: string;
setSrc: (src: string) => void;
setTempSrc: (src: string) => void;
isOpen: boolean; isOpen: boolean;
isActionsDisabled: boolean; setIsOpen: (isOpen: boolean) => void;
selectedImage: any | null; height: number;
croppedImage: any | null; width: number;
setCropModalProps: (props: { aspectRatio: number;
isOpen: boolean; guides: boolean;
isActionsDisabled: boolean; quality: number;
selectedImage: any | null;
croppedImage: any | null;
}) => void;
cropParams: {
guides?: boolean;
width?: number;
height?: number;
quality?: number;
aspectRatio?: number;
forceAspect?: boolean; forceAspect?: boolean;
}; };
};
export const CropModal: React.FC<CropModalProps> = ({ export const CropModal: React.FC<Props> = (props) => {
isOpen,
setCropModalProps,
cropParams,
selectedImage,
croppedImage,
isActionsDisabled,
}) => {
const cropperRef = useRef<ReactCropperElement>(null); const cropperRef = useRef<ReactCropperElement>(null);
const getCropData = () => { const getCropData = () => {
if (typeof cropperRef.current?.cropper !== "undefined") { if (typeof cropperRef.current?.cropper !== "undefined") {
const croppedImage = cropperRef.current?.cropper props.setSrc(
cropperRef.current?.cropper
.getCroppedCanvas({ .getCroppedCanvas({
width: cropParams.width, width: props.width,
height: cropParams.height, height: props.height,
maxWidth: cropParams.width, maxWidth: props.width,
maxHeight: cropParams.height, maxHeight: props.height,
}) })
.toDataURL( .toDataURL("image/jpeg", props.quality)
"image/jpeg",
cropParams.quality || false ? cropParams.quality : 100
); );
props.setTempSrc("");
setCropModalProps({
isOpen: true,
isActionsDisabled: false,
selectedImage: selectedImage,
croppedImage: croppedImage,
});
} }
}; };
return ( return (
<Modal <Modal
dismissible dismissible
show={isOpen} show={props.isOpen}
onClose={() => { onClose={() => props.setIsOpen(false)}
setCropModalProps({
isOpen: false,
isActionsDisabled: false,
selectedImage: null,
croppedImage: null,
});
}}
size={"7xl"} size={"7xl"}
> >
<Modal.Header>Обрезать изображение</Modal.Header> <Modal.Header>Обрезать изображение</Modal.Header>
<Modal.Body> <Modal.Body>
<Cropper <Cropper
src={selectedImage} src={props.src}
style={{ height: 400, width: "100%" }} style={{ height: 400, width: "100%" }}
responsive={true} responsive={true}
// Cropper.js options // Cropper.js options
initialAspectRatio={cropParams.aspectRatio || 1 / 1} initialAspectRatio={props.aspectRatio}
aspectRatio={ aspectRatio={props.forceAspect ? props.aspectRatio : undefined}
cropParams.forceAspect || false ? cropParams.aspectRatio : undefined guides={props.guides}
}
guides={cropParams.guides || false}
ref={cropperRef} ref={cropperRef}
/> />
@ -99,26 +69,23 @@ export const CropModal: React.FC<CropModalProps> = ({
<Modal.Footer> <Modal.Footer>
<Button <Button
color={"blue"} color={"blue"}
disabled={isActionsDisabled}
onClick={() => { onClick={() => {
getCropData(); getCropData();
props.setIsOpen(false);
}} }}
> >
Сохранить Сохранить
</Button> </Button>
<Button <Button
color={"red"} color={"red"}
disabled={isActionsDisabled}
onClick={() => { onClick={() => {
setCropModalProps({ props.setSrc(null);
isOpen: false, props.setTempSrc(null);
isActionsDisabled: false, // props.setImageData(null);
selectedImage: null, props.setIsOpen(false);
croppedImage: null,
});
}} }}
> >
Отменить Удалить
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>

View file

@ -0,0 +1,290 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useUserStore } from "#/store/auth";
import { Dropdown } from "flowbite-react";
import { useState } from "react";
import Image from "next/image";
import { SettingsModal } from "#/components/SettingsModal/SettingsModal";
export const Navbar = () => {
const pathname = usePathname();
const userStore: any = useUserStore((state) => state);
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
const navLinks = [
{
id: 1,
icon: "material-symbols--home-outline",
iconActive: "material-symbols--home",
title: "Домашняя",
href: "/",
categoryHref: "/home",
withAuthOnly: false,
mobileMenu: false,
},
{
id: 2,
icon: "material-symbols--search",
iconActive: "material-symbols--search",
title: "Поиск",
href: "/search",
withAuthOnly: false,
mobileMenu: false,
},
{
id: 3,
icon: "material-symbols--bookmarks-outline",
iconActive: "material-symbols--bookmarks",
title: "Закладки",
href: "/bookmarks",
withAuthOnly: true,
mobileMenu: false,
},
{
id: 4,
icon: "material-symbols--favorite-outline",
iconActive: "material-symbols--favorite",
title: "Избранное",
href: "/favorites",
withAuthOnly: true,
mobileMenu: true,
},
{
id: 5,
icon: "material-symbols--collections-bookmark-outline",
iconActive: "material-symbols--collections-bookmark",
title: "Коллекции",
href: "/collections",
withAuthOnly: true,
mobileMenu: true,
},
{
id: 6,
icon: "material-symbols--history",
iconActive: "material-symbols--history",
title: "История",
href: "/history",
withAuthOnly: true,
mobileMenu: true,
},
];
return (
<>
<header className="fixed bottom-0 left-0 z-50 w-full text-white bg-black sm:sticky sm:top-0">
<div className="container flex items-center justify-center gap-4 px-4 py-4 mx-auto lg:justify-between lg:gap-0">
<nav className="flex gap-4">
{navLinks.map((link) => {
return (
<Link
key={link.id}
href={link.href}
className={`flex-col items-center lg:flex-row ${
link.withAuthOnly && !userStore.isAuth
? "hidden"
: link.mobileMenu
? "hidden sm:flex"
: "flex"
}`}
>
<span
className={`iconify ${
[link.href, link.categoryHref].includes(
"/" + pathname.split("/")[1]
)
? link.iconActive
: link.icon
} w-6 h-6`}
></span>
<span
className={`${
[link.href, link.categoryHref].includes(
"/" + pathname.split("/")[1]
)
? "font-bold"
: ""
} text-sm sm:text-base`}
>
{link.title}
</span>
</Link>
);
})}
</nav>
{userStore.isAuth ? (
<>
<div className="flex-col items-center justify-end hidden text-sm md:flex lg:gap-1 lg:justify-center lg:flex-row lg:text-base">
<Image
src={userStore.user.avatar}
alt=""
className="w-6 h-6 rounded-full"
width={24}
height={24}
/>
<Dropdown
label={userStore.user.login}
inline={true}
dismissOnClick={true}
theme={{
arrowIcon:
"ml-1 w-4 h-4 [transform:rotateX(180deg)] sm:transform-none",
floating: {
target: "text-sm sm:text-base",
},
}}
>
<Dropdown.Item className="text-sm md:text-base">
<Link
href={`/profile/${userStore.user.id}`}
className="flex items-center gap-1"
>
<span
className={`iconify ${
pathname == `/profile/${userStore.user.id}`
? "font-bold mdi--user"
: "mdi--user-outline"
} w-6 h-6`}
></span>
<span>Профиль</span>
</Link>
</Dropdown.Item>
{navLinks.map((link) => {
return (
<Dropdown.Item
key={link.id + "_mobile"}
className={`${
link.mobileMenu ? "block sm:hidden" : "hidden"
} text-sm md:text-base`}
>
<Link
href={link.href}
className={`flex items-center gap-1`}
>
<span
className={`iconify ${
[link.href, link.categoryHref].includes(
"/" + pathname.split("/")[1]
)
? link.iconActive
: link.icon
} w-6 h-6`}
></span>
<span
className={`${
[link.href, link.categoryHref].includes(
"/" + pathname.split("/")[1]
)
? "font-bold"
: ""
}`}
>
{link.title}
</span>
</Link>
</Dropdown.Item>
);
})}
<Dropdown.Item
onClick={() => {
setIsSettingModalOpen(true);
}}
className="flex items-center gap-1 text-sm md:text-base"
>
<span
className={`iconify material-symbols--settings-outline-rounded w-6 h-6`}
></span>
<span>Настройки</span>
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
userStore.logout();
}}
className="flex items-center gap-1 text-sm md:text-base"
>
<span
className={`iconify material-symbols--logout-rounded w-6 h-6`}
></span>
<span>Выйти</span>
</Dropdown.Item>
</Dropdown>
</div>
<div className="block md:hidden">
<Link
href={"/menu"}
className={`flex flex-col items-center justify-end text-sm md:hidden lg:gap-1 lg:justify-center lg:flex-row lg:text-base ${
pathname == "/menu" ? "font-bold" : ""
}`}
>
<Image
src={userStore.user.avatar}
alt=""
className="w-6 h-6 rounded-full"
width={24}
height={24}
/>
<p>{userStore.user.login}</p>
</Link>
</div>
</>
) : (
<Dropdown
label=""
renderTrigger={() => (
<div className="flex flex-col items-center text-sm md:text-base">
<span className="w-6 h-6 iconify mdi--menu"></span>
<span>Меню</span>
</div>
)}
inline={true}
dismissOnClick={true}
theme={{
arrowIcon:
"ml-1 w-4 h-4 [transform:rotateX(180deg)] sm:transform-none",
}}
>
<Dropdown.Item className="text-sm md:text-base">
<Link
href={
pathname != "/login" ? `/login?redirect=${pathname}` : "#"
}
className="flex items-center gap-1"
>
<span
className={`w-6 h-6 sm:w-6 sm:h-6 iconify ${
pathname == "/login"
? "mdi--user-circle"
: "mdi--user-circle-outline"
}`}
></span>
<span
className={`${
pathname == "/login" ? "font-bold" : ""
} text-sm sm:text-base`}
>
Войти
</span>
</Link>
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
setIsSettingModalOpen(true);
}}
className="flex items-center gap-1 text-sm md:text-base"
>
<span
className={`iconify material-symbols--settings-outline-rounded w-6 h-6 sm:w-6 sm:h-6`}
></span>
<span>Настройки</span>
</Dropdown.Item>
</Dropdown>
)}
</div>
</header>
<SettingsModal
isOpen={isSettingModalOpen}
setIsOpen={setIsSettingModalOpen}
/>
</>
);
};

View file

@ -87,8 +87,8 @@ export const Navbar = () => {
return ( return (
<> <>
<header className="fixed bottom-0 left-0 z-50 w-full text-white bg-black rounded-t-lg sm:sticky sm:top-0 sm:rounded-t-none sm:rounded-b-lg"> <header className="fixed bottom-0 left-0 z-50 w-full text-white bg-black rounded-t-lg sm:sticky sm:top-0 sm:rounded-t-none sm:rounded-b-lg">
<div className="container flex items-center justify-center gap-4 mx-auto sm:gap-0 sm:justify-between"> <div className="container flex items-center justify-center mx-auto sm:justify-between">
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4"> <div className="flex items-center gap-4 px-2 py-4">
{menuItems.map((item) => { {menuItems.map((item) => {
return ( return (
<Link <Link
@ -112,7 +112,7 @@ export const Navbar = () => {
); );
})} })}
</div> </div>
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4"> <div className="flex items-center gap-4 px-2 py-4">
{!userStore.isAuth ? {!userStore.isAuth ?
<Link <Link
href={ href={

View file

@ -1,10 +1,8 @@
"use client"; "use client";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { tryCatchAPI } from "#/api/utils"; import { Card, Button } from "flowbite-react";
import { Card, Button, useThemeMode } from "flowbite-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-toastify";
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
// null - не друзья // null - не друзья
@ -26,12 +24,11 @@ export const ProfileActions = (props: {
edit_isOpen: boolean; edit_isOpen: boolean;
edit_setIsOpen: any; edit_setIsOpen: any;
}) => { }) => {
const router = useRouter();
const profileIdIsSmaller = props.my_profile_id < props.profile_id; const profileIdIsSmaller = props.my_profile_id < props.profile_id;
const theme = useThemeMode(); const [friendRequestDisabled, setFriendRequestDisabled] = useState(false);
const [blockRequestDisabled, setBlockRequestDisabled] = useState(false);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [actionsDisabled, setActionsDisabled] = useState(false);
function _getFriendStatus() { function _getFriendStatus() {
const num = props.friendStatus; const num = props.friendStatus;
@ -57,119 +54,53 @@ export const ProfileActions = (props: {
} }
const FriendStatus = _getFriendStatus(); const FriendStatus = _getFriendStatus();
const isRequestedStatus = const isRequestedStatus =
FriendStatus != null ? FriendStatus != null
profileIdIsSmaller ? profileIdIsSmaller && FriendStatus != 0 ? profileIdIsSmaller
? profileIdIsSmaller && FriendStatus != 0
: !profileIdIsSmaller && FriendStatus == 2 : !profileIdIsSmaller && FriendStatus == 2
: null; : null;
// ^ This is some messed up shit // ^ This is some messed up shit
async function _addToFriends() { function _addToFriends() {
setActionsDisabled(true);
const tid = toast.loading("Добавляем в друзья...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
let url = `${ENDPOINTS.user.profile}/friend/request`; let url = `${ENDPOINTS.user.profile}/friend/request`;
FriendStatus == 1 ? (url += "/remove/") setFriendRequestDisabled(true);
: isRequestedStatus ? (url += "/remove/") setBlockRequestDisabled(true);
FriendStatus == 1
? (url += "/remove/")
: isRequestedStatus
? (url += "/remove/")
: (url += "/send/"); : (url += "/send/");
url += `${props.profile_id}?token=${props.token}`; url += `${props.profile_id}?token=${props.token}`;
fetch(url).then((res) => {
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render:
FriendStatus == 1 || isRequestedStatus ?
"Ошибка удаления из друзей"
: "Ошибка добавления в друзья",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setActionsDisabled(false);
return;
}
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
setTimeout(() => {
toast.update(tid, { setBlockRequestDisabled(false);
render: setFriendRequestDisabled(false);
FriendStatus == 1 || isRequestedStatus ? }, 100);
"Удален из друзей"
: "Добавлен в друзья",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
}); });
setActionsDisabled(false);
} }
async function _addToBlocklist() { function _addToBlocklist() {
setActionsDisabled(true);
const tid = toast.loading(
!props.is_blocked ?
"Блокируем пользователя..."
: "Разблокируем пользователя...",
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
let url = `${ENDPOINTS.user.profile}/blocklist`; let url = `${ENDPOINTS.user.profile}/blocklist`;
setBlockRequestDisabled(true);
setFriendRequestDisabled(true);
!props.is_blocked ? (url += "/add/") : (url += "/remove/"); !props.is_blocked ? (url += "/add/") : (url += "/remove/");
url += `${props.profile_id}?token=${props.token}`; url += `${props.profile_id}?token=${props.token}`;
fetch(url).then((res) => {
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: !props.is_blocked ? "Ошибка блокировки" : "Ошибка разблокировки",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setActionsDisabled(false);
return;
}
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
setTimeout(() => {
toast.update(tid, { setBlockRequestDisabled(false);
render: setFriendRequestDisabled(false);
!props.is_blocked ? }, 100);
"Пользователь заблокирован"
: "Пользователь разблокирован",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
}); });
setActionsDisabled(false);
} }
return ( return (
@ -178,14 +109,7 @@ export const ProfileActions = (props: {
<p>Отправил(-а) вам заявку в друзья</p> <p>Отправил(-а) вам заявку в друзья</p>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
{props.isMyProfile && ( {props.isMyProfile && <Button color={"blue"} onClick={() => props.edit_setIsOpen(!props.edit_isOpen)}>Редактировать</Button>}
<Button
color={"blue"}
onClick={() => props.edit_setIsOpen(!props.edit_isOpen)}
>
Редактировать
</Button>
)}
{!props.isMyProfile && ( {!props.isMyProfile && (
<> <>
{(!props.isFriendRequestsDisallowed || {(!props.isFriendRequestsDisallowed ||
@ -194,25 +118,26 @@ export const ProfileActions = (props: {
!props.is_me_blocked && !props.is_me_blocked &&
!props.is_blocked && ( !props.is_blocked && (
<Button <Button
disabled={actionsDisabled} disabled={friendRequestDisabled}
color={ color={
FriendStatus == 1 ? "red" FriendStatus == 1
: isRequestedStatus ? ? "red"
"light" : isRequestedStatus
? "light"
: "blue" : "blue"
} }
onClick={() => _addToFriends()} onClick={() => _addToFriends()}
> >
{FriendStatus == 1 ? {FriendStatus == 1
"Удалить из друзей" ? "Удалить из друзей"
: isRequestedStatus ? : isRequestedStatus
"Заявка отправлена" ? "Заявка отправлена"
: "Добавить в друзья"} : "Добавить в друзья"}
</Button> </Button>
)} )}
<Button <Button
color={!props.is_blocked ? "red" : "blue"} color={!props.is_blocked ? "red" : "blue"}
disabled={actionsDisabled} disabled={blockRequestDisabled}
onClick={() => _addToBlocklist()} onClick={() => _addToBlocklist()}
> >
{!props.is_blocked ? "Заблокировать" : "Разблокировать"} {!props.is_blocked ? "Заблокировать" : "Разблокировать"}

View file

@ -1,13 +1,11 @@
"use client"; "use client";
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react"; import { Button, Modal, Textarea } from "flowbite-react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { Spinner } from "../Spinner/Spinner"; import { Spinner } from "../Spinner/Spinner";
import { unixToDate } from "#/api/utils"; import { unixToDate } from "#/api/utils";
import { toast } from "react-toastify";
import { tryCatchAPI } from "#/api/utils";
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
export const ProfileEditLoginModal = (props: { export const ProfileEditLoginModal = (props: {
@ -31,33 +29,21 @@ export const ProfileEditLoginModal = (props: {
const [_loginLength, _setLoginLength] = useState(0); const [_loginLength, _setLoginLength] = useState(0);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const userStore = useUserStore(); const userStore = useUserStore();
const theme = useThemeMode();
useEffect(() => { useEffect(() => {
async function _fetchLogin() {
setLoading(true); setLoading(true);
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.settings.login.info}?token=${props.token}`) fetch(`${ENDPOINTS.user.settings.login.info}?token=${props.token}`)
); .then((res) => {
if (res.ok) {
if (error) { return res.json();
toast.error("Ошибка получения текущего никнейма", {
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setLoading(false);
props.setIsOpen(false);
return;
} }
})
.then((data) => {
_setLoginData(data); _setLoginData(data);
_setLogin(data.login); _setLogin(data.login);
_setLoginLength(data.login.length); _setLoginLength(data.login.length);
setLoading(false); setLoading(false);
} });
_fetchLogin();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.isOpen]); }, [props.isOpen]);
@ -66,62 +52,31 @@ export const ProfileEditLoginModal = (props: {
_setLoginLength(e.target.value.length); _setLoginLength(e.target.value.length);
} }
async function _setLoginSetting() { function _setLoginSetting() {
setSending(true);
if (!_login || _login == "") { if (!_login || _login == "") {
toast.error("Никнейм не может быть пустым", { alert("Никнейм не может быть пустым");
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return; return;
} }
setSending(true);
const tid = toast.loading("Обновляем никнейм...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch( fetch(
`${ENDPOINTS.user.settings.login.change}?login=${encodeURIComponent( `${ENDPOINTS.user.settings.login.change}?login=${encodeURIComponent(
_login _login
)}&token=${props.token}` )}&token=${props.token}`
) )
); .then((res) => {
if (res.ok) {
if (error) { return res.json();
let message = `Ошибка обновления никнейма: ${error.code}`; } else {
if (error.code == 3) { new Error("failed to send data");
message = "Данный никнейм уже существует, попробуйте другой";
} }
toast.update(tid, { })
render: message, .then((data) => {
type: "error", if (data.code == 3) {
autoClose: 2500, alert("Данный никнейм уже существует, попробуйте другой");
isLoading: false,
closeOnClick: true,
draggable: true,
});
setSending(false); setSending(false);
return; return;
} }
toast.update(tid, {
render: "Никнейм обновлён",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
@ -129,6 +84,11 @@ export const ProfileEditLoginModal = (props: {
props.setLogin(_login); props.setLogin(_login);
setSending(false); setSending(false);
props.setIsOpen(false); props.setIsOpen(false);
})
.catch((err) => {
console.log(err);
setSending(false);
});
} }
return ( return (
@ -140,12 +100,13 @@ export const ProfileEditLoginModal = (props: {
> >
<Modal.Header>Изменить никнейм</Modal.Header> <Modal.Header>Изменить никнейм</Modal.Header>
<Modal.Body> <Modal.Body>
{loading ? {loading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Spinner /> <Spinner />
</div> </div>
: <> ) : (
{!_loginData.is_change_available ? <>
{!_loginData.is_change_available ? (
<> <>
<p>Вы недавно изменили никнейм</p> <p>Вы недавно изменили никнейм</p>
<p> <p>
@ -155,7 +116,8 @@ export const ProfileEditLoginModal = (props: {
</span> </span>
</p> </p>
</> </>
: <> ) : (
<>
<Textarea <Textarea
disabled={sending} disabled={sending}
rows={1} rows={1}
@ -170,9 +132,9 @@ export const ProfileEditLoginModal = (props: {
{_loginLength}/20 {_loginLength}/20
</p> </p>
</> </>
} )}
</> </>
} )}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
{_loginData.is_change_available && ( {_loginData.is_change_available && (
@ -184,11 +146,7 @@ export const ProfileEditLoginModal = (props: {
Сохранить Сохранить
</Button> </Button>
)} )}
<Button <Button color="red" onClick={() => props.setIsOpen(false)}>
color="red"
onClick={() => props.setIsOpen(false)}
disabled={sending || loading}
>
Отмена Отмена
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View file

@ -1,11 +1,11 @@
"use client"; "use client";
import { FileInput, Label, Modal, useThemeMode } from "flowbite-react"; import { FileInput, Label, Modal } from "flowbite-react";
import { Spinner } from "../Spinner/Spinner"; import { Spinner } from "../Spinner/Spinner";
import useSWR from "swr"; import useSWR from "swr";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { b64toBlob, tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils"; import { b64toBlob, unixToDate } from "#/api/utils";
import { ProfileEditPrivacyModal } from "./Profile.EditPrivacyModal"; import { ProfileEditPrivacyModal } from "./Profile.EditPrivacyModal";
import { ProfileEditStatusModal } from "./Profile.EditStatusModal"; import { ProfileEditStatusModal } from "./Profile.EditStatusModal";
import { ProfileEditSocialModal } from "./Profile.EditSocialModal"; import { ProfileEditSocialModal } from "./Profile.EditSocialModal";
@ -13,7 +13,20 @@ import { CropModal } from "../CropModal/CropModal";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { ProfileEditLoginModal } from "./Profile.EditLoginModal"; import { ProfileEditLoginModal } from "./Profile.EditLoginModal";
import { toast } from "react-toastify";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export const ProfileEditModal = (props: { export const ProfileEditModal = (props: {
isOpen: boolean; isOpen: boolean;
@ -24,7 +37,10 @@ export const ProfileEditModal = (props: {
const [privacyModalOpen, setPrivacyModalOpen] = useState(false); const [privacyModalOpen, setPrivacyModalOpen] = useState(false);
const [statusModalOpen, setStatusModalOpen] = useState(false); const [statusModalOpen, setStatusModalOpen] = useState(false);
const [socialModalOpen, setSocialModalOpen] = useState(false); const [socialModalOpen, setSocialModalOpen] = useState(false);
const [avatarModalOpen, setAvatarModalOpen] = useState(false);
const [loginModalOpen, setLoginModalOpen] = useState(false); const [loginModalOpen, setLoginModalOpen] = useState(false);
const [avatarUri, setAvatarUri] = useState(null);
const [tempAvatarUri, setTempAvatarUri] = useState(null);
const [privacyModalSetting, setPrivacyModalSetting] = useState("none"); const [privacyModalSetting, setPrivacyModalSetting] = useState("none");
const [privacySettings, setPrivacySettings] = useState({ const [privacySettings, setPrivacySettings] = useState({
privacy_stats: 9, privacy_stats: 9,
@ -40,14 +56,6 @@ export const ProfileEditModal = (props: {
const [login, setLogin] = useState(""); const [login, setLogin] = useState("");
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const userStore = useUserStore(); const userStore = useUserStore();
const theme = useThemeMode();
const [avatarModalProps, setAvatarModalProps] = useState({
isOpen: false,
isActionsDisabled: false,
selectedImage: null,
croppedImage: null,
});
const privacy_stat_act_social_text = { const privacy_stat_act_social_text = {
0: "Все пользователи", 0: "Все пользователи",
@ -62,11 +70,7 @@ export const ProfileEditModal = (props: {
}; };
function useFetchInfo(url: string) { function useFetchInfo(url: string) {
if (!props.token) { const { data, isLoading, error } = useSWR(url, fetcher);
url = "";
}
const { data, isLoading, error } = useSWR(url, useSWRfetcher);
return [data, isLoading, error]; return [data, isLoading, error];
} }
@ -77,17 +81,15 @@ export const ProfileEditModal = (props: {
`${ENDPOINTS.user.settings.login.info}?token=${props.token}` `${ENDPOINTS.user.settings.login.info}?token=${props.token}`
); );
const handleAvatarPreview = (e: any) => { const handleFileRead = (e, fileReader) => {
const file = e.target.files[0];
const fileReader = new FileReader();
fileReader.onloadend = () => {
const content = fileReader.result; const content = fileReader.result;
setAvatarModalProps({ setTempAvatarUri(content);
...avatarModalProps, };
isOpen: true,
selectedImage: content, const handleFilePreview = (file) => {
}); const fileReader = new FileReader();
e.target.value = ""; fileReader.onloadend = (e) => {
handleFileRead(e, fileReader);
}; };
fileReader.readAsDataURL(file); fileReader.readAsDataURL(file);
}; };
@ -115,8 +117,8 @@ export const ProfileEditModal = (props: {
}, [loginData]); }, [loginData]);
useEffect(() => { useEffect(() => {
async function _uploadAvatar() { if (avatarUri) {
let block = avatarModalProps.croppedImage.split(";"); let block = avatarUri.split(";");
let contentType = block[0].split(":")[1]; let contentType = block[0].split(":")[1];
let realData = block[1].split(",")[1]; let realData = block[1].split(",")[1];
const blob = b64toBlob(realData, contentType); const blob = b64toBlob(realData, contentType);
@ -124,73 +126,23 @@ export const ProfileEditModal = (props: {
const formData = new FormData(); const formData = new FormData();
formData.append("image", blob, "cropped.jpg"); formData.append("image", blob, "cropped.jpg");
formData.append("name", "image"); formData.append("name", "image");
const uploadRes = fetch(
setAvatarModalProps( `${ENDPOINTS.user.settings.avatar}?token=${props.token}`,
(state) => (state = { ...state, isActionsDisabled: true }) {
);
const tid = toast.loading("Обновление аватара...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.settings.avatar}?token=${props.token}`, {
method: "POST", method: "POST",
body: formData, body: formData,
})
);
if (error) {
toast.update(tid, {
render: "Ошибка обновления аватара",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setAvatarModalProps(
(state) => (state = { ...state, isActionsDisabled: false })
);
return;
} }
).then((res) => {
toast.update(tid, { if (res.ok) {
render: "Аватар обновлён",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setAvatarModalProps(
(state) =>
(state = {
isOpen: false,
isActionsDisabled: false,
selectedImage: null,
croppedImage: null,
})
);
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
userStore.checkAuth(); userStore.checkAuth();
} }
});
if (avatarModalProps.croppedImage) {
_uploadAvatar();
}
}, [avatarModalProps.croppedImage]);
if (!prefData || !loginData || prefError || loginError) {
return <></>;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [avatarUri]);
return ( return (
<> <>
@ -201,9 +153,10 @@ export const ProfileEditModal = (props: {
> >
<Modal.Header>Редактирование профиля</Modal.Header> <Modal.Header>Редактирование профиля</Modal.Header>
<Modal.Body> <Modal.Body>
{prefLoading ? {prefLoading ? (
<Spinner /> <Spinner />
: <div className="flex flex-col gap-4"> ) : (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 pb-4 border-b-2 border-gray-300 border-solid"> <div className="flex flex-col gap-2 pb-4 border-b-2 border-gray-300 border-solid">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -221,14 +174,15 @@ export const ProfileEditModal = (props: {
className="hidden" className="hidden"
accept="image/jpg, image/jpeg, image/png" accept="image/jpg, image/jpeg, image/png"
onChange={(e) => { onChange={(e) => {
handleAvatarPreview(e); handleFilePreview(e.target.files[0]);
setAvatarModalOpen(true);
}} }}
/> />
<div> <div>
<p className="text-lg">Изменить фото профиля</p> <p className="text-lg">Изменить фото профиля</p>
<p className="text-base text-gray-500 dark:text-gray-400"> <p className="text-base text-gray-500 dark:text-gray-400">
{prefData.is_change_avatar_banned ? {prefData.is_change_avatar_banned
`Заблокировано до ${unixToDate( ? `Заблокировано до ${unixToDate(
prefData.ban_change_avatar_expires, prefData.ban_change_avatar_expires,
"full" "full"
)}` )}`
@ -257,8 +211,8 @@ export const ProfileEditModal = (props: {
> >
<p className="text-lg">Изменить никнейм</p> <p className="text-lg">Изменить никнейм</p>
<p className="text-base text-gray-500 dark:text-gray-400"> <p className="text-base text-gray-500 dark:text-gray-400">
{prefData.is_change_login_banned ? {prefData.is_change_login_banned
`Заблокировано до ${unixToDate( ? `Заблокировано до ${unixToDate(
prefData.ban_change_login_expires, prefData.ban_change_login_expires,
"full" "full"
)}` )}`
@ -376,8 +330,8 @@ export const ProfileEditModal = (props: {
<div className="p-2 mt-2 cursor-not-allowed"> <div className="p-2 mt-2 cursor-not-allowed">
<p className="text-lg">Связанные аккаунты</p> <p className="text-lg">Связанные аккаунты</p>
<p className="text-base text-gray-500 dark:text-gray-400"> <p className="text-base text-gray-500 dark:text-gray-400">
{socialBounds.vk || socialBounds.google ? {socialBounds.vk || socialBounds.google
"Аккаунт привязан к:" ? "Аккаунт привязан к:"
: "не привязан к сервисам"}{" "} : "не привязан к сервисам"}{" "}
{socialBounds.vk && "ВК"} {socialBounds.vk && "ВК"}
{socialBounds.vk && socialBounds.google && ", "} {socialBounds.vk && socialBounds.google && ", "}
@ -386,11 +340,9 @@ export const ProfileEditModal = (props: {
</div> </div>
</div> </div>
</div> </div>
} )}
</Modal.Body> </Modal.Body>
</Modal> </Modal>
{props.token ?
<>
<ProfileEditPrivacyModal <ProfileEditPrivacyModal
isOpen={privacyModalOpen} isOpen={privacyModalOpen}
setIsOpen={setPrivacyModalOpen} setIsOpen={setPrivacyModalOpen}
@ -414,15 +366,17 @@ export const ProfileEditModal = (props: {
profile_id={props.profile_id} profile_id={props.profile_id}
/> />
<CropModal <CropModal
{...avatarModalProps} src={tempAvatarUri}
cropParams={{ setSrc={setAvatarUri}
aspectRatio: 1 / 1, setTempSrc={setTempAvatarUri}
forceAspect: true, aspectRatio={1 / 1}
guides: true, guides={true}
width: 600, quality={100}
height: 600, isOpen={avatarModalOpen}
}} setIsOpen={setAvatarModalOpen}
setCropModalProps={setAvatarModalProps} forceAspect={true}
width={600}
height={600}
/> />
<ProfileEditLoginModal <ProfileEditLoginModal
isOpen={loginModalOpen} isOpen={loginModalOpen}
@ -432,7 +386,5 @@ export const ProfileEditModal = (props: {
profile_id={props.profile_id} profile_id={props.profile_id}
/> />
</> </>
: ""}
</>
); );
}; };

View file

@ -1,10 +1,8 @@
"use client"; "use client";
import { Modal, useThemeMode } from "flowbite-react"; import { Modal } from "flowbite-react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-toastify";
import { tryCatchAPI } from "#/api/utils";
export const ProfileEditPrivacyModal = (props: { export const ProfileEditPrivacyModal = (props: {
isOpen: boolean; isOpen: boolean;
@ -35,22 +33,10 @@ export const ProfileEditPrivacyModal = (props: {
}; };
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const theme = useThemeMode();
async function _setPrivacySetting(el: any) { function _setPrivacySetting(el: any) {
let privacySettings = structuredClone(props.privacySettings); let privacySettings = structuredClone(props.privacySettings);
setLoading(true); setLoading(true);
const tid = toast.loading("Обновление настроек приватности...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch(_endpoints[props.setting], { fetch(_endpoints[props.setting], {
method: "POST", method: "POST",
headers: { headers: {
@ -60,35 +46,20 @@ export const ProfileEditPrivacyModal = (props: {
permission: el.target.value, permission: el.target.value,
}), }),
}) })
); .then((res) => {
if (res.ok) {
if (error) {
toast.update(tid, {
render: "Ошибка обновления настроек приватности",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setLoading(false);
return;
}
toast.update(tid, {
render: "Настройки приватности обновлены",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setLoading(false); setLoading(false);
privacySettings[el.target.name] = el.target.value; privacySettings[el.target.name] = el.target.value;
props.setPrivacySettings(privacySettings); props.setPrivacySettings(privacySettings);
props.setIsOpen(false); props.setIsOpen(false)
} else {
new Error("failed to send data");
}
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} }
return ( return (
@ -100,10 +71,10 @@ export const ProfileEditPrivacyModal = (props: {
> >
<Modal.Header>{setting_text[props.setting]}</Modal.Header> <Modal.Header>{setting_text[props.setting]}</Modal.Header>
<Modal.Body> <Modal.Body>
{props.setting != "none" ? {props.setting != "none" ? (
<> <>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{props.setting == "privacy_friend_requests" ? {props.setting == "privacy_friend_requests" ? (
<> <>
<div className="flex items-center"> <div className="flex items-center">
<input <input
@ -142,7 +113,8 @@ export const ProfileEditPrivacyModal = (props: {
</label> </label>
</div> </div>
</> </>
: <> ) : (
<>
<div className="flex items-center"> <div className="flex items-center">
<input <input
disabled={loading} disabled={loading}
@ -198,10 +170,12 @@ export const ProfileEditPrivacyModal = (props: {
</label> </label>
</div> </div>
</> </>
} )}
</div> </div>
</> </>
: ""} ) : (
""
)}
</Modal.Body> </Modal.Body>
</Modal> </Modal>
); );

View file

@ -1,12 +1,10 @@
"use client"; "use client";
import { Button, Modal, Label, TextInput, useThemeMode } from "flowbite-react"; import { Button, Modal, Label, TextInput } from "flowbite-react";
import { Spinner } from "../Spinner/Spinner"; import { Spinner } from "../Spinner/Spinner";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { toast } from "react-toastify";
import { tryCatchAPI } from "#/api/utils";
export const ProfileEditSocialModal = (props: { export const ProfileEditSocialModal = (props: {
isOpen: boolean; isOpen: boolean;
@ -24,7 +22,6 @@ export const ProfileEditSocialModal = (props: {
ttPage: "", ttPage: "",
}); });
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const theme = useThemeMode();
function _addUrl(username: string, social: string) { function _addUrl(username: string, social: string) {
if (!username) { if (!username) {
@ -55,27 +52,14 @@ export const ProfileEditSocialModal = (props: {
} }
useEffect(() => { useEffect(() => {
async function _fetchSettings() {
setLoading(true); setLoading(true);
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.settings.socials.info}?token=${props.token}`) fetch(`${ENDPOINTS.user.settings.socials.info}?token=${props.token}`)
); .then((res) => {
if (res.ok) {
if (error) { return res.json();
toast.error("Ошибка получения соц. сетей", {
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setLoading(false);
props.setIsOpen(false);
return;
} }
})
.then((data) => {
setSocials({ setSocials({
vkPage: data.vk_page, vkPage: data.vk_page,
tgPage: data.tg_page, tgPage: data.tg_page,
@ -84,22 +68,21 @@ export const ProfileEditSocialModal = (props: {
ttPage: data.tt_page, ttPage: data.tt_page,
}); });
setLoading(false); setLoading(false);
} });
_fetchSettings();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.isOpen]); }, [props.isOpen]);
function handleInput(e: any) { function handleInput(e: any) {
const social = { const social = {
...socials, ...socials,
[e.target.name]: e.target.value, [e.target.name]: e.target.value
}; }
setSocials(social); setSocials(social);
} }
async function _setSocialSetting() { function _setSocialSetting() {
const body = { const data = {
vkPage: _removeUrl(socials.vkPage), vkPage: _removeUrl(socials.vkPage),
tgPage: _removeUrl(socials.tgPage), tgPage: _removeUrl(socials.tgPage),
discordPage: _removeUrl(socials.discordPage), discordPage: _removeUrl(socials.discordPage),
@ -108,53 +91,28 @@ export const ProfileEditSocialModal = (props: {
}; };
setUpdating(true); setUpdating(true);
const tid = toast.loading("Обновление соц. сетей...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.settings.socials.edit}?token=${props.token}`, { fetch(`${ENDPOINTS.user.settings.socials.edit}?token=${props.token}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(body), body: JSON.stringify(data),
}) })
); .then((res) => {
if (res.ok) {
if (error) {
toast.update(tid, {
render: "Ошибка обновления соц. сетей",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setUpdating(false);
return;
}
toast.update(tid, {
render: "Соц. сети обновлены",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
setUpdating(false); setUpdating(false);
props.setIsOpen(false); props.setIsOpen(false);
} else {
new Error("failed to send data");
}
})
.catch((err) => {
console.log(err);
setUpdating(false);
});
} }
return ( return (
@ -170,11 +128,12 @@ export const ProfileEditSocialModal = (props: {
Укажите ссылки на свои социальные сети, чтобы другие пользователи Укажите ссылки на свои социальные сети, чтобы другие пользователи
могли с вами связаться могли с вами связаться
</p> </p>
{loading ? {loading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Spinner /> <Spinner />
</div> </div>
: <div className="flex flex-col gap-4 py-4"> ) : (
<div className="flex flex-col gap-4 py-4">
<div> <div>
<div className="block mb-2"> <div className="block mb-2">
<Label htmlFor="vk-page" value="ВКонтакте" /> <Label htmlFor="vk-page" value="ВКонтакте" />
@ -236,7 +195,7 @@ export const ProfileEditSocialModal = (props: {
/> />
</div> </div>
</div> </div>
} )}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button <Button
@ -246,11 +205,7 @@ export const ProfileEditSocialModal = (props: {
> >
Сохранить Сохранить
</Button> </Button>
<Button <Button color="red" onClick={() => props.setIsOpen(false)}>
color="red"
onClick={() => props.setIsOpen(false)}
disabled={updating}
>
Отмена Отмена
</Button> </Button>
</Modal.Footer> </Modal.Footer>

View file

@ -1,12 +1,9 @@
"use client"; "use client";
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react"; import { Button, Modal, Textarea } from "flowbite-react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useSWRConfig } from "swr"; import { useSWRConfig } from "swr";
import { toast } from "react-toastify";
import { tryCatchAPI } from "#/api/utils";
import { useUserStore } from "#/store/auth";
export const ProfileEditStatusModal = (props: { export const ProfileEditStatusModal = (props: {
isOpen: boolean; isOpen: boolean;
@ -20,8 +17,6 @@ export const ProfileEditStatusModal = (props: {
const [_status, _setStatus] = useState(""); const [_status, _setStatus] = useState("");
const [_stringLength, _setStringLength] = useState(0); const [_stringLength, _setStringLength] = useState(0);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const theme = useThemeMode();
const userStore = useUserStore();
useEffect(() => { useEffect(() => {
_setStatus(props.status); _setStatus(props.status);
@ -34,19 +29,8 @@ export const ProfileEditStatusModal = (props: {
_setStringLength(e.target.value.length); _setStringLength(e.target.value.length);
} }
async function _setStatusSetting() { function _setStatusSetting() {
setLoading(true); setLoading(true);
const tid = toast.loading("Обновление статуса...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.settings.status}?token=${props.token}`, { fetch(`${ENDPOINTS.user.settings.status}?token=${props.token}`, {
method: "POST", method: "POST",
headers: { headers: {
@ -56,37 +40,22 @@ export const ProfileEditStatusModal = (props: {
status: _status, status: _status,
}), }),
}) })
); .then((res) => {
if (res.ok) {
if (error) {
toast.update(tid, {
render: "Ошибка обновления статуса",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setLoading(false);
return;
}
toast.update(tid, {
render: "Статус обновлён",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
props.setStatus(_status);
mutate( mutate(
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}` `${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
); );
userStore.checkAuth();
setLoading(false); setLoading(false);
props.setStatus(_status);
props.setIsOpen(false); props.setIsOpen(false);
} else {
new Error("failed to send data");
}
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} }
return ( return (
@ -113,13 +82,7 @@ export const ProfileEditStatusModal = (props: {
</p> </p>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button <Button color="blue" onClick={() => _setStatusSetting()} disabled={loading}>Сохранить</Button>
color="blue"
onClick={() => _setStatusSetting()}
disabled={loading}
>
Сохранить
</Button>
<Button color="red" onClick={() => props.setIsOpen(false)}> <Button color="red" onClick={() => props.setIsOpen(false)}>
Отмена Отмена
</Button> </Button>

View file

@ -11,7 +11,7 @@ import type {
FlowbiteCarouselControlTheme, FlowbiteCarouselControlTheme,
} from "flowbite-react"; } from "flowbite-react";
import Image from "next/image"; import Image from "next/image";
import { unixToDate, useSWRfetcher } from "#/api/utils"; import { unixToDate } from "#/api/utils";
import Link from "next/link"; import Link from "next/link";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
@ -95,6 +95,7 @@ const ProfileReleaseRatingsModal = (props: {
profile_id: number; profile_id: number;
token: string | null; token: string | null;
}) => { }) => {
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const [currentRef, setCurrentRef] = useState<any>(null); const [currentRef, setCurrentRef] = useState<any>(null);
const modalRef = useCallback((ref) => { const modalRef = useCallback((ref) => {
setCurrentRef(ref); setCurrentRef(ref);
@ -109,9 +110,23 @@ const ProfileReleaseRatingsModal = (props: {
return url; return url;
}; };
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -123,6 +138,7 @@ const ProfileReleaseRatingsModal = (props: {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -154,8 +170,8 @@ const ProfileReleaseRatingsModal = (props: {
onScroll={handleScroll} onScroll={handleScroll}
ref={modalRef} ref={modalRef}
> >
{isLoading && <Spinner />} {!isLoadingEnd && isLoading && <Spinner />}
{content && content.length > 0 ? ( {isLoadingEnd && !isLoading && content.length > 0 ? (
content.map((release) => { content.map((release) => {
return ( return (
<Link <Link

View file

@ -13,7 +13,7 @@ export const ReleaseInfoBasics = (props: {
const [isFullDescription, setIsFullDescription] = useState(false); const [isFullDescription, setIsFullDescription] = useState(false);
return ( return (
<Card className="h-full row-span-2"> <Card className="h-full">
<div className="flex flex-col w-full h-full gap-4 lg:flex-row"> <div className="flex flex-col w-full h-full gap-4 lg:flex-row">
<Image <Image
className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700" className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700"

View file

@ -28,7 +28,7 @@ export const ReleaseInfoInfo = (props: {
genres: string; genres: string;
}) => { }) => {
return ( return (
<Card> <Card className="h-full">
<Table> <Table>
<Table.Body> <Table.Body>
<Table.Row> <Table.Row>

View file

@ -3,9 +3,6 @@ import { ENDPOINTS } from "#/api/config";
import Link from "next/link"; import Link from "next/link";
import useSWRInfinite from "swr/infinite"; import useSWRInfinite from "swr/infinite";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { tryCatchAPI, useSWRfetcher } from "#/api/utils";
import { toast } from "react-toastify";
import { useThemeMode } from "flowbite-react";
const lists = [ const lists = [
{ list: 0, name: "Не смотрю" }, { list: 0, name: "Не смотрю" },
@ -34,108 +31,25 @@ export const ReleaseInfoUserList = (props: {
}) => { }) => {
const [AddReleaseToCollectionModalOpen, setAddReleaseToCollectionModalOpen] = const [AddReleaseToCollectionModalOpen, setAddReleaseToCollectionModalOpen] =
useState(false); useState(false);
const [favButtonDisabled, setFavButtonDisabled] = useState(false);
const [listEventDisabled, setListEventDisabled] = useState(false);
const theme = useThemeMode();
function _addToFavorite() { function _addToFavorite() {
async function _setFav(url: string) {
setFavButtonDisabled(true);
const tid = toast.loading(
!props.isFavorite ?
"Добавляем в избранное..."
: "Удаляем из избранное...",
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render:
!props.isFavorite ?
"Ошибка добавления в избранное"
: "Ошибка удаления из избранного",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setFavButtonDisabled(false);
return;
}
toast.update(tid, {
render:
!props.isFavorite ? "Добавлено в избранное" : "Удалено из избранного",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
props.setIsFavorite(!props.isFavorite);
setFavButtonDisabled(false);
}
if (props.token) { if (props.token) {
let url = `${ENDPOINTS.user.favorite}/add/${props.release_id}?token=${props.token}`; props.setIsFavorite(!props.isFavorite);
if (props.isFavorite) { if (props.isFavorite) {
url = `${ENDPOINTS.user.favorite}/delete/${props.release_id}?token=${props.token}`; fetch(
`${ENDPOINTS.user.favorite}/delete/${props.release_id}?token=${props.token}`
);
} else {
fetch(
`${ENDPOINTS.user.favorite}/add/${props.release_id}?token=${props.token}`
);
} }
_setFav(url);
} }
} }
function _addToList(list: number) { function _addToList(list: number) {
async function _setList(url: string) {
setListEventDisabled(true);
const tid = toast.loading("Добавляем в список...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: `Ошибка добавления в список: ${lists[list].name}`,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setListEventDisabled(false);
return;
}
toast.update(tid, {
render: `Добавлено в список: ${lists[list].name}`,
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setListEventDisabled(false);
props.setUserList(list);
}
if (props.token) { if (props.token) {
_setList( props.setUserList(list);
fetch(
`${ENDPOINTS.user.bookmark}/add/${list}/${props.release_id}?token=${props.token}` `${ENDPOINTS.user.bookmark}/add/${list}/${props.release_id}?token=${props.token}`
); );
} }
@ -144,7 +58,7 @@ export const ReleaseInfoUserList = (props: {
return ( return (
<Card className="h-full"> <Card className="h-full">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
<Button color={"blue"} size="sm" className={props.token ? "w-full sm:w-[49%] lg:w-full 2xl:w-[60%]" : "w-full"}> <Button color={"blue"} size="sm" className="w-full lg:w-auto ">
<Link href={`/release/${props.release_id}/collections`}> <Link href={`/release/${props.release_id}/collections`}>
Показать в коллекциях{" "} Показать в коллекциях{" "}
<span className="p-1 ml-1 text-gray-500 rounded bg-gray-50"> <span className="p-1 ml-1 text-gray-500 rounded bg-gray-50">
@ -156,14 +70,14 @@ export const ReleaseInfoUserList = (props: {
<Button <Button
color={"blue"} color={"blue"}
size="sm" size="sm"
className="w-full sm:w-1/2 lg:w-full 2xl:w-[39%]" className="w-full lg:w-auto lg:flex-1"
onClick={() => setAddReleaseToCollectionModalOpen(true)} onClick={() => setAddReleaseToCollectionModalOpen(true)}
> >
В коллекцию{" "} В коллекцию{" "}
<span className="w-6 h-6 iconify mdi--bookmark-add "></span> <span className="w-6 h-6 iconify mdi--bookmark-add "></span>
</Button> </Button>
)} )}
{props.token ? {props.token ? (
<> <>
<Dropdown <Dropdown
label={lists[props.userList].name} label={lists[props.userList].name}
@ -171,7 +85,6 @@ export const ReleaseInfoUserList = (props: {
theme={DropdownTheme} theme={DropdownTheme}
color="blue" color="blue"
size="sm" size="sm"
disabled={listEventDisabled}
> >
{lists.map((list) => ( {lists.map((list) => (
<Dropdown.Item <Dropdown.Item
@ -188,7 +101,6 @@ export const ReleaseInfoUserList = (props: {
_addToFavorite(); _addToFavorite();
}} }}
size="sm" size="sm"
disabled={favButtonDisabled}
> >
<span <span
className={`iconify w-6 h-6 ${ className={`iconify w-6 h-6 ${
@ -197,11 +109,9 @@ export const ReleaseInfoUserList = (props: {
></span> ></span>
</Button> </Button>
</> </>
: <div className="flex items-center justify-center w-full gap-2 px-2 py-2 text-gray-600 bg-gray-200 rounded-lg dark:text-gray-200 dark:bg-gray-600"> ) : (
<span className="w-6 h-6 iconify material-symbols--info-outline"></span>
<p>Войдите что-бы добавить в список, избранное или коллекцию</p> <p>Войдите что-бы добавить в список, избранное или коллекцию</p>
</div> )}
}
</div> </div>
<AddReleaseToCollectionModal <AddReleaseToCollectionModal
isOpen={AddReleaseToCollectionModalOpen} isOpen={AddReleaseToCollectionModalOpen}
@ -214,6 +124,20 @@ export const ReleaseInfoUserList = (props: {
); );
}; };
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
const AddReleaseToCollectionModal = (props: { const AddReleaseToCollectionModal = (props: {
isOpen: boolean; isOpen: boolean;
setIsOpen: (isopen: boolean) => void; setIsOpen: (isopen: boolean) => void;
@ -226,11 +150,10 @@ const AddReleaseToCollectionModal = (props: {
if (previousPageData && !previousPageData.content.length) return null; if (previousPageData && !previousPageData.content.length) return null;
return `${ENDPOINTS.collection.userCollections}/${props.profile_id}/${pageIndex}?token=${props.token}`; return `${ENDPOINTS.collection.userCollections}/${props.profile_id}/${pageIndex}?token=${props.token}`;
}; };
const theme = useThemeMode();
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -265,53 +188,28 @@ const AddReleaseToCollectionModal = (props: {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]); }, [scrollPosition]);
function _addToCollection(collection: any) {
async function _ToCollection(url: string) {
const tid = toast.loading(
`Добавление в коллекцию ${collection.title}... `,
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
let message = `${error.message}, code: ${error.code}`;
if (error.code == 5) {
message = "Релиз уже есть в коллекции";
}
toast.update(tid, {
render: message,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
theme: theme.mode == "light" ? "light" : "dark",
});
return;
}
toast.update(tid, {
render: "Релиз добавлен в коллекцию",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
theme: theme.mode == "light" ? "light" : "dark",
});
}
function _addToCollection(collection_id: number) {
if (props.token) { if (props.token) {
_ToCollection( fetch(
`${ENDPOINTS.collection.addRelease}/${collection.id}?release_id=${props.release_id}&token=${props.token}` `${ENDPOINTS.collection.addRelease}/${collection_id}?release_id=${props.release_id}&token=${props.token}`
)
.then((res) => {
if (!res.ok) {
alert("Ошибка добавления релиза в коллекцию.");
} else {
return res.json();
}
})
.then((data) => {
if (data.code != 0) {
alert(
"Не удалось добавить релиз в коллекцию, возможно он уже в ней находится."
); );
} else {
props.setIsOpen(false);
}
});
} }
} }
@ -327,15 +225,15 @@ const AddReleaseToCollectionModal = (props: {
onScroll={handleScroll} onScroll={handleScroll}
ref={modalRef} ref={modalRef}
> >
{content && content.length > 0 ? {content && content.length > 0
content.map((collection) => ( ? content.map((collection) => (
<button <button
className="relative w-full h-64 overflow-hidden bg-center bg-no-repeat bg-cover rounded-sm group-hover:animate-bg_zoom animate-bg_zoom_rev group-hover:[background-size:110%] " className="relative w-full h-64 overflow-hidden bg-center bg-no-repeat bg-cover rounded-sm group-hover:animate-bg_zoom animate-bg_zoom_rev group-hover:[background-size:110%] "
style={{ style={{
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${collection.image})`, backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${collection.image})`,
}} }}
key={`collection_${collection.id}`} key={`collection_${collection.id}`}
onClick={() => _addToCollection(collection)} onClick={() => _addToCollection(collection.id)}
> >
<div className="absolute bottom-0 left-0 gap-1 p-2"> <div className="absolute bottom-0 left-0 gap-1 p-2">
<p className="text-xl font-bold text-white"> <p className="text-xl font-bold text-white">

View file

@ -36,7 +36,7 @@ export const ReleaseLink169 = (props: any) => {
<Image <Image
src={props.image} src={props.image}
fill={true} fill={true}
alt={props.title || ""} alt={props.title}
className="-z-[1] object-cover" className="-z-[1] object-cover"
sizes=" sizes="
(max-width: 768px) 300px, (max-width: 768px) 300px,

View file

@ -33,7 +33,7 @@ export const ReleaseLink169Poster = (props: any) => {
src={props.image} src={props.image}
height={250} height={250}
width={250} width={250}
alt={props.title || ""} alt={props.title}
className="object-cover aspect-[9/16] h-auto w-24 md:w-32 lg:w-48 rounded-md" className="object-cover aspect-[9/16] h-auto w-24 md:w-32 lg:w-48 rounded-md"
/> />
</div> </div>

View file

@ -43,7 +43,7 @@ export const ReleaseLink169Related = (props: any) => {
src={props.image} src={props.image}
height={250} height={250}
width={250} width={250}
alt={props.title || ""} alt={props.title}
className="object-cover aspect-[9/16] lg:aspect-[12/16] h-auto w-24 md:w-32 lg:w-48 rounded-md" className="object-cover aspect-[9/16] lg:aspect-[12/16] h-auto w-24 md:w-32 lg:w-48 rounded-md"
/> />
</div> </div>

View file

@ -590,7 +590,7 @@ export default function Page(props: { children: any, className?: string }) {
<!-- Skip opening Button --> <!-- Skip opening Button -->
<media-seek-forward-button class="media-button" seekoffset="90"> <media-seek-forward-button class="media-button" seekoffset="90">
<svg slot="icon" width="256" height="256" viewBox="-65 -75 400 400"> <svg slot="icon" width="256" height="256" viewBox="-75 -75 400 400">
<path fill="#fff" d="m246.52 118l-88.19-56.13a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 140 71.84v44.59L54.33 61.87a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 36 71.84v112.32a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L140 139.57v44.59a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L246.52 138a11.81 11.81 0 0 0 0-19.94Zm-108.3 13.19L50 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Zm104 0L154 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Z" /> <path fill="#fff" d="m246.52 118l-88.19-56.13a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 140 71.84v44.59L54.33 61.87a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 36 71.84v112.32a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L140 139.57v44.59a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L246.52 138a11.81 11.81 0 0 0 0-19.94Zm-108.3 13.19L50 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Zm104 0L154 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Z" />
</svg> </svg>
</media-seek-forward-button> </media-seek-forward-button>

View file

@ -157,6 +157,7 @@ export const ReleasePlayer = (props: { id: number }) => {
} }
}) })
.catch((err) => { .catch((err) => {
console.log(err);
_setError("Ошибка получение ответа от сервера"); _setError("Ошибка получение ответа от сервера");
return; return;
}); });

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { Button, Card } from "flowbite-react"; import { Card } from "flowbite-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
@ -14,7 +14,6 @@ import HlsVideo from "hls-video-element/react";
import VideoJS from "videojs-video-element/react"; import VideoJS from "videojs-video-element/react";
import MediaThemeSutro from "./MediaThemeSutro"; import MediaThemeSutro from "./MediaThemeSutro";
import { getAnonEpisodesWatched } from "./ReleasePlayer"; import { getAnonEpisodesWatched } from "./ReleasePlayer";
import { tryCatchPlayer, tryCatchAPI } from "#/api/utils";
export const ReleasePlayerCustom = (props: { export const ReleasePlayerCustom = (props: {
id: number; id: number;
@ -39,76 +38,48 @@ export const ReleasePlayerCustom = (props: {
useCustom: false, useCustom: false,
}); });
const [playbackRate, setPlaybackRate] = useState(1); const [playbackRate, setPlaybackRate] = useState(1);
const [playerError, setPlayerError] = useState(null);
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const playerPreferenceStore = useUserPlayerPreferencesStore(); const playerPreferenceStore = useUserPlayerPreferencesStore();
const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id); const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id);
const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id); const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id);
async function _fetchAPI( const _fetchVoiceover = async (release_id: number) => {
url: string, let url = `${ENDPOINTS.release.episode}/${release_id}`;
onErrorMsg: string, if (props.token) {
onErrorCodes?: Record<number, string> url += `?token=${props.token}`;
) {
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
let errorDetail = "Мы правда не знаем что произошло...";
if (error.name) {
if (error.name == "TypeError") {
errorDetail = "Не удалось подключиться к серверу";
} else {
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
}
}
if (error.code) {
if (Object.keys(onErrorCodes).includes(error.code.toString())) {
errorDetail = onErrorCodes[error.code.toString()];
} else {
errorDetail = `API вернуло ошибку: ${error.code}`;
}
}
setPlayerError({
message: onErrorMsg,
detail: errorDetail,
});
return null;
} }
const response = await fetch(url);
const data = await response.json();
return data; return data;
} };
async function _fetchPlayer(url: string) { const _fetchSource = async (release_id: number, voiceover_id: number) => {
const { data, error } = (await tryCatchPlayer(fetch(url))) as any; const response = await fetch(
if (error) { `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}`
let errorDetail = "Мы правда не знаем что произошло..."; );
const data = await response.json();
if (error.name) {
if (error.name == "TypeError") {
errorDetail = "Не удалось подключиться к серверу";
} else {
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
}
} else if (error.message) {
errorDetail = error.message;
}
setPlayerError({
message: "Не удалось получить ссылку на видео",
detail: errorDetail,
});
return null;
}
return data; return data;
};
const _fetchEpisode = async (
release_id: number,
voiceover_id: number,
source_id: number
) => {
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}/${source_id}`;
if (props.token) {
url += `?token=${props.token}`;
} }
const response = await fetch(url);
const data = await response.json();
return data;
};
const _fetchKodikManifest = async (url: string) => { const _fetchKodikManifest = async (url: string) => {
const data = await _fetchPlayer( const response = await fetch(
`https://anix-player.wah.su/?url=${url}&player=kodik` `https://anix-player.wah.su/?url=${url}&player=kodik`
); );
if (data) { const data = await response.json();
let lowQualityLink = data.links["360"][0].src; let lowQualityLink = data.links["360"][0].src;
if (lowQualityLink.includes("https://")) { if (lowQualityLink.includes("https://")) {
lowQualityLink = lowQualityLink.replace("https://", "//"); lowQualityLink = lowQualityLink.replace("https://", "//");
@ -141,8 +112,7 @@ export const ReleasePlayerCustom = (props: {
} }
if (data.links.hasOwnProperty("720")) { if (data.links.hasOwnProperty("720")) {
blobTxt += blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
"#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
!data.links["720"][0].src.startsWith("https:") ? !data.links["720"][0].src.startsWith("https:") ?
(blobTxt += `https:${data.links["720"][0].src}\n`) (blobTxt += `https:${data.links["720"][0].src}\n`)
: (blobTxt += `${data.links["720"][0].src}\n`); : (blobTxt += `${data.links["720"][0].src}\n`);
@ -154,16 +124,14 @@ export const ReleasePlayerCustom = (props: {
manifest = URL.createObjectURL(file); manifest = URL.createObjectURL(file);
} }
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
const _fetchAnilibriaManifest = async (url: string) => { const _fetchAnilibriaManifest = async (url: string) => {
const id = url.split("?id=")[1].split("&ep=")[0]; const id = url.split("?id=")[1].split("&ep=")[0];
const data = await _fetchPlayer(
`https://api.anilibria.tv/v3/title?id=${id}` const response = await fetch(`https://api.anilibria.tv/v3/title?id=${id}`);
); const data = await response.json();
if (data) {
const host = `https://${data.player.host}`; const host = `https://${data.player.host}`;
const ep = data.player.list[episode.selected.position]; const ep = data.player.list[episode.selected.position];
@ -174,34 +142,22 @@ export const ReleasePlayerCustom = (props: {
let manifest = URL.createObjectURL(file); let manifest = URL.createObjectURL(file);
let poster = `https://anixart.libria.fun${ep.preview}`; let poster = `https://anixart.libria.fun${ep.preview}`;
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
const _fetchSibnetManifest = async (url: string) => { const _fetchSibnetManifest = async (url: string) => {
const data = await _fetchPlayer( const response = await fetch(
`https://sibnet.anix-player.wah.su/?url=${url}` `https://sibnet.anix-player.wah.su/?url=${url}`
); );
if (data) { const data = await response.json();
let manifest = data.video; let manifest = data.video;
let poster = data.poster; let poster = data.poster;
return { manifest, poster }; return { manifest, poster };
}
return { manifest: null, poster: null };
}; };
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
let url = `${ENDPOINTS.release.episode}/${props.id}`; const vo = await _fetchVoiceover(props.id);
if (props.token) {
url += `?token=${props.token}`;
}
const vo = await _fetchAPI(
url,
"Не удалось получить информацию о озвучках",
{ 1: "Просмотр запрещён" }
);
if (vo) {
const selectedVO = const selectedVO =
vo.types.find((voiceover: any) => voiceover.name === preferredVO) || vo.types.find((voiceover: any) => voiceover.name === preferredVO) ||
vo.types[0]; vo.types[0];
@ -209,19 +165,13 @@ export const ReleasePlayerCustom = (props: {
selected: selectedVO, selected: selectedVO,
available: vo.types, available: vo.types,
}); });
}
}; };
__getInfo(); __getInfo();
}, []); }, []);
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}`; const src = await _fetchSource(props.id, voiceover.selected.id);
const src = await _fetchAPI(
url,
"Не удалось получить информацию о источниках"
);
if (src) {
const selectedSrc = const selectedSrc =
src.sources.find((source: any) => source.name === preferredSource) || src.sources.find((source: any) => source.name === preferredSource) ||
src.sources[0]; src.sources[0];
@ -239,7 +189,6 @@ export const ReleasePlayerCustom = (props: {
selected: selectedSrc, selected: selectedSrc,
available: src.sources, available: src.sources,
}); });
}
}; };
if (voiceover.selected) { if (voiceover.selected) {
__getInfo(); __getInfo();
@ -248,15 +197,12 @@ export const ReleasePlayerCustom = (props: {
useEffect(() => { useEffect(() => {
const __getInfo = async () => { const __getInfo = async () => {
let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`; const episodes = await _fetchEpisode(
if (props.token) { props.id,
url += `?token=${props.token}`; voiceover.selected.id,
} source.selected.id
const episodes = await _fetchAPI(
url,
"Не удалось получить информацию о эпизодах"
); );
if (episodes) {
let anonEpisodesWatched = getAnonEpisodesWatched( let anonEpisodesWatched = getAnonEpisodesWatched(
props.id, props.id,
source.selected.id, source.selected.id,
@ -279,7 +225,6 @@ export const ReleasePlayerCustom = (props: {
selected: selectedEpisode, selected: selectedEpisode,
available: episodes.episodes, available: episodes.episodes,
}); });
}
}; };
if (source.selected) { if (source.selected) {
__getInfo(); __getInfo();
@ -292,45 +237,36 @@ export const ReleasePlayerCustom = (props: {
const { manifest, poster } = await _fetchKodikManifest( const { manifest, poster } = await _fetchKodikManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "hls", type: "hls",
}); });
setIsLoading(false);
}
return; return;
} }
if (source.selected.name == "Libria") { if (source.selected.name == "Libria") {
const { manifest, poster } = await _fetchAnilibriaManifest( const { manifest, poster } = await _fetchAnilibriaManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "hls", type: "hls",
}); });
setIsLoading(false);
}
return; return;
} }
if (source.selected.name == "Sibnet") { if (source.selected.name == "Sibnet") {
const { manifest, poster } = await _fetchSibnetManifest( const { manifest, poster } = await _fetchSibnetManifest(
episode.selected.url episode.selected.url
); );
if (manifest) {
SetPlayerProps({ SetPlayerProps({
src: manifest, src: manifest,
poster: poster, poster: poster,
useCustom: true, useCustom: true,
type: "mp4", type: "mp4",
}); });
setIsLoading(false);
}
return; return;
} }
SetPlayerProps({ SetPlayerProps({
@ -339,7 +275,6 @@ export const ReleasePlayerCustom = (props: {
useCustom: false, useCustom: false,
type: null, type: null,
}); });
setIsLoading(false);
}; };
if (episode.selected) { if (episode.selected) {
__getInfo(); __getInfo();
@ -347,48 +282,32 @@ export const ReleasePlayerCustom = (props: {
}, [episode.selected]); }, [episode.selected]);
return ( return (
<Card className="aspect-video min-h-min-h-[300px] sm:min-h-[466px] md:min-h-[540px] lg:min-h-[512px] xl:min-h-[608px] 2xl:min-h-[712px]"> <Card className="h-full">
{(
!voiceover.selected ||
!source.selected ||
!episode.selected ||
!playerProps.src
) ?
<div className="flex items-center justify-center w-full aspect-video">
<Spinner />
</div>
: <div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{voiceover.selected && (
<VoiceoverSelector <VoiceoverSelector
availableVoiceover={voiceover.available} availableVoiceover={voiceover.available}
voiceover={voiceover.selected} voiceover={voiceover.selected}
setVoiceover={setVoiceover} setVoiceover={setVoiceover}
release_id={props.id} release_id={props.id}
/> />
)}
{source.selected && (
<SourceSelector <SourceSelector
availableSource={source.available} availableSource={source.available}
source={source.selected} source={source.selected}
setSource={setSource} setSource={setSource}
release_id={props.id} release_id={props.id}
/> />
)}
</div> </div>
{playerProps.useCustom ?
<div className="flex items-center justify-center w-full h-full">
{isLoading ?
!playerError ?
<Spinner />
: <div className="flex flex-col gap-2">
<p className="text-lg font-bold">Ошибка: {playerError.message}</p>
{!isErrorDetailsOpen ?
<Button
color="light"
size="xs"
onClick={() => setIsErrorDetailsOpen(true)}
>
Подробнее
</Button>
: <p className="text-gray-600 dark:text-gray-100">
{playerError.detail}
</p>
}
</div>
: playerProps.useCustom ?
!playerError ?
<MediaThemeSutro className="object-none w-full aspect-video"> <MediaThemeSutro className="object-none w-full aspect-video">
{playerProps.type == "hls" ? {playerProps.type == "hls" ?
<HlsVideo <HlsVideo
@ -415,27 +334,7 @@ export const ReleasePlayerCustom = (props: {
></VideoJS> ></VideoJS>
} }
</MediaThemeSutro> </MediaThemeSutro>
: <div className="flex flex-col gap-2">
<p className="text-lg font-bold">Ошибка: {playerError.message}</p>
{!isErrorDetailsOpen ?
<Button
color="light"
size="xs"
onClick={() => setIsErrorDetailsOpen(true)}
>
Подробнее
</Button>
: <p className="text-gray-600 dark:text-gray-100">
{playerError.detail}
</p>
}
</div>
: <iframe src={playerProps.src} className="w-full aspect-video" />} : <iframe src={playerProps.src} className="w-full aspect-video" />}
</div>
<div>
{episode.selected && source.selected && voiceover.selected && (
<EpisodeSelector <EpisodeSelector
availableEpisodes={episode.available} availableEpisodes={episode.available}
episode={episode.selected} episode={episode.selected}
@ -445,8 +344,8 @@ export const ReleasePlayerCustom = (props: {
voiceover={voiceover.selected} voiceover={voiceover.selected}
token={props.token} token={props.token}
/> />
)}
</div> </div>
}
</Card> </Card>
); );
}; };

View file

@ -17,7 +17,7 @@ export const UserSection = (props: { sectionTitle?: string; content: any }) => {
return ( return (
<Link href={`/profile/${user.id}`} key={user.id} className="w-full max-w-[234px] h-full max-h-[234px] aspect-square flex-shrink-0"> <Link href={`/profile/${user.id}`} key={user.id} className="w-full max-w-[234px] h-full max-h-[234px] aspect-square flex-shrink-0">
<Card className="items-center justify-center w-full h-full"> <Card className="items-center justify-center w-full h-full">
<Avatar img={user.avatar} alt={user.login || ""} size="lg" rounded={true} /> <Avatar img={user.avatar} alt={user.login} size="lg" rounded={true} />
<h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white"> <h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">
{user.login} {user.login}
</h5> </h5>

View file

@ -2,9 +2,11 @@
import useSWR from "swr"; import useSWR from "swr";
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel"; import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
import { Spinner } from "#/components/Spinner/Spinner"; import { Spinner } from "#/components/Spinner/Spinner";
const fetcher = (...args: any) =>
fetch([...args] as any).then((res) => res.json());
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { usePreferencesStore } from "#/store/preferences"; import { usePreferencesStore } from "#/store/preferences";
import { BookmarksList, useSWRfetcher } from "#/api/utils"; import { BookmarksList } from "#/api/utils";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
@ -33,7 +35,7 @@ export function BookmarksPage(props: { profile_id?: number }) {
function useFetchReleases(listName: string) { function useFetchReleases(listName: string) {
let url: string; let url: string;
if (preferenceStore.params.skipToCategory.enabled) { if (preferenceStore.params.skipToCategory.enabled) {
return [null, null]; return [null];
} }
if (props.profile_id) { if (props.profile_id) {
@ -48,8 +50,8 @@ export function BookmarksPage(props: { profile_id?: number }) {
} }
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const { data, error } = useSWR(url, useSWRfetcher); const { data } = useSWR(url, fetcher);
return [data, error]; return [data];
} }
useEffect(() => { useEffect(() => {
@ -59,11 +61,11 @@ export function BookmarksPage(props: { profile_id?: number }) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [authState, token]); }, [authState, token]);
const [watchingData, watchingError] = useFetchReleases("watching"); const [watchingData] = useFetchReleases("watching");
const [plannedData, plannedError] = useFetchReleases("planned"); const [plannedData] = useFetchReleases("planned");
const [watchedData, watchedError] = useFetchReleases("watched"); const [watchedData] = useFetchReleases("watched");
const [delayedData, delayedError] = useFetchReleases("delayed"); const [delayedData] = useFetchReleases("delayed");
const [abandonedData, abandonedError] = useFetchReleases("abandoned"); const [abandonedData] = useFetchReleases("abandoned");
return ( return (
<> <>
@ -83,8 +85,8 @@ export function BookmarksPage(props: { profile_id?: number }) {
<ReleaseCourusel <ReleaseCourusel
sectionTitle="Смотрю" sectionTitle="Смотрю"
showAllLink={ showAllLink={
!props.profile_id ? !props.profile_id
"/bookmarks/watching" ? "/bookmarks/watching"
: `/profile/${props.profile_id}/bookmarks/watching` : `/profile/${props.profile_id}/bookmarks/watching`
} }
content={watchingData.content} content={watchingData.content}
@ -94,9 +96,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
<ReleaseCourusel <ReleaseCourusel
sectionTitle="В планах" sectionTitle="В планах"
showAllLink={ showAllLink={
!props.profile_id ? "/bookmarks/planned" : ( !props.profile_id
`/profile/${props.profile_id}/bookmarks/planned` ? "/bookmarks/planned"
) : `/profile/${props.profile_id}/bookmarks/planned`
} }
content={plannedData.content} content={plannedData.content}
/> />
@ -105,9 +107,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
<ReleaseCourusel <ReleaseCourusel
sectionTitle="Просмотрено" sectionTitle="Просмотрено"
showAllLink={ showAllLink={
!props.profile_id ? "/bookmarks/watched" : ( !props.profile_id
`/profile/${props.profile_id}/bookmarks/watched` ? "/bookmarks/watched"
) : `/profile/${props.profile_id}/bookmarks/watched`
} }
content={watchedData.content} content={watchedData.content}
/> />
@ -116,9 +118,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
<ReleaseCourusel <ReleaseCourusel
sectionTitle="Отложено" sectionTitle="Отложено"
showAllLink={ showAllLink={
!props.profile_id ? "/bookmarks/delayed" : ( !props.profile_id
`/profile/${props.profile_id}/bookmarks/delayed` ? "/bookmarks/delayed"
) : `/profile/${props.profile_id}/bookmarks/delayed`
} }
content={delayedData.content} content={delayedData.content}
/> />
@ -129,28 +131,13 @@ export function BookmarksPage(props: { profile_id?: number }) {
<ReleaseCourusel <ReleaseCourusel
sectionTitle="Заброшено" sectionTitle="Заброшено"
showAllLink={ showAllLink={
!props.profile_id ? !props.profile_id
"/bookmarks/abandoned" ? "/bookmarks/abandoned"
: `/profile/${props.profile_id}/bookmarks/abandoned` : `/profile/${props.profile_id}/bookmarks/abandoned`
} }
content={abandonedData.content} content={abandonedData.content}
/> />
)} )}
{(watchingError ||
plannedError ||
watchedError ||
delayedError ||
abandonedError) && (
<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>
)}
</> </>
); );
} }

View file

@ -5,10 +5,10 @@ import { Spinner } from "#/components/Spinner/Spinner";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition"; import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth"; import { useUserStore } from "../store/auth";
import { Dropdown, Button } from "flowbite-react"; import { Dropdown, Button, Tabs } from "flowbite-react";
import { sort } from "./common"; import { sort } from "./common";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { BookmarksList, useSWRfetcher } from "#/api/utils"; import { BookmarksList } from "#/api/utils";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
const DropdownTheme = { const DropdownTheme = {
@ -17,10 +17,25 @@ const DropdownTheme = {
}, },
}; };
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export function BookmarksCategoryPage(props: any) { export function BookmarksCategoryPage(props: any) {
const token = useUserStore((state) => state.token); const token = useUserStore((state) => state.token);
const authState = useUserStore((state) => state.state); const authState = useUserStore((state) => state.state);
const [selectedSort, setSelectedSort] = useState(0); const [selectedSort, setSelectedSort] = useState(0);
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const [searchVal, setSearchVal] = useState(""); const [searchVal, setSearchVal] = useState("");
const router = useRouter(); const router = useRouter();
@ -46,7 +61,7 @@ export function BookmarksCategoryPage(props: any) {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -58,6 +73,7 @@ export function BookmarksCategoryPage(props: any) {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -76,31 +92,9 @@ export function BookmarksCategoryPage(props: any) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [authState, token]); }, [authState, token]);
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner />
</div>
);
};
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 (
<> <>
{!props.profile_id ? {!props.profile_id ? (
<form <form
className="flex-1 max-w-full mx-4" className="flex-1 max-w-full mx-4"
onSubmit={(e) => { onSubmit={(e) => {
@ -149,7 +143,9 @@ export function BookmarksCategoryPage(props: any) {
</button> </button>
</div> </div>
</form> </form>
: ""} ) : (
""
)}
<div className="m-4 overflow-auto"> <div className="m-4 overflow-auto">
<Button.Group> <Button.Group>
<Button <Button
@ -158,8 +154,8 @@ export function BookmarksCategoryPage(props: any) {
color="light" color="light"
onClick={() => onClick={() =>
router.push( router.push(
props.profile_id ? props.profile_id
`/profile/${props.profile_id}/bookmarks/watching` ? `/profile/${props.profile_id}/bookmarks/watching`
: "/bookmarks/watching" : "/bookmarks/watching"
) )
} }
@ -172,8 +168,8 @@ export function BookmarksCategoryPage(props: any) {
color="light" color="light"
onClick={() => onClick={() =>
router.push( router.push(
props.profile_id ? props.profile_id
`/profile/${props.profile_id}/bookmarks/planned` ? `/profile/${props.profile_id}/bookmarks/planned`
: "/bookmarks/planned" : "/bookmarks/planned"
) )
} }
@ -186,8 +182,8 @@ export function BookmarksCategoryPage(props: any) {
color="light" color="light"
onClick={() => onClick={() =>
router.push( router.push(
props.profile_id ? props.profile_id
`/profile/${props.profile_id}/bookmarks/watched` ? `/profile/${props.profile_id}/bookmarks/watched`
: "/bookmarks/watched" : "/bookmarks/watched"
) )
} }
@ -200,8 +196,8 @@ export function BookmarksCategoryPage(props: any) {
color="light" color="light"
onClick={() => onClick={() =>
router.push( router.push(
props.profile_id ? props.profile_id
`/profile/${props.profile_id}/bookmarks/delayed` ? `/profile/${props.profile_id}/bookmarks/delayed`
: "/bookmarks/delayed" : "/bookmarks/delayed"
) )
} }
@ -214,8 +210,8 @@ export function BookmarksCategoryPage(props: any) {
color="light" color="light"
onClick={() => onClick={() =>
router.push( router.push(
props.profile_id ? props.profile_id
`/profile/${props.profile_id}/bookmarks/abandoned` ? `/profile/${props.profile_id}/bookmarks/abandoned`
: "/bookmarks/abandoned" : "/bookmarks/abandoned"
) )
} }
@ -240,8 +236,8 @@ export function BookmarksCategoryPage(props: any) {
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}> <Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
<span <span
className={`w-6 h-6 iconify ${ className={`w-6 h-6 iconify ${
sort.values[index].value.split("_")[1] == "descending" ? sort.values[index].value.split("_")[1] == "descending"
sort.descendingIcon ? sort.descendingIcon
: sort.ascendingIcon : sort.ascendingIcon
}`} }`}
></span> ></span>
@ -250,15 +246,20 @@ export function BookmarksCategoryPage(props: any) {
))} ))}
</Dropdown> </Dropdown>
</div> </div>
{content && content.length > 0 ? {content && content.length > 0 ? (
<ReleaseSection content={content} /> <ReleaseSection content={content} />
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl"> ) : !isLoadingEnd || isLoading ? (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner />
</div>
) : (
<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--broken-heart"></span> <span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
<p> <p>
В списке {props.SectionTitleMapping[props.slug]} пока ничего нет... В списке {props.SectionTitleMapping[props.slug]} пока ничего нет...
</p> </p>
</div> </div>
} )}
{data && {data &&
data[data.length - 1].current_page < data[data.length - 1].current_page <
data[data.length - 1].total_page_count && ( data[data.length - 1].total_page_count && (

View file

@ -2,7 +2,8 @@
import useSWR from "swr"; import useSWR from "swr";
import { CollectionCourusel } from "#/components/CollectionCourusel/CollectionCourusel"; import { CollectionCourusel } from "#/components/CollectionCourusel/CollectionCourusel";
import { Spinner } from "#/components/Spinner/Spinner"; import { Spinner } from "#/components/Spinner/Spinner";
import { useSWRfetcher } from "#/api/utils"; const fetcher = (...args: any) =>
fetch([...args] as any).then((res) => res.json());
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -24,15 +25,12 @@ export function CollectionsPage() {
} }
} }
const { data, error } = useSWR(url, useSWRfetcher); const { data } = useSWR(url, fetcher);
return [data, error]; return [data];
} }
const [userCollections, userCollectionsError] = const [userCollections] = useFetchReleases("userCollections");
useFetchReleases("userCollections"); const [favoriteCollections] = useFetchReleases("userFavoriteCollections");
const [favoriteCollections, favoriteCollectionsError] = useFetchReleases(
"userFavoriteCollections"
);
useEffect(() => { useEffect(() => {
if (userStore.state === "finished" && !userStore.token) { if (userStore.state === "finished" && !userStore.token) {
@ -116,18 +114,6 @@ export function CollectionsPage() {
content={favoriteCollections.content} content={favoriteCollections.content}
/> />
)} )}
{(userCollectionsError || favoriteCollectionsError) && (
<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>
)}
</> </>
); );
} }

View file

@ -8,7 +8,20 @@ import { useUserStore } from "../store/auth";
import { Button } from "flowbite-react"; import { Button } from "flowbite-react";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSWRfetcher } from "#/api/utils";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export function CollectionsFullPage(props: { export function CollectionsFullPage(props: {
type: "favorites" | "profile" | "release"; type: "favorites" | "profile" | "release";
@ -17,12 +30,13 @@ export function CollectionsFullPage(props: {
release_id?: number; release_id?: number;
}) { }) {
const userStore = useUserStore(); const userStore = useUserStore();
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const router = useRouter(); const router = useRouter();
const getKey = (pageIndex: number, previousPageData: any) => { const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.content.length) return null; if (previousPageData && !previousPageData.content.length) return null;
let url: string; let url;
if (props.type == "favorites") { if (props.type == "favorites") {
url = `${ENDPOINTS.collection.favoriteCollections}/all/${pageIndex}`; url = `${ENDPOINTS.collection.favoriteCollections}/all/${pageIndex}`;
@ -41,7 +55,7 @@ export function CollectionsFullPage(props: {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -53,6 +67,7 @@ export function CollectionsFullPage(props: {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -75,45 +90,26 @@ export function CollectionsFullPage(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userStore.state, userStore.token]); }, [userStore.state, userStore.token]);
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner />
</div>
);
};
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 (
<> <>
{content && content.length > 0 ? {content && content.length > 0 ? (
<CollectionsSection <CollectionsSection
sectionTitle={props.title} sectionTitle={props.title}
content={content} content={content}
isMyCollections={ isMyCollections={
props.type == "profile" && props.type == "profile" && userStore.user && props.profile_id == userStore.user.id
userStore.user &&
props.profile_id == userStore.user.id
} }
/> />
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl"> ) : !isLoadingEnd || isLoading ? (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner />
</div>
) : (
<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--broken-heart"></span> <span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
<p>Тут пока ничего нет...</p> <p>Тут пока ничего нет...</p>
</div> </div>
} )}
{data && {data &&
data[data.length - 1].current_page < data[data.length - 1].current_page <
data[data.length - 1].total_page_count && ( data[data.length - 1].total_page_count && (

View file

@ -13,30 +13,34 @@ import {
FileInput, FileInput,
Label, Label,
Modal, Modal,
useThemeMode,
} from "flowbite-react"; } from "flowbite-react";
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink"; import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
import { CropModal } from "#/components/CropModal/CropModal"; import { CropModal } from "#/components/CropModal/CropModal";
import { b64toBlob, tryCatchAPI } from "#/api/utils"; import { b64toBlob } from "#/api/utils";
import { useSWRfetcher } from "#/api/utils"; const fetcher = async (url: string) => {
import { Spinner } from "#/components/Spinner/Spinner"; const res = await fetch(url);
import { toast } from "react-toastify";
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export const CreateCollectionPage = () => { export const CreateCollectionPage = () => {
const userStore = useUserStore(); const userStore = useUserStore();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const theme = useThemeMode();
useEffect(() => {
if (userStore.state === "finished" && !userStore.token) {
router.push("/login?redirect=/collections/create");
}
}, [userStore]);
const [edit, setEdit] = useState(false); const [edit, setEdit] = useState(false);
const [imageUrl, setImageUrl] = useState<string>(null);
const [tempImageUrl, setTempImageUrl] = useState<string>(null);
const [isPrivate, setIsPrivate] = useState(false); const [isPrivate, setIsPrivate] = useState(false);
const [collectionInfo, setCollectionInfo] = useState({ const [collectionInfo, setCollectionInfo] = useState({
title: "", title: "",
@ -49,14 +53,7 @@ export const CreateCollectionPage = () => {
const [addedReleases, setAddedReleases] = useState([]); const [addedReleases, setAddedReleases] = useState([]);
const [addedReleasesIds, setAddedReleasesIds] = useState([]); const [addedReleasesIds, setAddedReleasesIds] = useState([]);
const [releasesEditModalOpen, setReleasesEditModalOpen] = useState(false); const [releasesEditModalOpen, setReleasesEditModalOpen] = useState(false);
const [cropModalOpen, setCropModalOpen] = useState(false);
const [imageModalProps, setImageModalProps] = useState({
isOpen: false,
isActionsDisabled: false,
selectedImage: null,
croppedImage: null,
});
const [imageUrl, setImageUrl] = useState<string>(null);
const collection_id = searchParams.get("id") || null; const collection_id = searchParams.get("id") || null;
const mode = searchParams.get("mode") || null; const mode = searchParams.get("mode") || null;
@ -123,29 +120,15 @@ export const CreateCollectionPage = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userStore.user]); }, [userStore.user]);
useEffect(() => { const handleFileRead = (e, fileReader) => {
if (imageModalProps.croppedImage) {
setImageUrl(imageModalProps.croppedImage);
setImageModalProps({
isOpen: false,
isActionsDisabled: false,
selectedImage: null,
croppedImage: null,
});
}
}, [imageModalProps.croppedImage]);
const handleImagePreview = (e: any) => {
const file = e.target.files[0];
const fileReader = new FileReader();
fileReader.onloadend = () => {
const content = fileReader.result; const content = fileReader.result;
setImageModalProps({ setTempImageUrl(content);
...imageModalProps, };
isOpen: true,
selectedImage: content, const handleFilePreview = (file) => {
}); const fileReader = new FileReader();
e.target.value = ""; fileReader.onloadend = (e) => {
handleFileRead(e, fileReader);
}; };
fileReader.readAsDataURL(file); fileReader.readAsDataURL(file);
}; };
@ -166,25 +149,12 @@ export const CreateCollectionPage = () => {
e.preventDefault(); e.preventDefault();
async function _createCollection() { async function _createCollection() {
setIsSending(true);
const tid = toast.loading(
mode === "edit" ? "Редактируем коллекцию..." : "Создаём коллекцию...",
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
const url = const url =
mode === "edit" ? mode === "edit"
`${ENDPOINTS.collection.edit}/${collection_id}?token=${userStore.token}` ? `${ENDPOINTS.collection.edit}/${collection_id}?token=${userStore.token}`
: `${ENDPOINTS.collection.create}?token=${userStore.token}`; : `${ENDPOINTS.collection.create}?token=${userStore.token}`;
const { data, error } = await tryCatchAPI( const res = await fetch(url, {
fetch(url, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
...collectionInfo, ...collectionInfo,
@ -192,24 +162,12 @@ export const CreateCollectionPage = () => {
private: isPrivate, private: isPrivate,
releases: addedReleasesIds, releases: addedReleasesIds,
}), }),
})
);
if (error) {
let message = `${error.message}, code: ${error.code}`;
if (error.code == 5) {
message =
"Вы превысили допустимый еженедельный лимит создания коллекций";
}
toast.update(tid, {
render: message,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
}); });
setIsSending(false);
const data = await res.json();
if (data.code == 5) {
alert("Вы превысили допустимый еженедельный лимит создания коллекций!");
return; return;
} }
@ -222,92 +180,33 @@ export const CreateCollectionPage = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("image", blob, "cropped.jpg"); formData.append("image", blob, "cropped.jpg");
formData.append("name", "image"); formData.append("name", "image");
const uploadRes = await fetch(
const tiid = toast.loading(
`Обновление обложки коллекции ${collectionInfo.title}...`,
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
const { data: imageData, error } = await tryCatchAPI(
fetch(
`${ENDPOINTS.collection.editImage}/${data.collection.id}?token=${userStore.token}`, `${ENDPOINTS.collection.editImage}/${data.collection.id}?token=${userStore.token}`,
{ {
method: "POST", method: "POST",
body: formData, body: formData,
} }
)
); );
const uploadData = await uploadRes.json();
if (error) {
toast.update(tiid, {
render: "Не удалось обновить постер коллекции",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
} else {
toast.update(tiid, {
render: "Постер коллекции обновлён",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
}
} }
toast.update(tid, {
render:
mode === "edit" ?
`Коллекция ${collectionInfo.title} обновлена`
: `Коллекция ${collectionInfo.title} создана`,
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
router.push(`/collection/${data.collection.id}`); router.push(`/collection/${data.collection.id}`);
setIsSending(false);
}
if (collectionInfo.title.length < 10) {
toast.error("Необходимо ввести название коллекции не менее 10 символов", {
position: "bottom-center",
hideProgressBar: true,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return;
}
if (addedReleasesIds.length < 1) {
toast.error("Необходимо добавить хотя бы один релиз в коллекцию", {
position: "bottom-center",
hideProgressBar: true,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return;
} }
if (
collectionInfo.title.length >= 10 &&
addedReleasesIds.length >= 1 &&
userStore.token
) {
// setIsSending(true);
_createCollection(); _createCollection();
} else if (collectionInfo.title.length < 10) {
alert("Необходимо ввести название коллекции не менее 10 символов");
} else if (!userStore.token) {
alert("Для создания коллекции необходимо войти в аккаунт");
} else if (addedReleasesIds.length < 1) {
alert("Необходимо добавить хотя бы один релиз в коллекцию");
}
} }
function _deleteRelease(release: any) { function _deleteRelease(release: any) {
@ -340,8 +239,7 @@ export const CreateCollectionPage = () => {
className="flex flex-col items-center w-full sm:max-w-[600px] h-[337px] border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-600" className="flex flex-col items-center w-full sm:max-w-[600px] h-[337px] border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-600"
> >
<div className="flex flex-col items-center justify-center max-w-[595px] h-[inherit] rounded-[inherit] pt-5 pb-6 overflow-hidden"> <div className="flex flex-col items-center justify-center max-w-[595px] h-[inherit] rounded-[inherit] pt-5 pb-6 overflow-hidden">
{ {!imageUrl ? (
!imageUrl ?
<> <>
<svg <svg
className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
@ -359,30 +257,29 @@ export const CreateCollectionPage = () => {
/> />
</svg> </svg>
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400"> <p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold"> <span className="font-semibold">Нажмите для загрузки</span>{" "}
Нажмите для загрузки
</span>{" "}
или перетащите файл или перетащите файл
</p> </p>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
PNG или JPG (Макс. 600x337 пикселей) PNG или JPG (Макс. 600x337 пикселей)
</p> </p>
</> </>
) : (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
: <img <img
src={imageUrl} src={imageUrl}
className="object-cover w-[inherit] h-[inherit]" className="object-cover w-[inherit] h-[inherit]"
alt="" alt=""
/> />
)}
}
</div> </div>
<FileInput <FileInput
id="dropzone-file" id="dropzone-file"
className="hidden" className="hidden"
accept="image/jpg, image/jpeg, image/png" accept="image/jpg, image/jpeg, image/png"
onChange={(e) => { onChange={(e) => {
handleImagePreview(e); handleFilePreview(e.target.files[0]);
setCropModalOpen(true);
}} }}
/> />
</Label> </Label>
@ -492,15 +389,18 @@ export const CreateCollectionPage = () => {
setReleasesIds={setAddedReleasesIds} setReleasesIds={setAddedReleasesIds}
/> />
<CropModal <CropModal
{...imageModalProps} src={tempImageUrl}
cropParams={{ setSrc={setImageUrl}
aspectRatio: 600 / 337, setTempSrc={setTempImageUrl}
forceAspect: true, // setImageData={setImageData}
guides: true, aspectRatio={600 / 337}
width: 600, guides={false}
height: 337, quality={100}
}} isOpen={cropModalOpen}
setCropModalProps={setImageModalProps} setIsOpen={setCropModalOpen}
forceAspect={true}
width={600}
height={337}
/> />
</> </>
); );
@ -528,7 +428,7 @@ export const ReleasesEditModal = (props: {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2, revalidateFirstPage: false } { initialSize: 2, revalidateFirstPage: false }
); );
@ -564,31 +464,12 @@ export const ReleasesEditModal = (props: {
function _addRelease(release: any) { function _addRelease(release: any) {
if (props.releasesIds.length == 100) { if (props.releasesIds.length == 100) {
toast.error( alert("Достигнуто максимальное количество релизов в коллекции - 100");
"Достигнуто максимальное количество релизов в коллекции - 100",
{
position: "bottom-center",
hideProgressBar: true,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
}
);
return; return;
} }
if (props.releasesIds.includes(release.id)) { if (props.releasesIds.includes(release.id)) {
toast.error("Релиз уже добавлен в коллекцию", { alert("Релиз уже добавлен в коллекцию");
position: "bottom-center",
hideProgressBar: true,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return; return;
} }
@ -613,7 +494,7 @@ export const ReleasesEditModal = (props: {
className="max-w-full mx-auto" className="max-w-full mx-auto"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
setContent([]); props.setReleases([]);
setQuery(e.target[0].value.trim()); setQuery(e.target[0].value.trim());
}} }}
> >
@ -658,12 +539,12 @@ export const ReleasesEditModal = (props: {
</div> </div>
</form> </form>
<div className="grid grid-cols-2 gap-2 mt-2 md:grid-cols-4"> <div className="flex flex-wrap gap-1 mt-2">
{content.map((release) => { {content.map((release) => {
return ( return (
<button <button
key={release.id} key={release.id}
className="overflow-hidden" className=""
onClick={() => _addRelease(release)} onClick={() => _addRelease(release)}
> >
<ReleaseLink type="poster" {...release} isLinkDisabled={true} /> <ReleaseLink type="poster" {...release} isLinkDisabled={true} />
@ -672,12 +553,6 @@ export const ReleasesEditModal = (props: {
})} })}
{content.length == 1 && <div></div>} {content.length == 1 && <div></div>}
</div> </div>
{isLoading && (
<div className="flex items-center justify-center h-full min-h-24">
<Spinner />
</div>
)}
{error && <div>Произошла ошибка</div>}
</div> </div>
</Modal> </Modal>
); );

View file

@ -9,7 +9,6 @@ import { Dropdown, Button } from "flowbite-react";
import { sort } from "./common"; import { sort } from "./common";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSWRfetcher } from "#/api/utils";
const DropdownTheme = { const DropdownTheme = {
floating: { floating: {
@ -17,10 +16,25 @@ const DropdownTheme = {
}, },
}; };
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export function FavoritesPage() { export function FavoritesPage() {
const token = useUserStore((state) => state.token); const token = useUserStore((state) => state.token);
const authState = useUserStore((state) => state.state); const authState = useUserStore((state) => state.state);
const [selectedSort, setSelectedSort] = useState(0); const [selectedSort, setSelectedSort] = useState(0);
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const router = useRouter(); const router = useRouter();
const [searchVal, setSearchVal] = useState(""); const [searchVal, setSearchVal] = useState("");
@ -33,7 +47,7 @@ export function FavoritesPage() {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -45,6 +59,7 @@ export function FavoritesPage() {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -141,7 +156,7 @@ export function FavoritesPage() {
</div> </div>
{content && content.length > 0 ? ( {content && content.length > 0 ? (
<ReleaseSection content={content} /> <ReleaseSection content={content} />
) : isLoading ? ( ) : !isLoadingEnd || isLoading ? (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen"> <div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner /> <Spinner />
</div> </div>

View file

@ -8,12 +8,25 @@ import { useUserStore } from "../store/auth";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { Button } from "flowbite-react"; import { Button } from "flowbite-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSWRfetcher } from "#/api/utils";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export function HistoryPage() { export function HistoryPage() {
const token = useUserStore((state) => state.token); const token = useUserStore((state) => state.token);
const authState = useUserStore((state) => state.state); const authState = useUserStore((state) => state.state);
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const router = useRouter(); const router = useRouter();
const [searchVal, setSearchVal] = useState(""); const [searchVal, setSearchVal] = useState("");
@ -26,7 +39,7 @@ export function HistoryPage() {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -38,6 +51,7 @@ export function HistoryPage() {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -122,7 +136,7 @@ export function HistoryPage() {
</Button> </Button>
)} )}
</> </>
) : isLoading ? ( ) : !isLoadingEnd || isLoading ? (
<div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]"> <div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]">
<Spinner /> <Spinner />
</div> </div>

View file

@ -1,11 +1,8 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { setJWT, tryCatchAPI } from "#/api/utils"; import { setJWT } from "#/api/utils";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useThemeMode } from "flowbite-react";
import { toast } from "react-toastify";
import { ENDPOINTS } from "#/api/config";
export function LoginPage() { export function LoginPage() {
const [login, setLogin] = useState(""); const [login, setLogin] = useState("");
@ -15,77 +12,36 @@ export function LoginPage() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get("redirect") || null; const redirect = searchParams.get("redirect") || null;
const theme = useThemeMode();
const [isSending, setIsSending] = useState(false);
async function submit(e) { function submit(e) {
e.preventDefault(); e.preventDefault();
setIsSending(true); fetch("/api/profile/login", {
const tid = toast.loading("Выполняем вход...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
const { data, error } = await tryCatchAPI(
fetch(`${ENDPOINTS.user.auth}?login=${login}&password=${password}`, {
method: "POST", method: "POST",
headers: { headers: {
Sign: "9aa5c7af74e8cd70c86f7f9587bde23d", "Content-Type": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
}, },
body: JSON.stringify({
login: login,
password: password,
}),
}) })
); .then((response) => {
if (response.ok) {
if (error) { return response.json();
let message = `Ошибка получения пользователя, code: ${error.code}` } else {
if (error.code == 2) { alert("Ошибка получения пользователя.");
message = "Такого пользователя не существует"
} }
if (error.code == 3) { })
message = "Неправильно указан логин и/или пароль" .then((data) => {
} if (data.profileToken) {
toast.update(tid, {
render: message,
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsSending(false);
return;
}
if (!data.profileToken) {
toast.update(tid, {
render: "Не удалось войти в аккаунт",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setIsSending(false);
return;
}
userStore.login(data.profile, data.profileToken.token); userStore.login(data.profile, data.profileToken.token);
if (remember) { if (remember) {
setJWT(data.profile.id, data.profileToken.token); setJWT(data.profile.id, data.profileToken.token);
} }
router.push("/");
toast.update(tid, { } else {
render: "Вход успешен!", alert("Неверные данные.");
type: "success", }
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
}); });
} }
@ -97,7 +53,7 @@ export function LoginPage() {
}, [userStore.user]); }, [userStore.user]);
return ( return (
<section> <section className="bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0"> <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8">

View file

@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import { Spinner } from "../components/Spinner/Spinner"; import { Spinner } from "../components/Spinner/Spinner";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import useSWR from "swr"; import useSWR from "swr";
import { useSWRfetcher } from "#/api/utils";
import { ProfileUser } from "#/components/Profile/Profile.User"; import { ProfileUser } from "#/components/Profile/Profile.User";
import { ProfileBannedBanner } from "#/components/Profile/ProfileBannedBanner"; import { ProfileBannedBanner } from "#/components/Profile/ProfileBannedBanner";
@ -17,6 +16,20 @@ import { ProfileReleaseRatings } from "#/components/Profile/Profile.ReleaseRatin
import { ProfileReleaseHistory } from "#/components/Profile/Profile.ReleaseHistory"; import { ProfileReleaseHistory } from "#/components/Profile/Profile.ReleaseHistory";
import { ProfileEditModal } from "#/components/Profile/Profile.EditModal"; import { ProfileEditModal } from "#/components/Profile/Profile.EditModal";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export const ProfilePage = (props: any) => { export const ProfilePage = (props: any) => {
const authUser = useUserStore(); const authUser = useUserStore();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
@ -28,7 +41,7 @@ export const ProfilePage = (props: any) => {
if (authUser.token) { if (authUser.token) {
url += `?token=${authUser.token}`; url += `?token=${authUser.token}`;
} }
const { data, error } = useSWR(url, useSWRfetcher); const { data } = useSWR(url, fetcher);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@ -37,7 +50,7 @@ export const ProfilePage = (props: any) => {
} }
}, [data]); }, [data]);
if (!user && !error) { if (!user) {
return ( return (
<main className="flex items-center justify-center min-h-screen"> <main className="flex items-center justify-center min-h-screen">
<Spinner /> <Spinner />
@ -45,20 +58,6 @@ export const ProfilePage = (props: any) => {
); );
} }
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>
);
}
const hasSocials = const hasSocials =
user.vk_page != "" || user.vk_page != "" ||
user.tg_page != "" || user.tg_page != "" ||
@ -158,11 +157,7 @@ export const ProfilePage = (props: any) => {
{!user.is_stats_hidden && ( {!user.is_stats_hidden && (
<div className="flex-col hidden gap-2 xl:flex"> <div className="flex-col hidden gap-2 xl:flex">
{user.votes && user.votes.length > 0 && ( {user.votes && user.votes.length > 0 && (
<ProfileReleaseRatings <ProfileReleaseRatings ratings={user.votes} token={authUser.token} profile_id={user.id} />
ratings={user.votes}
token={authUser.token}
profile_id={user.id}
/>
)} )}
{user.history && user.history.length > 0 && ( {user.history && user.history.length > 0 && (
<ProfileReleaseHistory history={user.history} /> <ProfileReleaseHistory history={user.history} />
@ -202,11 +197,7 @@ export const ProfilePage = (props: any) => {
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} /> <ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
<div className="flex flex-col gap-2 xl:hidden"> <div className="flex flex-col gap-2 xl:hidden">
{user.votes && user.votes.length > 0 && ( {user.votes && user.votes.length > 0 && (
<ProfileReleaseRatings <ProfileReleaseRatings ratings={user.votes} token={authUser.token} profile_id={user.id} />
ratings={user.votes}
token={authUser.token}
profile_id={user.id}
/>
)} )}
{user.history && user.history.length > 0 && ( {user.history && user.history.length > 0 && (
<ProfileReleaseHistory history={user.history} /> <ProfileReleaseHistory history={user.history} />
@ -216,12 +207,7 @@ export const ProfilePage = (props: any) => {
)} )}
</div> </div>
</div> </div>
<ProfileEditModal <ProfileEditModal isOpen={isOpen && isMyProfile} setIsOpen={setIsOpen} token={authUser.token} profile_id={user.id}/>
isOpen={isOpen && isMyProfile}
setIsOpen={setIsOpen}
token={authUser.token}
profile_id={user.id}
/>
</> </>
); );
}; };

View file

@ -6,11 +6,22 @@ import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth"; import { useUserStore } from "../store/auth";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { ReleaseLink169Related } from "#/components/ReleaseLink/ReleaseLink.16_9Related"; import { ReleaseLink169Related } from "#/components/ReleaseLink/ReleaseLink.16_9Related";
import { useSWRfetcher } from "#/api/utils";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(`An error occurred while fetching the data. status: ${res.status}`);
error.message = await res.json();
throw error;
}
return res.json();
};
export function RelatedPage(props: {id: number|string, title: string}) { export function RelatedPage(props: {id: number|string, title: string}) {
const token = useUserStore((state) => state.token); const token = useUserStore((state) => state.token);
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const getKey = (pageIndex: number, previousPageData: any) => { const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.content.length) return null; if (previousPageData && !previousPageData.content.length) return null;
@ -22,7 +33,7 @@ export function RelatedPage(props: {id: number|string, title: string}) {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 1 } { initialSize: 1 }
); );
@ -34,6 +45,7 @@ export function RelatedPage(props: {id: number|string, title: string}) {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -58,7 +70,7 @@ export function RelatedPage(props: {id: number|string, title: string}) {
return <ReleaseLink169Related {...release} key={release.id} _position={index + 1} /> return <ReleaseLink169Related {...release} key={release.id} _position={index + 1} />
})} })}
</div> </div>
) : isLoading ? ( ) : !isLoadingEnd || isLoading ? (
<div className="flex flex-col items-center justify-center min-w-full min-h-screen"> <div className="flex flex-col items-center justify-center min-w-full min-h-screen">
<Spinner /> <Spinner />
</div> </div>

View file

@ -2,7 +2,8 @@
import useSWR from "swr"; import useSWR from "swr";
import { Spinner } from "#/components/Spinner/Spinner"; import { Spinner } from "#/components/Spinner/Spinner";
import { useSWRfetcher } from "#/api/utils"; const fetcher = (...args: any) =>
fetch([...args] as any).then((res) => res.json());
import { useUserStore } from "#/store/auth"; import { useUserStore } from "#/store/auth";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -32,7 +33,7 @@ export const ReleasePage = (props: any) => {
if (userStore.token) { if (userStore.token) {
url += `?token=${userStore.token}`; url += `?token=${userStore.token}`;
} }
const { data, isLoading, error } = useSWR(url, useSWRfetcher); const { data, isLoading, error } = useSWR(url, fetcher);
return [data, isLoading, error]; return [data, isLoading, error];
} }
const [data, isLoading, error] = useFetch(props.id); const [data, isLoading, error] = useFetch(props.id);
@ -48,31 +49,10 @@ export const ReleasePage = (props: any) => {
} }
}, [data]); }, [data]);
if (isLoading) { return data ? (
return ( <>
<main className="flex items-center justify-center min-h-screen"> <div className="flex flex-col lg:grid lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
<Spinner /> <div className="[grid-column:1] [grid-row:span_2]">
</main>
);
}
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 (
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
<ReleaseInfoBasics <ReleaseInfoBasics
image={data.release.image} image={data.release.image}
title={{ title={{
@ -83,6 +63,8 @@ export const ReleasePage = (props: any) => {
note={data.release.note} note={data.release.note}
release_id={data.release.id} release_id={data.release.id}
/> />
</div>
<div className="[grid-column:2]">
<ReleaseInfoInfo <ReleaseInfoInfo
country={data.release.country} country={data.release.country}
aired_on_date={data.release.aired_on_date} aired_on_date={data.release.aired_on_date}
@ -101,6 +83,8 @@ export const ReleasePage = (props: any) => {
director={data.release.director} director={data.release.director}
genres={data.release.genres} genres={data.release.genres}
/> />
</div>
<div className="[grid-column:2]">
<ReleaseInfoUserList <ReleaseInfoUserList
userList={userList} userList={userList}
isFavorite={userFavorite} isFavorite={userFavorite}
@ -112,23 +96,16 @@ export const ReleasePage = (props: any) => {
collection_count={data.release.collection_count} collection_count={data.release.collection_count}
/> />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
<div className="flex flex-col gap-2">
{data.release.status && {data.release.status &&
data.release.status.name.toLowerCase() != "анонс" && ( data.release.status.name.toLowerCase() != "анонс" && (
<> <div className="[grid-column:1] [grid-row:span_12]">
{preferenceStore.params.experimental.newPlayer ? {preferenceStore.params.experimental.newPlayer ? (
<ReleasePlayerCustom id={props.id} token={userStore.token} /> <ReleasePlayerCustom id={props.id} token={userStore.token} />
: <ReleasePlayer id={props.id} />} ) : (
</> <ReleasePlayer id={props.id} />
)} )}
<CommentsMain
release_id={props.id}
token={userStore.token}
comments={data.release.comments}
/>
</div> </div>
<div className="flex flex-col gap-2"> )}
{data.release.status && {data.release.status &&
data.release.status.name.toLowerCase() != "анонс" && ( data.release.status.name.toLowerCase() != "анонс" && (
<div className="[grid-column:2]"> <div className="[grid-column:2]">
@ -148,6 +125,7 @@ export const ReleasePage = (props: any) => {
/> />
</div> </div>
)} )}
<div className="[grid-column:2] [grid-row:span_4]">
<InfoLists <InfoLists
completed={data.release.completed_count} completed={data.release.completed_count}
planned={data.release.plan_count} planned={data.release.plan_count}
@ -155,18 +133,36 @@ export const ReleasePage = (props: any) => {
delayed={data.release.hold_on_count} delayed={data.release.hold_on_count}
watching={data.release.watching_count} watching={data.release.watching_count}
/> />
</div>
{data.release.screenshot_images.length > 0 && ( {data.release.screenshot_images.length > 0 && (
<div className="[grid-column:2] [grid-row:span_11]">
<ReleaseInfoScreenshots images={data.release.screenshot_images} /> <ReleaseInfoScreenshots images={data.release.screenshot_images} />
</div>
)} )}
{data.release.related_releases.length > 0 && ( {data.release.related_releases.length > 0 && (
<div className="[grid-column:2] [grid-row:span_2]">
<ReleaseInfoRelated <ReleaseInfoRelated
release_id={props.id} release_id={props.id}
related={data.release.related} related={data.release.related}
related_releases={data.release.related_releases} related_releases={data.release.related_releases}
/> />
</div>
)} )}
<div className="[grid-column:1] [grid-row:span_32]">
<CommentsMain
release_id={props.id}
token={userStore.token}
comments={data.release.comments}
/>
</div> </div>
</div> </div>
</>
) : (
<div className="flex h-[100dvh] w-full justify-center items-center">
<Spinner />
</div> </div>
); );
}; };

View file

@ -11,7 +11,20 @@ import { useUserStore } from "../store/auth";
import { Button, Dropdown, Modal } from "flowbite-react"; import { Button, Dropdown, Modal } from "flowbite-react";
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection"; import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
import { UserSection } from "#/components/UserSection/UserSection"; import { UserSection } from "#/components/UserSection/UserSection";
import { useSWRfetcher } from "#/api/utils";
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
const ListsMapping = { const ListsMapping = {
watching: { watching: {
@ -115,7 +128,7 @@ export function SearchPage() {
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2, revalidateFirstPage: false } { initialSize: 2, revalidateFirstPage: false }
); );
@ -161,18 +174,7 @@ export function SearchPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchVal]); }, [searchVal]);
if (error) if (error) return <div>failed to load: {error.message}</div>;
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 (
<> <>
@ -235,35 +237,39 @@ export function SearchPage() {
</div> </div>
<div className="mt-2"> <div className="mt-2">
{data && data[0].related && <RelatedSection {...data[0].related} />} {data && data[0].related && <RelatedSection {...data[0].related} />}
{content ? {content ? (
content.length > 0 ? content.length > 0 ? (
<> <>
{where == "collections" ? {where == "collections" ? (
<CollectionsSection <CollectionsSection
sectionTitle="Найденные Коллекции" sectionTitle="Найденные Коллекции"
content={content} content={content}
/> />
: where == "profiles" ? ) : where == "profiles" ? (
<UserSection <UserSection
sectionTitle="Найденные Пользователи" sectionTitle="Найденные Пользователи"
content={content} content={content}
/> />
: <ReleaseSection ) : (
<ReleaseSection
sectionTitle="Найденные релизы" sectionTitle="Найденные релизы"
content={content} 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 && ( )
) : (
isLoading && (
<div className="flex items-center justify-center min-w-full min-h-screen"> <div className="flex items-center justify-center min-w-full min-h-screen">
<Spinner /> <Spinner />
</div> </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>
@ -271,13 +277,11 @@ export function SearchPage() {
</div> </div>
)} )}
</div> </div>
{( {data &&
data &&
data.length > 1 && data.length > 1 &&
(where == "releases" ? (where == "releases"
data[data.length - 1].releases.length == 25 ? data[data.length - 1].releases.length == 25
: data[data.length - 1].content.length == 25) : data[data.length - 1].content.length == 25) ? (
) ?
<Button <Button
className="w-full" className="w-full"
color={"light"} color={"light"}
@ -288,7 +292,9 @@ export function SearchPage() {
<span className="text-lg">Загрузить ещё</span> <span className="text-lg">Загрузить ещё</span>
</div> </div>
</Button> </Button>
: ""} ) : (
""
)}
<FiltersModal <FiltersModal
isOpen={filtersModalOpen} isOpen={filtersModalOpen}
setIsOpen={setFiltersModalOpen} setIsOpen={setFiltersModalOpen}
@ -388,7 +394,9 @@ const FiltersModal = (props: {
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
{props.isAuth && where == "list" && ListsMapping.hasOwnProperty(list) ? {props.isAuth &&
where == "list" &&
ListsMapping.hasOwnProperty(list) ? (
<div className="my-4"> <div className="my-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Список</p> <p className="font-bold dark:text-white">Список</p>
@ -406,8 +414,10 @@ const FiltersModal = (props: {
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
: ""} ) : (
{!["profiles", "collections"].includes(where) ? ""
)}
{!["profiles", "collections"].includes(where) ? (
<div className="my-4"> <div className="my-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Искать по</p> <p className="font-bold dark:text-white">Искать по</p>
@ -425,7 +435,9 @@ const FiltersModal = (props: {
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
: ""} ) : (
""
)}
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<div className="flex justify-end w-full gap-2"> <div className="flex justify-end w-full gap-2">

View file

@ -6,6 +6,7 @@ import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition"; import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth"; import { useUserStore } from "../store/auth";
import { ENDPOINTS } from "#/api/config"; import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation";
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection"; import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
import { CollectionInfoBasics } from "#/components/CollectionInfo/CollectionInfo.Basics"; import { CollectionInfoBasics } from "#/components/CollectionInfo/CollectionInfo.Basics";
@ -13,10 +14,24 @@ import { InfoLists } from "#/components/InfoLists/InfoLists";
import { CollectionInfoControls } from "#/components/CollectionInfo/CollectionInfoControls"; import { CollectionInfoControls } from "#/components/CollectionInfo/CollectionInfoControls";
import { CommentsMain } from "#/components/Comments/Comments.Main"; import { CommentsMain } from "#/components/Comments/Comments.Main";
import { useSWRfetcher } from "#/api/utils"; const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(
`An error occurred while fetching the data. status: ${res.status}`
);
error.message = await res.json();
throw error;
}
return res.json();
};
export const ViewCollectionPage = (props: { id: number }) => { export const ViewCollectionPage = (props: { id: number }) => {
const userStore = useUserStore(); const userStore = useUserStore();
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
const router = useRouter();
function useFetchCollectionInfo(type: "info" | "comments") { function useFetchCollectionInfo(type: "info" | "comments") {
let url: string; let url: string;
@ -31,8 +46,8 @@ export const ViewCollectionPage = (props: { id: number }) => {
url += `${type != "info" ? "&" : "?"}token=${userStore.token}`; url += `${type != "info" ? "&" : "?"}token=${userStore.token}`;
} }
const { data, error, isLoading } = useSWR(url, useSWRfetcher); const { data, isLoading } = useSWR(url, fetcher);
return [data, error, isLoading]; return [data, isLoading];
} }
const getKey = (pageIndex: number, previousPageData: any) => { const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.content.length) return null; if (previousPageData && !previousPageData.content.length) return null;
@ -43,17 +58,14 @@ export const ViewCollectionPage = (props: { id: number }) => {
return url; return url;
}; };
const [collectionInfo, collectionInfoError, collectionInfoIsLoading] = const [collectionInfo, collectionInfoIsLoading] =
useFetchCollectionInfo("info"); useFetchCollectionInfo("info");
const [ const [collectionComments, collectionCommentsIsLoading] =
collectionComments, useFetchCollectionInfo("comments");
collectionCommentsError,
collectionCommentsIsLoading,
] = useFetchCollectionInfo("comments");
const { data, error, isLoading, size, setSize } = useSWRInfinite( const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey, getKey,
useSWRfetcher, fetcher,
{ initialSize: 2 } { initialSize: 2 }
); );
@ -65,6 +77,7 @@ export const ViewCollectionPage = (props: { id: number }) => {
allReleases.push(...data[i].content); allReleases.push(...data[i].content);
} }
setContent(allReleases); setContent(allReleases);
setIsLoadingEnd(true);
} }
}, [data]); }, [data]);
@ -76,35 +89,14 @@ export const ViewCollectionPage = (props: { id: number }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]); }, [scrollPosition]);
if (isLoading) {
return (
<div className="flex items-center justify-center w-full h-screen">
<Spinner />
</div>
);
}
if (error || collectionInfoError) {
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 (
<> <>
{collectionInfoIsLoading ? {collectionInfoIsLoading ? (
<div className="flex items-center justify-center w-full h-screen"> <div className="flex items-center justify-center w-full h-screen">
<Spinner /> <Spinner />
</div> </div>
: collectionInfo && ( ) : (
collectionInfo && (
<> <>
<div className="flex flex-col flex-wrap gap-2 px-2 pb-2 sm:flex-row"> <div className="flex flex-col flex-wrap gap-2 px-2 pb-2 sm:flex-row">
<CollectionInfoBasics <CollectionInfoBasics
@ -146,7 +138,11 @@ export const ViewCollectionPage = (props: { id: number }) => {
)} )}
</div> </div>
</div> </div>
{content && ( {isLoading || !content || !isLoadingEnd ? (
<div className="flex items-center justify-center w-full h-screen">
<Spinner />
</div>
) : (
<ReleaseSection <ReleaseSection
sectionTitle={"Релизов в коллекции: " + data[0].total_count} sectionTitle={"Релизов в коллекции: " + data[0].total_count}
content={content} content={content}
@ -154,7 +150,7 @@ export const ViewCollectionPage = (props: { id: number }) => {
)} )}
</> </>
) )
} )}
</> </>
); );
}; };

View file

@ -16,26 +16,19 @@ export async function generateMetadata(
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const profile: any = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `https://api.anixart.tv/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: SectionTitleMapping[params.slug] + " - " + profile.profile.login,
description: "Ошибка", description: profile.profile.status,
};
};
return {
title:"Закладки Пользователя - " + data.profile.login + " - " + SectionTitleMapping[params.slug],
description: "Закладки Пользователя - " + data.profile.login + " - " + SectionTitleMapping[params.slug],
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.profile.avatar, // Must be an absolute URL url: profile.profile.avatar, // Must be an absolute URL
width: 600, width: 600,
height: 600, height: 600,
}, },

View file

@ -1,33 +1,26 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const profile: any = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `https://api.anixart.tv/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: "Закладки - " + profile.profile.login,
description: "Ошибка", description: profile.profile.status,
};
};
return {
title: "Закладки Пользователя - " + data.profile.login,
description: "Закладки Пользователя - " + data.profile.login,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.profile.avatar, // Must be an absolute URL url: profile.profile.avatar, // Must be an absolute URL
width: 600, width: 600,
height: 600, height: 600,
}, },

View file

@ -1,65 +1,43 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const profile: any = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `https://api.anixart.tv/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: "Коллекции - " + profile.profile.login,
description: "Ошибка", description: profile.profile.status,
};
};
return {
title: "Коллекции Пользователя - " + data.profile.login,
description: "Коллекции Пользователя - " + data.profile.login,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.profile.avatar, // Must be an absolute URL url: profile.profile.avatar, // Must be an absolute URL
width: 600, width: 600,
height: 600, height: 600,
}, },
], ],
}, },
}; };
};
export default async function Collections({ params }) {
const { data, error } = await fetchDataViaGet(
`https://api.anixart.tv/profile/${params.id}`
);
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>
);
} }
export default async function Collections({ params }) {
const profile: any = await fetchDataViaGet(
`https://api.anixart.tv/profile/${params.id}`
);
return ( return (
<CollectionsFullPage <CollectionsFullPage
type="profile" type="profile"
title={`Коллекции пользователя: ${data.profile.login}`} title={`Коллекции пользователя ${profile.profile.login}`}
profile_id={params.id} profile_id={params.id}
/> />
); );
}; }

View file

@ -1,33 +1,26 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id: string = params.id; const id: string = params.id;
const { data, error } = await fetchDataViaGet( const profile: any = await fetchDataViaGet(
`https://api.anixart.tv/profile/${id}` `https://api.anixart.tv/profile/${id}`
); );
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: "Профиль - " + profile.profile.login,
description: "Ошибка", description: profile.profile.status,
};
}
return {
title: "Профиль - " + data.profile.login,
description: data.profile.status,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.profile.avatar, // Must be an absolute URL url: profile.profile.avatar, // Must be an absolute URL
width: 600, width: 600,
height: 600, height: 600,
}, },

View file

@ -3,31 +3,12 @@ import { fetchDataViaGet } from "#/api/utils";
import type { Metadata, ResolvingMetadata } from "next"; import type { Metadata, ResolvingMetadata } from "next";
export const dynamic = 'force-static'; export const dynamic = 'force-static';
const _getData = async (url: string) => {
const { data, error } = await fetchDataViaGet(url);
return [data, error];
}
export async function generateMetadata({ params }, parent: ResolvingMetadata): Promise<Metadata> { export async function generateMetadata({ params }, parent: ResolvingMetadata): Promise<Metadata> {
const id:string = params.id; const id:string = params.id;
const related: any = await fetchDataViaGet(`https://api.anixart.tv/related/${id}/0`);
const firstRelease: any = await fetchDataViaGet(`https://api.anixart.tv/release/${related.content[0].id}`);
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
const [ related, relatedError ] = await _getData(`https://api.anixart.tv/related/${id}/0`);
if (relatedError || related.content.length == 0) {
return {
title: "Ошибка",
description: "Ошибка",
};
};
const [ firstRelease, firstReleaseError ] = await _getData(`https://api.anixart.tv/release/${related.content[0].id}`);
if (firstReleaseError) {
return {
title: "Ошибка",
description: "Ошибка",
};
};
return { return {
title: "Франшиза - " + firstRelease.release.related.name_ru || firstRelease.release.related.name, title: "Франшиза - " + firstRelease.release.related.name_ru || firstRelease.release.related.name,
description: firstRelease.release.description, description: firstRelease.release.description,
@ -46,25 +27,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: any = await fetchDataViaGet(`https://api.anixart.tv/related/${id}/0`);
if (relatedError || related.content.length == 0) { const firstRelease: any = await fetchDataViaGet(`https://api.anixart.tv/release/${related.content[0].id}`);
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>
};
const [ firstRelease, firstReleaseError ] = await _getData(`https://api.anixart.tv/release/${related.content[0].id}`);
if (firstReleaseError) {
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 <RelatedPage id={id} title={firstRelease.release.related.name_ru || firstRelease.release.related.name} />; return <RelatedPage id={id} title={firstRelease.release.related.name_ru || firstRelease.release.related.name} />;
} }

View file

@ -1,33 +1,24 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const release = await fetchDataViaGet(`https://api.anixart.tv/release/${id}`);
`https://api.anixart.tv/release/${id}`
);
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: release.release.title_ru + " - в коллекциях",
description: "Ошибка", description: release.release.description,
};
}
return {
title: data.release.title_ru + " - в коллекциях",
description: data.release.description,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.release.image, // Must be an absolute URL url: release.release.image, // Must be an absolute URL
width: 600, width: 600,
height: 800, height: 800,
}, },
@ -37,26 +28,13 @@ export async function generateMetadata(
} }
export default async function Collections({ params }) { export default async function Collections({ params }) {
const { data, error } = await fetchDataViaGet( const release: any = await fetchDataViaGet(
`https://api.anixart.tv/release/${params.id}` `https://api.anixart.tv/release/${params.id}`
); );
if (error) {
<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 (
<CollectionsFullPage <CollectionsFullPage
type="release" type="release"
title={data.release.title_ru + " в коллекциях"} title={release.release.title_ru + " в коллекциях"}
release_id={params.id} release_id={params.id}
/> />
); );

View file

@ -1,33 +1,24 @@
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"; export const dynamic = 'force-static';
export async function generateMetadata( export async function generateMetadata(
{ params }, { params },
parent: ResolvingMetadata parent: ResolvingMetadata
): Promise<Metadata> { ): Promise<Metadata> {
const id = params.id; const id = params.id;
const { data, error } = await fetchDataViaGet( const release = await fetchDataViaGet(`https://api.anixart.tv/release/${id}`);
`https://api.anixart.tv/release/${id}`
);
const previousOG = (await parent).openGraph; const previousOG = (await parent).openGraph;
if (error) {
return { return {
title: "Ошибка", title: release.release.title_ru,
description: "Ошибка", description: release.release.description,
};
}
return {
title: data.release.title_ru,
description: data.release.description,
openGraph: { openGraph: {
...previousOG, ...previousOG,
images: [ images: [
{ {
url: data.release.image, // Must be an absolute URL url: release.release.image, // Must be an absolute URL
width: 600, width: 600,
height: 800, height: 800,
}, },

View file

@ -44,16 +44,14 @@ export const useUserStore = create<userState>((set, get) => ({
const jwt = getJWT(); const jwt = getJWT();
if (jwt) { if (jwt) {
const _checkAuth = async () => { const _checkAuth = async () => {
const { data, error } = await fetchDataViaGet( const data = await fetchDataViaGet(
`${ENDPOINTS.user.profile}/${jwt.user_id}?token=${jwt.jwt}` `${ENDPOINTS.user.profile}/${jwt.user_id}?token=${jwt.jwt}`
); );
if (data && data.is_my_profile) {
if (error || !data.is_my_profile) {
get().logout();
return;
}
get().login(data.profile, jwt.jwt); get().login(data.profile, jwt.jwt);
} else {
get().logout();
}
}; };
_checkAuth(); _checkAuth();
} else { } else {

View file

@ -6,10 +6,10 @@ AniX - это неофициальный веб-клиент для Android-пр
## Список изменений ## Список изменений
- [3.4.0](/public/changelog/3.4.0.md)
- [3.3.0](/public/changelog/3.3.0.md) - [3.3.0](/public/changelog/3.3.0.md)
- [3.2.3](/public/changelog/3.2.3.md) - [3.2.3](/public/changelog/3.2.3.md)
- [3.2.2](/public/changelog/3.2.2.md) - [3.2.2](/public/changelog/3.2.2.md)
- [3.2.1](/public/changelog/3.2.1.md)
[другие версии](/public/changelog) [другие версии](/public/changelog)

View file

@ -18,13 +18,10 @@ export default async function middleware(
} }
let path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search; let path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search;
const { data, error } = await fetchDataViaGet( const data = await fetchDataViaGet(`${API_URL}/${path}`, isApiV2);
`${API_URL}/${path}`,
isApiV2
);
if (error) { if (!data) {
return new Response(JSON.stringify(error), { return new Response(JSON.stringify({ message: "Error Fetching Data" }), {
status: 500, status: 500,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -49,33 +46,26 @@ export default async function middleware(
const path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search; const path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search;
const ReqContentTypeHeader = request.headers.get("Content-Type") || ""; const ReqContentTypeHeader = request.headers.get("Content-Type") || "";
const ReqSignHeader = request.headers.get("Sign") || null;
let ResContentTypeHeader = ""; let ResContentTypeHeader = "";
let body = null; let body = null;
if (ReqContentTypeHeader.split(";")[0] == "multipart/form-data") { if (ReqContentTypeHeader.split(";")[0] == "multipart/form-data") {
ResContentTypeHeader = ReqContentTypeHeader; ResContentTypeHeader = ReqContentTypeHeader;
body = await request.arrayBuffer(); body = await request.arrayBuffer();
} else if (ReqContentTypeHeader == "application/x-www-form-urlencoded") {
ResContentTypeHeader = ReqContentTypeHeader;
} else { } else {
ResContentTypeHeader = "application/json; charset=UTF-8"; ResContentTypeHeader = "application/json; charset=UTF-8";
body = JSON.stringify(await request.json()); body = JSON.stringify(await request.json());
} }
let resHeaders = {}; const data = await fetchDataViaPost(
resHeaders["Content-Type"] = ResContentTypeHeader;
ReqSignHeader && (resHeaders["Sign"] = ReqSignHeader);
const { data, error } = await fetchDataViaPost(
`${API_URL}/${path}`, `${API_URL}/${path}`,
body, body,
isApiV2, isApiV2,
resHeaders ResContentTypeHeader
); );
if (error) { if (!data) {
return new Response(JSON.stringify(error), { return new Response(JSON.stringify({ message: "Error Fetching Data" }), {
status: 500, status: 500,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

23
package-lock.json generated
View file

@ -20,7 +20,6 @@
"react": "^18", "react": "^18",
"react-cropper": "^2.3.3", "react-cropper": "^2.3.3",
"react-dom": "^18", "react-dom": "^18",
"react-toastify": "^11.0.5",
"swiper": "^11.1.4", "swiper": "^11.1.4",
"swr": "^2.2.5", "swr": "^2.2.5",
"videojs-video-element": "^1.4.1", "videojs-video-element": "^1.4.1",
@ -1672,15 +1671,6 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -4775,19 +4765,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true "dev": true
}, },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View file

@ -21,7 +21,6 @@
"react": "^18", "react": "^18",
"react-cropper": "^2.3.3", "react-cropper": "^2.3.3",
"react-dom": "^18", "react-dom": "^18",
"react-toastify": "^11.0.5",
"swiper": "^11.1.4", "swiper": "^11.1.4",
"swr": "^2.2.5", "swr": "^2.2.5",
"videojs-video-element": "^1.4.1", "videojs-video-element": "^1.4.1",

View file

@ -14,13 +14,13 @@
- Исправлено отображение времени года в информации о релизе - Исправлено отображение времени года в информации о релизе
- Исправлена ошибка когда плеер не может загрузиться - Исправлена ошибка когда плеер не может загрузиться
## 3.3.0 - Update 1 # 3.3.0 - Update 1
- Добавлена Кнопка пропуска опенинга (90 секунд) - Добавлена Кнопка пропуска опенинга (90 секунд)
- Исправлено обрезание квадратных видео в собственном плеере - Исправлено обрезание квадратных видео в собственном плеере
- Изменён вид навигации и добавлена возможность изменять отображение текста на кнопках - Изменён вид навигации и добавлена возможность изменять отображение текста на кнопках
## 3.3.0 - Update 2 # 3.3.0 - Update 2
- Исправлен сброс скорости воспроизведения при смене серии в собственном плеере - Исправлен сброс скорости воспроизведения при смене серии в собственном плеере
- Исправлен парсинг ссылок источника kodik для некоторых аниме - Исправлен парсинг ссылок источника kodik для некоторых аниме

View file

@ -1,17 +0,0 @@
# 3.4.0
## Добавлено
- Добавлены уведомления о действиях пользователя на различных страницах (например: добавление в избранное или друзья).
- Добавлен показ ошибок при загрузке своего плеера.
## Изменено
- Улучшено отображение ошибок
- Улучшено отображение страницы релиза на некоторых релизах
- Добавлено больше пространства между иконками в навигации на телефонах
## Исправлено
- Вид карточек в окне поиска релизов для добавления в коллекцию
- Сброс добавленных релизов при изменении или создании коллекции при поиска другого запроса в окне поиска релизов