mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-04 23:34:38 +00:00
commit
c5a1c40f51
55 changed files with 2345 additions and 1622 deletions
15
app/App.tsx
15
app/App.tsx
|
@ -8,6 +8,7 @@ import { Button, Modal } from "flowbite-react";
|
|||
import { Spinner } from "./components/Spinner/Spinner";
|
||||
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
import { Bounce, ToastContainer } from "react-toastify";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
@ -111,6 +112,20 @@ export const App = (props) => {
|
|||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const CURRENT_APP_VERSION = "3.3.0";
|
||||
export const CURRENT_APP_VERSION = "3.4.0";
|
||||
|
||||
export const API_URL = "https://api.anixart.tv";
|
||||
export const API_PREFIX = "/api/proxy";
|
||||
|
@ -13,6 +13,7 @@ export const ENDPOINTS = {
|
|||
licensed: `${API_PREFIX}/release/streaming/platform`,
|
||||
},
|
||||
user: {
|
||||
auth: `${API_PREFIX}/auth/signIn`,
|
||||
profile: `${API_PREFIX}/profile`,
|
||||
bookmark: `${API_PREFIX}/profile/list`,
|
||||
history: `${API_PREFIX}/history`,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -49,16 +49,26 @@ export async function GET(request: NextRequest) {
|
|||
if (token) {
|
||||
url.searchParams.set("token", token);
|
||||
}
|
||||
const data = { query, searchBy };
|
||||
const body = { query, searchBy };
|
||||
|
||||
const response = await fetchDataViaPost(
|
||||
const { data, error } = await fetchDataViaPost(
|
||||
url.toString(),
|
||||
JSON.stringify(data),
|
||||
JSON.stringify(body),
|
||||
true
|
||||
);
|
||||
if (!response) {
|
||||
return NextResponse.json({ message: "Bad request" }, { status: 400 });
|
||||
if (error) {
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(response);
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
186
app/api/utils.ts
186
app/api/utils.ts
|
@ -4,79 +4,159 @@ export const HEADERS = {
|
|||
"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 (
|
||||
url: string,
|
||||
API_V2: string | boolean = false
|
||||
API_V2: string | boolean = false,
|
||||
addHeaders?: Record<string, any>
|
||||
) => {
|
||||
if (API_V2) {
|
||||
HEADERS["API-Version"] = "v2";
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: HEADERS,
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const { data, error } = await tryCatchAPI(
|
||||
fetch(url, {
|
||||
headers: { ...HEADERS, ...addHeaders },
|
||||
})
|
||||
);
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
export const fetchDataViaPost = async (
|
||||
url: string,
|
||||
body: string,
|
||||
API_V2: string | boolean = false,
|
||||
contentType: string = ""
|
||||
addHeaders?: Record<string, any>
|
||||
) => {
|
||||
if (API_V2) {
|
||||
HEADERS["API-Version"] = "v2";
|
||||
}
|
||||
if (contentType != "") {
|
||||
HEADERS["Content-Type"] = contentType;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const { data, error } = await tryCatchAPI(
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: HEADERS,
|
||||
body: body,
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
headers: { ...HEADERS, ...addHeaders },
|
||||
})
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
export function setJWT(user_id: number | string, jwt: string) {
|
||||
|
|
|
@ -1,28 +1,36 @@
|
|||
import { ViewCollectionPage } from "#/pages/ViewCollection";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id = params.id;
|
||||
const collection = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/collection/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Приватная коллекция",
|
||||
description: "Приватная коллекция",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: collection.collection
|
||||
? "коллекция - " + collection.collection.title
|
||||
title:
|
||||
data.collection ?
|
||||
"коллекция - " + data.collection.title
|
||||
: "Приватная коллекция",
|
||||
description: collection.collection && collection.collection.description,
|
||||
description: data.collection && data.collection.description,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: collection.collection && collection.collection.image, // Must be an absolute URL
|
||||
url: data.collection && data.collection.image, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 800,
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Modal, Accordion } from "flowbite-react";
|
|||
import Markdown from "markdown-to-jsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import Styles from "./ChangelogModal.module.css";
|
||||
import { tryCatch } from "#/api/utils";
|
||||
|
||||
export const ChangelogModal = (props: {
|
||||
isOpen: boolean;
|
||||
|
@ -17,29 +18,20 @@ export const ChangelogModal = (props: {
|
|||
>({});
|
||||
|
||||
async function _fetchVersionChangelog(version: string) {
|
||||
const res = await fetch(`/changelog/${version}.md`);
|
||||
return await res.text();
|
||||
const { data, error } = await tryCatch(fetch(`/changelog/${version}.md`));
|
||||
if (error) {
|
||||
return "Нет списка изменений";
|
||||
}
|
||||
return await data.text();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.version != "" && currentVersionChangelog == "") {
|
||||
setCurrentVersionChangelog("Загрузка ...");
|
||||
_fetchVersionChangelog(props.version).then((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
|
||||
}, [props.version]);
|
||||
|
||||
|
@ -50,20 +42,38 @@ export const ChangelogModal = (props: {
|
|||
<Markdown className={Styles.markdown}>
|
||||
{currentVersionChangelog}
|
||||
</Markdown>
|
||||
{Object.keys(previousVersionsChangelog).length == props.previousVersions.length && (
|
||||
<Accordion collapseAll={true} className="mt-4">
|
||||
{props.previousVersions.map(
|
||||
(version) => (
|
||||
<Accordion collapseAll={true} className="mt-4">
|
||||
{props.previousVersions.length > 0 &&
|
||||
props.previousVersions.map((version) => {
|
||||
return (
|
||||
<Accordion.Panel key={version}>
|
||||
<Accordion.Title>Список изменений v{version}</Accordion.Title>
|
||||
<Accordion.Title
|
||||
onClickCapture={(e) => {
|
||||
if (!previousVersionsChangelog.hasOwnProperty(version)) {
|
||||
_fetchVersionChangelog(version).then((data) => {
|
||||
setPreviousVersionsChangelog((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[version]: data,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Список изменений v{version}
|
||||
</Accordion.Title>
|
||||
<Accordion.Content>
|
||||
<Markdown className={Styles.markdown}>{previousVersionsChangelog[version]}</Markdown>
|
||||
{previousVersionsChangelog.hasOwnProperty(version) ?
|
||||
<Markdown className={Styles.markdown}>
|
||||
{previousVersionsChangelog[version]}
|
||||
</Markdown>
|
||||
: <div>Загрузка ...</div>}
|
||||
</Accordion.Content>
|
||||
</Accordion.Panel>
|
||||
)
|
||||
)}
|
||||
</Accordion>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"use client";
|
||||
import { Card, Button } from "flowbite-react";
|
||||
import { Card, Button, useThemeMode } from "flowbite-react";
|
||||
import { useState } from "react";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const CollectionInfoControls = (props: {
|
||||
isFavorite: boolean;
|
||||
|
@ -12,36 +14,124 @@ export const CollectionInfoControls = (props: {
|
|||
isPrivate: boolean;
|
||||
}) => {
|
||||
const [isFavorite, setIsFavorite] = useState(props.isFavorite);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const theme = useThemeMode();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
async function _addToFavorite() {
|
||||
if (userStore.user) {
|
||||
setIsFavorite(!isFavorite);
|
||||
if (isFavorite) {
|
||||
fetch(
|
||||
`${ENDPOINTS.collection.favoriteCollections}/delete/${props.id}?token=${userStore.token}`
|
||||
);
|
||||
} else {
|
||||
fetch(
|
||||
`${ENDPOINTS.collection.favoriteCollections}/add/${props.id}?token=${userStore.token}`
|
||||
);
|
||||
async function _FavCol(url: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (userStore.token) {
|
||||
let url = `${ENDPOINTS.collection.favoriteCollections}/add/${props.id}?token=${userStore.token}`;
|
||||
if (isFavorite) {
|
||||
url = `${ENDPOINTS.collection.favoriteCollections}/delete/${props.id}?token=${userStore.token}`;
|
||||
}
|
||||
_FavCol(url);
|
||||
}
|
||||
}
|
||||
|
||||
async function _deleteCollection() {
|
||||
if (userStore.user) {
|
||||
fetch(
|
||||
async function _DelCol(url: string) {
|
||||
setIsUpdating(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: "Ошибка удаления коллекции",
|
||||
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}`
|
||||
);
|
||||
router.push("/collections");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full h-fit ">
|
||||
<Button color={"blue"} onClick={() => _addToFavorite()}>
|
||||
<Button
|
||||
color={"blue"}
|
||||
onClick={() => _addToFavorite()}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span
|
||||
className={`iconify w-6 h-6 mr-2 ${
|
||||
isFavorite ? "mdi--heart" : "mdi--heart-outline"
|
||||
|
@ -60,6 +150,7 @@ export const CollectionInfoControls = (props: {
|
|||
onClick={() =>
|
||||
router.push("/collections/create?mode=edit&id=" + props.id)
|
||||
}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span className="w-6 h-6 mr-2 iconify mdi--pencil"></span>{" "}
|
||||
Редактировать
|
||||
|
@ -68,6 +159,7 @@ export const CollectionInfoControls = (props: {
|
|||
color={"red"}
|
||||
className="w-full sm:max-w-64"
|
||||
onClick={() => _deleteCollection()}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<span className="w-6 h-6 mr-2 iconify mdi--trash"></span> Удалить
|
||||
</Button>
|
||||
|
|
|
@ -15,7 +15,7 @@ export const CollectionLink = (props: any) => {
|
|||
<Image
|
||||
src={props.image}
|
||||
fill={true}
|
||||
alt={props.title}
|
||||
alt={props.title || ""}
|
||||
className="-z-[1] object-cover"
|
||||
sizes="
|
||||
(max-width: 768px) 300px,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
|
|||
import { ENDPOINTS } from "#/api/config";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import { CommentsAddModal } from "./Comments.Add";
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
export const CommentsMain = (props: {
|
||||
release_id: number;
|
||||
|
@ -82,20 +83,6 @@ 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: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: any;
|
||||
|
@ -103,7 +90,6 @@ const CommentsAllModal = (props: {
|
|||
token: string | null;
|
||||
type?: "release" | "collection";
|
||||
}) => {
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const [currentRef, setCurrentRef] = useState<any>(null);
|
||||
const modalRef = useCallback((ref) => {
|
||||
setCurrentRef(ref);
|
||||
|
@ -127,7 +113,7 @@ const CommentsAllModal = (props: {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -139,7 +125,6 @@ const CommentsAllModal = (props: {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -170,7 +155,7 @@ const CommentsAllModal = (props: {
|
|||
Все комментарии
|
||||
</h2>
|
||||
<p className="text-sm font-light text-gray-600 dark:text-gray-300">
|
||||
всего: {!isLoadingEnd ? "загрузка..." : data[0].total_count}
|
||||
всего: {isLoading ? "загрузка..." : data[0].total_count}
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
|
@ -179,7 +164,7 @@ const CommentsAllModal = (props: {
|
|||
onScroll={handleScroll}
|
||||
ref={modalRef}
|
||||
>
|
||||
{!isLoadingEnd ? (
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : content ? (
|
||||
content.map((comment: any) => (
|
||||
|
|
|
@ -3,56 +3,86 @@ import Cropper, { ReactCropperElement } from "react-cropper";
|
|||
import "cropperjs/dist/cropper.css";
|
||||
import { Button, Modal } from "flowbite-react";
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
setSrc: (src: string) => void;
|
||||
setTempSrc: (src: string) => void;
|
||||
type CropModalProps = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
height: number;
|
||||
width: number;
|
||||
aspectRatio: number;
|
||||
guides: boolean;
|
||||
quality: number;
|
||||
forceAspect?: boolean;
|
||||
isActionsDisabled: boolean;
|
||||
selectedImage: any | null;
|
||||
croppedImage: any | null;
|
||||
setCropModalProps: (props: {
|
||||
isOpen: boolean;
|
||||
isActionsDisabled: boolean;
|
||||
selectedImage: any | null;
|
||||
croppedImage: any | null;
|
||||
}) => void;
|
||||
cropParams: {
|
||||
guides?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: number;
|
||||
aspectRatio?: number;
|
||||
forceAspect?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const CropModal: React.FC<Props> = (props) => {
|
||||
export const CropModal: React.FC<CropModalProps> = ({
|
||||
isOpen,
|
||||
setCropModalProps,
|
||||
cropParams,
|
||||
selectedImage,
|
||||
croppedImage,
|
||||
isActionsDisabled,
|
||||
}) => {
|
||||
const cropperRef = useRef<ReactCropperElement>(null);
|
||||
|
||||
const getCropData = () => {
|
||||
if (typeof cropperRef.current?.cropper !== "undefined") {
|
||||
props.setSrc(
|
||||
cropperRef.current?.cropper
|
||||
.getCroppedCanvas({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
maxWidth: props.width,
|
||||
maxHeight: props.height,
|
||||
})
|
||||
.toDataURL("image/jpeg", props.quality)
|
||||
);
|
||||
props.setTempSrc("");
|
||||
const croppedImage = cropperRef.current?.cropper
|
||||
.getCroppedCanvas({
|
||||
width: cropParams.width,
|
||||
height: cropParams.height,
|
||||
maxWidth: cropParams.width,
|
||||
maxHeight: cropParams.height,
|
||||
})
|
||||
.toDataURL(
|
||||
"image/jpeg",
|
||||
cropParams.quality || false ? cropParams.quality : 100
|
||||
);
|
||||
|
||||
setCropModalProps({
|
||||
isOpen: true,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: selectedImage,
|
||||
croppedImage: croppedImage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dismissible
|
||||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
show={isOpen}
|
||||
onClose={() => {
|
||||
setCropModalProps({
|
||||
isOpen: false,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: null,
|
||||
croppedImage: null,
|
||||
});
|
||||
}}
|
||||
size={"7xl"}
|
||||
>
|
||||
<Modal.Header>Обрезать изображение</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Cropper
|
||||
src={props.src}
|
||||
src={selectedImage}
|
||||
style={{ height: 400, width: "100%" }}
|
||||
responsive={true}
|
||||
// Cropper.js options
|
||||
initialAspectRatio={props.aspectRatio}
|
||||
aspectRatio={props.forceAspect ? props.aspectRatio : undefined}
|
||||
guides={props.guides}
|
||||
initialAspectRatio={cropParams.aspectRatio || 1 / 1}
|
||||
aspectRatio={
|
||||
cropParams.forceAspect || false ? cropParams.aspectRatio : undefined
|
||||
}
|
||||
guides={cropParams.guides || false}
|
||||
ref={cropperRef}
|
||||
/>
|
||||
|
||||
|
@ -69,23 +99,26 @@ export const CropModal: React.FC<Props> = (props) => {
|
|||
<Modal.Footer>
|
||||
<Button
|
||||
color={"blue"}
|
||||
disabled={isActionsDisabled}
|
||||
onClick={() => {
|
||||
getCropData();
|
||||
props.setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
color={"red"}
|
||||
disabled={isActionsDisabled}
|
||||
onClick={() => {
|
||||
props.setSrc(null);
|
||||
props.setTempSrc(null);
|
||||
// props.setImageData(null);
|
||||
props.setIsOpen(false);
|
||||
setCropModalProps({
|
||||
isOpen: false,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: null,
|
||||
croppedImage: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
Отменить
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
|
|
@ -1,290 +0,0 @@
|
|||
"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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -87,8 +87,8 @@ export const Navbar = () => {
|
|||
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">
|
||||
<div className="container flex items-center justify-center mx-auto sm:justify-between">
|
||||
<div className="flex items-center gap-4 px-2 py-4">
|
||||
<div className="container flex items-center justify-center gap-4 mx-auto sm:gap-0 sm:justify-between">
|
||||
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4">
|
||||
{menuItems.map((item) => {
|
||||
return (
|
||||
<Link
|
||||
|
@ -112,7 +112,7 @@ export const Navbar = () => {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 px-2 py-4">
|
||||
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4">
|
||||
{!userStore.isAuth ?
|
||||
<Link
|
||||
href={
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { Card, Button } from "flowbite-react";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
import { Card, Button, useThemeMode } from "flowbite-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
// null - не друзья
|
||||
|
@ -24,11 +26,12 @@ export const ProfileActions = (props: {
|
|||
edit_isOpen: boolean;
|
||||
edit_setIsOpen: any;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const profileIdIsSmaller = props.my_profile_id < props.profile_id;
|
||||
const [friendRequestDisabled, setFriendRequestDisabled] = useState(false);
|
||||
const [blockRequestDisabled, setBlockRequestDisabled] = useState(false);
|
||||
const theme = useThemeMode();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const [actionsDisabled, setActionsDisabled] = useState(false);
|
||||
|
||||
function _getFriendStatus() {
|
||||
const num = props.friendStatus;
|
||||
|
||||
|
@ -54,53 +57,119 @@ export const ProfileActions = (props: {
|
|||
}
|
||||
const FriendStatus = _getFriendStatus();
|
||||
const isRequestedStatus =
|
||||
FriendStatus != null
|
||||
? profileIdIsSmaller
|
||||
? profileIdIsSmaller && FriendStatus != 0
|
||||
: !profileIdIsSmaller && FriendStatus == 2
|
||||
: null;
|
||||
FriendStatus != null ?
|
||||
profileIdIsSmaller ? profileIdIsSmaller && FriendStatus != 0
|
||||
: !profileIdIsSmaller && FriendStatus == 2
|
||||
: null;
|
||||
// ^ This is some messed up shit
|
||||
|
||||
function _addToFriends() {
|
||||
let url = `${ENDPOINTS.user.profile}/friend/request`;
|
||||
setFriendRequestDisabled(true);
|
||||
setBlockRequestDisabled(true);
|
||||
async function _addToFriends() {
|
||||
setActionsDisabled(true);
|
||||
|
||||
FriendStatus == 1
|
||||
? (url += "/remove/")
|
||||
: isRequestedStatus
|
||||
? (url += "/remove/")
|
||||
: (url += "/send/");
|
||||
|
||||
url += `${props.profile_id}?token=${props.token}`;
|
||||
fetch(url).then((res) => {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setTimeout(() => {
|
||||
setBlockRequestDisabled(false);
|
||||
setFriendRequestDisabled(false);
|
||||
}, 100);
|
||||
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`;
|
||||
FriendStatus == 1 ? (url += "/remove/")
|
||||
: isRequestedStatus ? (url += "/remove/")
|
||||
: (url += "/send/");
|
||||
url += `${props.profile_id}?token=${props.token}`;
|
||||
|
||||
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(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
|
||||
toast.update(tid, {
|
||||
render:
|
||||
FriendStatus == 1 || isRequestedStatus ?
|
||||
"Удален из друзей"
|
||||
: "Добавлен в друзья",
|
||||
type: "success",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
setActionsDisabled(false);
|
||||
}
|
||||
|
||||
function _addToBlocklist() {
|
||||
async 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`;
|
||||
setBlockRequestDisabled(true);
|
||||
setFriendRequestDisabled(true);
|
||||
|
||||
!props.is_blocked ? (url += "/add/") : (url += "/remove/");
|
||||
|
||||
url += `${props.profile_id}?token=${props.token}`;
|
||||
fetch(url).then((res) => {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setTimeout(() => {
|
||||
setBlockRequestDisabled(false);
|
||||
setFriendRequestDisabled(false);
|
||||
}, 100);
|
||||
|
||||
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(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
|
||||
toast.update(tid, {
|
||||
render:
|
||||
!props.is_blocked ?
|
||||
"Пользователь заблокирован"
|
||||
: "Пользователь разблокирован",
|
||||
type: "success",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
setActionsDisabled(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -109,7 +178,14 @@ export const ProfileActions = (props: {
|
|||
<p>Отправил(-а) вам заявку в друзья</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{props.isMyProfile && <Button color={"blue"} onClick={() => props.edit_setIsOpen(!props.edit_isOpen)}>Редактировать</Button>}
|
||||
{props.isMyProfile && (
|
||||
<Button
|
||||
color={"blue"}
|
||||
onClick={() => props.edit_setIsOpen(!props.edit_isOpen)}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
)}
|
||||
{!props.isMyProfile && (
|
||||
<>
|
||||
{(!props.isFriendRequestsDisallowed ||
|
||||
|
@ -118,26 +194,25 @@ export const ProfileActions = (props: {
|
|||
!props.is_me_blocked &&
|
||||
!props.is_blocked && (
|
||||
<Button
|
||||
disabled={friendRequestDisabled}
|
||||
disabled={actionsDisabled}
|
||||
color={
|
||||
FriendStatus == 1
|
||||
? "red"
|
||||
: isRequestedStatus
|
||||
? "light"
|
||||
: "blue"
|
||||
FriendStatus == 1 ? "red"
|
||||
: isRequestedStatus ?
|
||||
"light"
|
||||
: "blue"
|
||||
}
|
||||
onClick={() => _addToFriends()}
|
||||
>
|
||||
{FriendStatus == 1
|
||||
? "Удалить из друзей"
|
||||
: isRequestedStatus
|
||||
? "Заявка отправлена"
|
||||
: "Добавить в друзья"}
|
||||
{FriendStatus == 1 ?
|
||||
"Удалить из друзей"
|
||||
: isRequestedStatus ?
|
||||
"Заявка отправлена"
|
||||
: "Добавить в друзья"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color={!props.is_blocked ? "red" : "blue"}
|
||||
disabled={blockRequestDisabled}
|
||||
disabled={actionsDisabled}
|
||||
onClick={() => _addToBlocklist()}
|
||||
>
|
||||
{!props.is_blocked ? "Заблокировать" : "Разблокировать"}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Modal, Textarea } from "flowbite-react";
|
||||
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
import { unixToDate } from "#/api/utils";
|
||||
import { toast } from "react-toastify";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
|
||||
export const ProfileEditLoginModal = (props: {
|
||||
|
@ -29,21 +31,33 @@ export const ProfileEditLoginModal = (props: {
|
|||
const [_loginLength, _setLoginLength] = useState(0);
|
||||
const { mutate } = useSWRConfig();
|
||||
const userStore = useUserStore();
|
||||
const theme = useThemeMode();
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${ENDPOINTS.user.settings.login.info}?token=${props.token}`)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
_setLoginData(data);
|
||||
_setLogin(data.login);
|
||||
_setLoginLength(data.login.length);
|
||||
async function _fetchLogin() {
|
||||
setLoading(true);
|
||||
|
||||
const { data, error } = await tryCatchAPI(
|
||||
fetch(`${ENDPOINTS.user.settings.login.info}?token=${props.token}`)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
toast.error("Ошибка получения текущего никнейма", {
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
props.setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
_setLoginData(data);
|
||||
_setLogin(data.login);
|
||||
_setLoginLength(data.login.length);
|
||||
setLoading(false);
|
||||
}
|
||||
_fetchLogin();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.isOpen]);
|
||||
|
||||
|
@ -52,43 +66,69 @@ export const ProfileEditLoginModal = (props: {
|
|||
_setLoginLength(e.target.value.length);
|
||||
}
|
||||
|
||||
function _setLoginSetting() {
|
||||
setSending(true);
|
||||
async function _setLoginSetting() {
|
||||
if (!_login || _login == "") {
|
||||
alert("Никнейм не может быть пустым");
|
||||
toast.error("Никнейм не может быть пустым", {
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
fetch(
|
||||
`${ENDPOINTS.user.settings.login.change}?login=${encodeURIComponent(
|
||||
_login
|
||||
)}&token=${props.token}`
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
new Error("failed to send data");
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.code == 3) {
|
||||
alert("Данный никнейм уже существует, попробуйте другой");
|
||||
setSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
userStore.checkAuth();
|
||||
props.setLogin(_login);
|
||||
setSending(false);
|
||||
props.setIsOpen(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setSending(false);
|
||||
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(
|
||||
`${ENDPOINTS.user.settings.login.change}?login=${encodeURIComponent(
|
||||
_login
|
||||
)}&token=${props.token}`
|
||||
)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
let message = `Ошибка обновления никнейма: ${error.code}`;
|
||||
if (error.code == 3) {
|
||||
message = "Данный никнейм уже существует, попробуйте другой";
|
||||
}
|
||||
toast.update(tid, {
|
||||
render: message,
|
||||
type: "error",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
setSending(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.update(tid, {
|
||||
render: "Никнейм обновлён",
|
||||
type: "success",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
userStore.checkAuth();
|
||||
props.setLogin(_login);
|
||||
setSending(false);
|
||||
props.setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -100,13 +140,12 @@ export const ProfileEditLoginModal = (props: {
|
|||
>
|
||||
<Modal.Header>Изменить никнейм</Modal.Header>
|
||||
<Modal.Body>
|
||||
{loading ? (
|
||||
{loading ?
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!_loginData.is_change_available ? (
|
||||
: <>
|
||||
{!_loginData.is_change_available ?
|
||||
<>
|
||||
<p>Вы недавно изменили никнейм</p>
|
||||
<p>
|
||||
|
@ -116,8 +155,7 @@ export const ProfileEditLoginModal = (props: {
|
|||
</span>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
: <>
|
||||
<Textarea
|
||||
disabled={sending}
|
||||
rows={1}
|
||||
|
@ -132,9 +170,9 @@ export const ProfileEditLoginModal = (props: {
|
|||
{_loginLength}/20
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
</>
|
||||
)}
|
||||
}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{_loginData.is_change_available && (
|
||||
|
@ -146,7 +184,11 @@ export const ProfileEditLoginModal = (props: {
|
|||
Сохранить
|
||||
</Button>
|
||||
)}
|
||||
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => props.setIsOpen(false)}
|
||||
disabled={sending || loading}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { FileInput, Label, Modal } from "flowbite-react";
|
||||
import { FileInput, Label, Modal, useThemeMode } from "flowbite-react";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
import useSWR from "swr";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { b64toBlob, unixToDate } from "#/api/utils";
|
||||
import { b64toBlob, tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils";
|
||||
import { ProfileEditPrivacyModal } from "./Profile.EditPrivacyModal";
|
||||
import { ProfileEditStatusModal } from "./Profile.EditStatusModal";
|
||||
import { ProfileEditSocialModal } from "./Profile.EditSocialModal";
|
||||
|
@ -13,20 +13,7 @@ import { CropModal } from "../CropModal/CropModal";
|
|||
import { useSWRConfig } from "swr";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { ProfileEditLoginModal } from "./Profile.EditLoginModal";
|
||||
|
||||
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();
|
||||
};
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const ProfileEditModal = (props: {
|
||||
isOpen: boolean;
|
||||
|
@ -37,10 +24,7 @@ export const ProfileEditModal = (props: {
|
|||
const [privacyModalOpen, setPrivacyModalOpen] = useState(false);
|
||||
const [statusModalOpen, setStatusModalOpen] = useState(false);
|
||||
const [socialModalOpen, setSocialModalOpen] = useState(false);
|
||||
const [avatarModalOpen, setAvatarModalOpen] = useState(false);
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
const [avatarUri, setAvatarUri] = useState(null);
|
||||
const [tempAvatarUri, setTempAvatarUri] = useState(null);
|
||||
const [privacyModalSetting, setPrivacyModalSetting] = useState("none");
|
||||
const [privacySettings, setPrivacySettings] = useState({
|
||||
privacy_stats: 9,
|
||||
|
@ -56,6 +40,14 @@ export const ProfileEditModal = (props: {
|
|||
const [login, setLogin] = useState("");
|
||||
const { mutate } = useSWRConfig();
|
||||
const userStore = useUserStore();
|
||||
const theme = useThemeMode();
|
||||
|
||||
const [avatarModalProps, setAvatarModalProps] = useState({
|
||||
isOpen: false,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: null,
|
||||
croppedImage: null,
|
||||
});
|
||||
|
||||
const privacy_stat_act_social_text = {
|
||||
0: "Все пользователи",
|
||||
|
@ -70,7 +62,11 @@ export const ProfileEditModal = (props: {
|
|||
};
|
||||
|
||||
function useFetchInfo(url: string) {
|
||||
const { data, isLoading, error } = useSWR(url, fetcher);
|
||||
if (!props.token) {
|
||||
url = "";
|
||||
}
|
||||
|
||||
const { data, isLoading, error } = useSWR(url, useSWRfetcher);
|
||||
return [data, isLoading, error];
|
||||
}
|
||||
|
||||
|
@ -81,15 +77,17 @@ export const ProfileEditModal = (props: {
|
|||
`${ENDPOINTS.user.settings.login.info}?token=${props.token}`
|
||||
);
|
||||
|
||||
const handleFileRead = (e, fileReader) => {
|
||||
const content = fileReader.result;
|
||||
setTempAvatarUri(content);
|
||||
};
|
||||
|
||||
const handleFilePreview = (file) => {
|
||||
const handleAvatarPreview = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (e) => {
|
||||
handleFileRead(e, fileReader);
|
||||
fileReader.onloadend = () => {
|
||||
const content = fileReader.result;
|
||||
setAvatarModalProps({
|
||||
...avatarModalProps,
|
||||
isOpen: true,
|
||||
selectedImage: content,
|
||||
});
|
||||
e.target.value = "";
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
};
|
||||
|
@ -117,8 +115,8 @@ export const ProfileEditModal = (props: {
|
|||
}, [loginData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarUri) {
|
||||
let block = avatarUri.split(";");
|
||||
async function _uploadAvatar() {
|
||||
let block = avatarModalProps.croppedImage.split(";");
|
||||
let contentType = block[0].split(":")[1];
|
||||
let realData = block[1].split(",")[1];
|
||||
const blob = b64toBlob(realData, contentType);
|
||||
|
@ -126,23 +124,73 @@ export const ProfileEditModal = (props: {
|
|||
const formData = new FormData();
|
||||
formData.append("image", blob, "cropped.jpg");
|
||||
formData.append("name", "image");
|
||||
const uploadRes = fetch(
|
||||
`${ENDPOINTS.user.settings.avatar}?token=${props.token}`,
|
||||
{
|
||||
|
||||
setAvatarModalProps(
|
||||
(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",
|
||||
body: formData,
|
||||
}
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
userStore.checkAuth();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (error) {
|
||||
toast.update(tid, {
|
||||
render: "Ошибка обновления аватара",
|
||||
type: "error",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
setAvatarModalProps(
|
||||
(state) => (state = { ...state, isActionsDisabled: false })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.update(tid, {
|
||||
render: "Аватар обновлён",
|
||||
type: "success",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
setAvatarModalProps(
|
||||
(state) =>
|
||||
(state = {
|
||||
isOpen: false,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: null,
|
||||
croppedImage: null,
|
||||
})
|
||||
);
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
userStore.checkAuth();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [avatarUri]);
|
||||
|
||||
if (avatarModalProps.croppedImage) {
|
||||
_uploadAvatar();
|
||||
}
|
||||
}, [avatarModalProps.croppedImage]);
|
||||
|
||||
if (!prefData || !loginData || prefError || loginError) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -153,10 +201,9 @@ export const ProfileEditModal = (props: {
|
|||
>
|
||||
<Modal.Header>Редактирование профиля</Modal.Header>
|
||||
<Modal.Body>
|
||||
{prefLoading ? (
|
||||
{prefLoading ?
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
|
@ -174,19 +221,18 @@ export const ProfileEditModal = (props: {
|
|||
className="hidden"
|
||||
accept="image/jpg, image/jpeg, image/png"
|
||||
onChange={(e) => {
|
||||
handleFilePreview(e.target.files[0]);
|
||||
setAvatarModalOpen(true);
|
||||
handleAvatarPreview(e);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-lg">Изменить фото профиля</p>
|
||||
<p className="text-base text-gray-500 dark:text-gray-400">
|
||||
{prefData.is_change_avatar_banned
|
||||
? `Заблокировано до ${unixToDate(
|
||||
prefData.ban_change_avatar_expires,
|
||||
"full"
|
||||
)}`
|
||||
: "Загрузить с устройства"}
|
||||
{prefData.is_change_avatar_banned ?
|
||||
`Заблокировано до ${unixToDate(
|
||||
prefData.ban_change_avatar_expires,
|
||||
"full"
|
||||
)}`
|
||||
: "Загрузить с устройства"}
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
|
@ -211,12 +257,12 @@ export const ProfileEditModal = (props: {
|
|||
>
|
||||
<p className="text-lg">Изменить никнейм</p>
|
||||
<p className="text-base text-gray-500 dark:text-gray-400">
|
||||
{prefData.is_change_login_banned
|
||||
? `Заблокировано до ${unixToDate(
|
||||
prefData.ban_change_login_expires,
|
||||
"full"
|
||||
)}`
|
||||
: login}
|
||||
{prefData.is_change_login_banned ?
|
||||
`Заблокировано до ${unixToDate(
|
||||
prefData.ban_change_login_expires,
|
||||
"full"
|
||||
)}`
|
||||
: login}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
|
@ -330,9 +376,9 @@ export const ProfileEditModal = (props: {
|
|||
<div className="p-2 mt-2 cursor-not-allowed">
|
||||
<p className="text-lg">Связанные аккаунты</p>
|
||||
<p className="text-base text-gray-500 dark:text-gray-400">
|
||||
{socialBounds.vk || socialBounds.google
|
||||
? "Аккаунт привязан к:"
|
||||
: "не привязан к сервисам"}{" "}
|
||||
{socialBounds.vk || socialBounds.google ?
|
||||
"Аккаунт привязан к:"
|
||||
: "не привязан к сервисам"}{" "}
|
||||
{socialBounds.vk && "ВК"}
|
||||
{socialBounds.vk && socialBounds.google && ", "}
|
||||
{socialBounds.google && "Google"}
|
||||
|
@ -340,51 +386,53 @@ export const ProfileEditModal = (props: {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
<ProfileEditPrivacyModal
|
||||
isOpen={privacyModalOpen}
|
||||
setIsOpen={setPrivacyModalOpen}
|
||||
token={props.token}
|
||||
setting={privacyModalSetting}
|
||||
privacySettings={privacySettings}
|
||||
setPrivacySettings={setPrivacySettings}
|
||||
/>
|
||||
<ProfileEditStatusModal
|
||||
isOpen={statusModalOpen}
|
||||
setIsOpen={setStatusModalOpen}
|
||||
token={props.token}
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
<ProfileEditSocialModal
|
||||
isOpen={socialModalOpen}
|
||||
setIsOpen={setSocialModalOpen}
|
||||
token={props.token}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
<CropModal
|
||||
src={tempAvatarUri}
|
||||
setSrc={setAvatarUri}
|
||||
setTempSrc={setTempAvatarUri}
|
||||
aspectRatio={1 / 1}
|
||||
guides={true}
|
||||
quality={100}
|
||||
isOpen={avatarModalOpen}
|
||||
setIsOpen={setAvatarModalOpen}
|
||||
forceAspect={true}
|
||||
width={600}
|
||||
height={600}
|
||||
/>
|
||||
<ProfileEditLoginModal
|
||||
isOpen={loginModalOpen}
|
||||
setIsOpen={setLoginModalOpen}
|
||||
token={props.token}
|
||||
setLogin={setLogin}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
{props.token ?
|
||||
<>
|
||||
<ProfileEditPrivacyModal
|
||||
isOpen={privacyModalOpen}
|
||||
setIsOpen={setPrivacyModalOpen}
|
||||
token={props.token}
|
||||
setting={privacyModalSetting}
|
||||
privacySettings={privacySettings}
|
||||
setPrivacySettings={setPrivacySettings}
|
||||
/>
|
||||
<ProfileEditStatusModal
|
||||
isOpen={statusModalOpen}
|
||||
setIsOpen={setStatusModalOpen}
|
||||
token={props.token}
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
<ProfileEditSocialModal
|
||||
isOpen={socialModalOpen}
|
||||
setIsOpen={setSocialModalOpen}
|
||||
token={props.token}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
<CropModal
|
||||
{...avatarModalProps}
|
||||
cropParams={{
|
||||
aspectRatio: 1 / 1,
|
||||
forceAspect: true,
|
||||
guides: true,
|
||||
width: 600,
|
||||
height: 600,
|
||||
}}
|
||||
setCropModalProps={setAvatarModalProps}
|
||||
/>
|
||||
<ProfileEditLoginModal
|
||||
isOpen={loginModalOpen}
|
||||
setIsOpen={setLoginModalOpen}
|
||||
token={props.token}
|
||||
setLogin={setLogin}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
</>
|
||||
: ""}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { Modal } from "flowbite-react";
|
||||
import { Modal, useThemeMode } from "flowbite-react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
|
||||
export const ProfileEditPrivacyModal = (props: {
|
||||
isOpen: boolean;
|
||||
|
@ -33,33 +35,60 @@ export const ProfileEditPrivacyModal = (props: {
|
|||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const theme = useThemeMode();
|
||||
|
||||
function _setPrivacySetting(el: any) {
|
||||
async function _setPrivacySetting(el: any) {
|
||||
let privacySettings = structuredClone(props.privacySettings);
|
||||
setLoading(true);
|
||||
fetch(_endpoints[props.setting], {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permission: el.target.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
setLoading(false);
|
||||
privacySettings[el.target.name] = el.target.value;
|
||||
props.setPrivacySettings(privacySettings);
|
||||
props.setIsOpen(false)
|
||||
} else {
|
||||
new Error("failed to send data");
|
||||
}
|
||||
|
||||
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], {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
permission: el.target.value,
|
||||
}),
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
);
|
||||
|
||||
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);
|
||||
privacySettings[el.target.name] = el.target.value;
|
||||
props.setPrivacySettings(privacySettings);
|
||||
props.setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -71,10 +100,10 @@ export const ProfileEditPrivacyModal = (props: {
|
|||
>
|
||||
<Modal.Header>{setting_text[props.setting]}</Modal.Header>
|
||||
<Modal.Body>
|
||||
{props.setting != "none" ? (
|
||||
{props.setting != "none" ?
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.setting == "privacy_friend_requests" ? (
|
||||
{props.setting == "privacy_friend_requests" ?
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
|
@ -113,8 +142,7 @@ export const ProfileEditPrivacyModal = (props: {
|
|||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
: <>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
disabled={loading}
|
||||
|
@ -170,12 +198,10 @@ export const ProfileEditPrivacyModal = (props: {
|
|||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
: ""}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Modal, Label, TextInput } from "flowbite-react";
|
||||
import { Button, Modal, Label, TextInput, useThemeMode } from "flowbite-react";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { toast } from "react-toastify";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
|
||||
export const ProfileEditSocialModal = (props: {
|
||||
isOpen: boolean;
|
||||
|
@ -22,6 +24,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
ttPage: "",
|
||||
});
|
||||
const { mutate } = useSWRConfig();
|
||||
const theme = useThemeMode();
|
||||
|
||||
function _addUrl(username: string, social: string) {
|
||||
if (!username) {
|
||||
|
@ -52,37 +55,51 @@ export const ProfileEditSocialModal = (props: {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${ENDPOINTS.user.settings.socials.info}?token=${props.token}`)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
setSocials({
|
||||
vkPage: data.vk_page,
|
||||
tgPage: data.tg_page,
|
||||
discordPage: data.discord_page,
|
||||
instPage: data.inst_page,
|
||||
ttPage: data.tt_page,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.isOpen]);
|
||||
async function _fetchSettings() {
|
||||
setLoading(true);
|
||||
|
||||
const { data, error } = await tryCatchAPI(
|
||||
fetch(`${ENDPOINTS.user.settings.socials.info}?token=${props.token}`)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
toast.error("Ошибка получения соц. сетей", {
|
||||
type: "error",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
props.setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSocials({
|
||||
vkPage: data.vk_page,
|
||||
tgPage: data.tg_page,
|
||||
discordPage: data.discord_page,
|
||||
instPage: data.inst_page,
|
||||
ttPage: data.tt_page,
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
_fetchSettings();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.isOpen]);
|
||||
|
||||
function handleInput(e: any) {
|
||||
const social = {
|
||||
...socials,
|
||||
[e.target.name]: e.target.value
|
||||
}
|
||||
[e.target.name]: e.target.value,
|
||||
};
|
||||
setSocials(social);
|
||||
}
|
||||
|
||||
function _setSocialSetting() {
|
||||
const data = {
|
||||
async function _setSocialSetting() {
|
||||
const body = {
|
||||
vkPage: _removeUrl(socials.vkPage),
|
||||
tgPage: _removeUrl(socials.tgPage),
|
||||
discordPage: _removeUrl(socials.discordPage),
|
||||
|
@ -91,28 +108,53 @@ export const ProfileEditSocialModal = (props: {
|
|||
};
|
||||
|
||||
setUpdating(true);
|
||||
fetch(`${ENDPOINTS.user.settings.socials.edit}?token=${props.token}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setUpdating(false);
|
||||
props.setIsOpen(false);
|
||||
} else {
|
||||
new Error("failed to send data");
|
||||
}
|
||||
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}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setUpdating(false);
|
||||
);
|
||||
|
||||
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(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setUpdating(false);
|
||||
props.setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -128,12 +170,11 @@ export const ProfileEditSocialModal = (props: {
|
|||
Укажите ссылки на свои социальные сети, чтобы другие пользователи
|
||||
могли с вами связаться
|
||||
</p>
|
||||
{loading ? (
|
||||
{loading ?
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
: <div className="flex flex-col gap-4 py-4">
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="vk-page" value="ВКонтакте" />
|
||||
|
@ -195,7 +236,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
|
@ -205,7 +246,11 @@ export const ProfileEditSocialModal = (props: {
|
|||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => props.setIsOpen(false)}
|
||||
disabled={updating}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Modal, Textarea } from "flowbite-react";
|
||||
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { toast } from "react-toastify";
|
||||
import { tryCatchAPI } from "#/api/utils";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
|
||||
export const ProfileEditStatusModal = (props: {
|
||||
isOpen: boolean;
|
||||
|
@ -17,6 +20,8 @@ export const ProfileEditStatusModal = (props: {
|
|||
const [_status, _setStatus] = useState("");
|
||||
const [_stringLength, _setStringLength] = useState(0);
|
||||
const { mutate } = useSWRConfig();
|
||||
const theme = useThemeMode();
|
||||
const userStore = useUserStore();
|
||||
|
||||
useEffect(() => {
|
||||
_setStatus(props.status);
|
||||
|
@ -29,33 +34,59 @@ export const ProfileEditStatusModal = (props: {
|
|||
_setStringLength(e.target.value.length);
|
||||
}
|
||||
|
||||
function _setStatusSetting() {
|
||||
async function _setStatusSetting() {
|
||||
setLoading(true);
|
||||
fetch(`${ENDPOINTS.user.settings.status}?token=${props.token}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: _status,
|
||||
}),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setLoading(false);
|
||||
props.setStatus(_status);
|
||||
props.setIsOpen(false);
|
||||
} else {
|
||||
new Error("failed to send data");
|
||||
}
|
||||
|
||||
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}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: _status,
|
||||
}),
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setLoading(false);
|
||||
);
|
||||
|
||||
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(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
userStore.checkAuth();
|
||||
setLoading(false);
|
||||
props.setIsOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -82,7 +113,13 @@ export const ProfileEditStatusModal = (props: {
|
|||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="blue" onClick={() => _setStatusSetting()} disabled={loading}>Сохранить</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => _setStatusSetting()}
|
||||
disabled={loading}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
FlowbiteCarouselControlTheme,
|
||||
} from "flowbite-react";
|
||||
import Image from "next/image";
|
||||
import { unixToDate } from "#/api/utils";
|
||||
import { unixToDate, useSWRfetcher } from "#/api/utils";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
|
@ -95,7 +95,6 @@ const ProfileReleaseRatingsModal = (props: {
|
|||
profile_id: number;
|
||||
token: string | null;
|
||||
}) => {
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const [currentRef, setCurrentRef] = useState<any>(null);
|
||||
const modalRef = useCallback((ref) => {
|
||||
setCurrentRef(ref);
|
||||
|
@ -110,23 +109,9 @@ const ProfileReleaseRatingsModal = (props: {
|
|||
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(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -138,7 +123,6 @@ const ProfileReleaseRatingsModal = (props: {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -170,8 +154,8 @@ const ProfileReleaseRatingsModal = (props: {
|
|||
onScroll={handleScroll}
|
||||
ref={modalRef}
|
||||
>
|
||||
{!isLoadingEnd && isLoading && <Spinner />}
|
||||
{isLoadingEnd && !isLoading && content.length > 0 ? (
|
||||
{isLoading && <Spinner />}
|
||||
{content && content.length > 0 ? (
|
||||
content.map((release) => {
|
||||
return (
|
||||
<Link
|
||||
|
|
|
@ -13,7 +13,7 @@ export const ReleaseInfoBasics = (props: {
|
|||
const [isFullDescription, setIsFullDescription] = useState(false);
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<Card className="h-full row-span-2">
|
||||
<div className="flex flex-col w-full h-full gap-4 lg:flex-row">
|
||||
<Image
|
||||
className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700"
|
||||
|
|
|
@ -28,7 +28,7 @@ export const ReleaseInfoInfo = (props: {
|
|||
genres: string;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<Card>
|
||||
<Table>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
|
|
|
@ -3,6 +3,9 @@ import { ENDPOINTS } from "#/api/config";
|
|||
import Link from "next/link";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { tryCatchAPI, useSWRfetcher } from "#/api/utils";
|
||||
import { toast } from "react-toastify";
|
||||
import { useThemeMode } from "flowbite-react";
|
||||
|
||||
const lists = [
|
||||
{ list: 0, name: "Не смотрю" },
|
||||
|
@ -31,25 +34,108 @@ export const ReleaseInfoUserList = (props: {
|
|||
}) => {
|
||||
const [AddReleaseToCollectionModalOpen, setAddReleaseToCollectionModalOpen] =
|
||||
useState(false);
|
||||
const [favButtonDisabled, setFavButtonDisabled] = useState(false);
|
||||
const [listEventDisabled, setListEventDisabled] = useState(false);
|
||||
const theme = useThemeMode();
|
||||
|
||||
function _addToFavorite() {
|
||||
if (props.token) {
|
||||
props.setIsFavorite(!props.isFavorite);
|
||||
if (props.isFavorite) {
|
||||
fetch(
|
||||
`${ENDPOINTS.user.favorite}/delete/${props.release_id}?token=${props.token}`
|
||||
);
|
||||
} else {
|
||||
fetch(
|
||||
`${ENDPOINTS.user.favorite}/add/${props.release_id}?token=${props.token}`
|
||||
);
|
||||
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) {
|
||||
let url = `${ENDPOINTS.user.favorite}/add/${props.release_id}?token=${props.token}`;
|
||||
if (props.isFavorite) {
|
||||
url = `${ENDPOINTS.user.favorite}/delete/${props.release_id}?token=${props.token}`;
|
||||
}
|
||||
_setFav(url);
|
||||
}
|
||||
}
|
||||
|
||||
function _addToList(list: number) {
|
||||
if (props.token) {
|
||||
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);
|
||||
fetch(
|
||||
}
|
||||
|
||||
if (props.token) {
|
||||
_setList(
|
||||
`${ENDPOINTS.user.bookmark}/add/${list}/${props.release_id}?token=${props.token}`
|
||||
);
|
||||
}
|
||||
|
@ -58,7 +144,7 @@ export const ReleaseInfoUserList = (props: {
|
|||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Button color={"blue"} size="sm" className="w-full lg:w-auto ">
|
||||
<Button color={"blue"} size="sm" className={props.token ? "w-full sm:w-[49%] lg:w-full 2xl:w-[60%]" : "w-full"}>
|
||||
<Link href={`/release/${props.release_id}/collections`}>
|
||||
Показать в коллекциях{" "}
|
||||
<span className="p-1 ml-1 text-gray-500 rounded bg-gray-50">
|
||||
|
@ -70,14 +156,14 @@ export const ReleaseInfoUserList = (props: {
|
|||
<Button
|
||||
color={"blue"}
|
||||
size="sm"
|
||||
className="w-full lg:w-auto lg:flex-1"
|
||||
className="w-full sm:w-1/2 lg:w-full 2xl:w-[39%]"
|
||||
onClick={() => setAddReleaseToCollectionModalOpen(true)}
|
||||
>
|
||||
В коллекцию{" "}
|
||||
<span className="w-6 h-6 iconify mdi--bookmark-add "></span>
|
||||
</Button>
|
||||
)}
|
||||
{props.token ? (
|
||||
{props.token ?
|
||||
<>
|
||||
<Dropdown
|
||||
label={lists[props.userList].name}
|
||||
|
@ -85,6 +171,7 @@ export const ReleaseInfoUserList = (props: {
|
|||
theme={DropdownTheme}
|
||||
color="blue"
|
||||
size="sm"
|
||||
disabled={listEventDisabled}
|
||||
>
|
||||
{lists.map((list) => (
|
||||
<Dropdown.Item
|
||||
|
@ -101,6 +188,7 @@ export const ReleaseInfoUserList = (props: {
|
|||
_addToFavorite();
|
||||
}}
|
||||
size="sm"
|
||||
disabled={favButtonDisabled}
|
||||
>
|
||||
<span
|
||||
className={`iconify w-6 h-6 ${
|
||||
|
@ -109,9 +197,11 @@ export const ReleaseInfoUserList = (props: {
|
|||
></span>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p>Войдите что-бы добавить в список, избранное или коллекцию</p>
|
||||
)}
|
||||
: <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>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<AddReleaseToCollectionModal
|
||||
isOpen={AddReleaseToCollectionModalOpen}
|
||||
|
@ -124,20 +214,6 @@ 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: {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isopen: boolean) => void;
|
||||
|
@ -150,10 +226,11 @@ const AddReleaseToCollectionModal = (props: {
|
|||
if (previousPageData && !previousPageData.content.length) return null;
|
||||
return `${ENDPOINTS.collection.userCollections}/${props.profile_id}/${pageIndex}?token=${props.token}`;
|
||||
};
|
||||
const theme = useThemeMode();
|
||||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -188,28 +265,53 @@ const AddReleaseToCollectionModal = (props: {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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));
|
||||
|
||||
function _addToCollection(collection_id: number) {
|
||||
if (props.token) {
|
||||
fetch(
|
||||
`${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);
|
||||
}
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
if (props.token) {
|
||||
_ToCollection(
|
||||
`${ENDPOINTS.collection.addRelease}/${collection.id}?release_id=${props.release_id}&token=${props.token}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,25 +327,25 @@ const AddReleaseToCollectionModal = (props: {
|
|||
onScroll={handleScroll}
|
||||
ref={modalRef}
|
||||
>
|
||||
{content && content.length > 0
|
||||
? content.map((collection) => (
|
||||
<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%] "
|
||||
style={{
|
||||
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}`}
|
||||
onClick={() => _addToCollection(collection.id)}
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 gap-1 p-2">
|
||||
<p className="text-xl font-bold text-white">
|
||||
{collection.title}
|
||||
</p>
|
||||
<p className="text-gray-400">{collection.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
: "коллекций не найдено"}
|
||||
{content && content.length > 0 ?
|
||||
content.map((collection) => (
|
||||
<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%] "
|
||||
style={{
|
||||
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}`}
|
||||
onClick={() => _addToCollection(collection)}
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 gap-1 p-2">
|
||||
<p className="text-xl font-bold text-white">
|
||||
{collection.title}
|
||||
</p>
|
||||
<p className="text-gray-400">{collection.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
: "коллекций не найдено"}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -36,7 +36,7 @@ export const ReleaseLink169 = (props: any) => {
|
|||
<Image
|
||||
src={props.image}
|
||||
fill={true}
|
||||
alt={props.title}
|
||||
alt={props.title || ""}
|
||||
className="-z-[1] object-cover"
|
||||
sizes="
|
||||
(max-width: 768px) 300px,
|
||||
|
|
|
@ -33,7 +33,7 @@ export const ReleaseLink169Poster = (props: any) => {
|
|||
src={props.image}
|
||||
height={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"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -43,7 +43,7 @@ export const ReleaseLink169Related = (props: any) => {
|
|||
src={props.image}
|
||||
height={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"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -590,7 +590,7 @@ export default function Page(props: { children: any, className?: string }) {
|
|||
<!-- Skip opening Button -->
|
||||
|
||||
<media-seek-forward-button class="media-button" seekoffset="90">
|
||||
<svg slot="icon" width="256" height="256" viewBox="-75 -75 400 400">
|
||||
<svg slot="icon" width="256" height="256" viewBox="-65 -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" />
|
||||
</svg>
|
||||
</media-seek-forward-button>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Card } from "flowbite-react";
|
||||
import { Button, Card } from "flowbite-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
|
||||
|
@ -14,6 +14,7 @@ import HlsVideo from "hls-video-element/react";
|
|||
import VideoJS from "videojs-video-element/react";
|
||||
import MediaThemeSutro from "./MediaThemeSutro";
|
||||
import { getAnonEpisodesWatched } from "./ReleasePlayer";
|
||||
import { tryCatchPlayer, tryCatchAPI } from "#/api/utils";
|
||||
|
||||
export const ReleasePlayerCustom = (props: {
|
||||
id: number;
|
||||
|
@ -38,157 +39,207 @@ export const ReleasePlayerCustom = (props: {
|
|||
useCustom: false,
|
||||
});
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [playerError, setPlayerError] = useState(null);
|
||||
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const playerPreferenceStore = useUserPlayerPreferencesStore();
|
||||
const preferredVO = playerPreferenceStore.getPreferredVoiceover(props.id);
|
||||
const preferredSource = playerPreferenceStore.getPreferredPlayer(props.id);
|
||||
|
||||
const _fetchVoiceover = async (release_id: number) => {
|
||||
let url = `${ENDPOINTS.release.episode}/${release_id}`;
|
||||
if (props.token) {
|
||||
url += `?token=${props.token}`;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
async function _fetchAPI(
|
||||
url: string,
|
||||
onErrorMsg: string,
|
||||
onErrorCodes?: Record<number, string>
|
||||
) {
|
||||
const { data, error } = await tryCatchAPI(fetch(url));
|
||||
if (error) {
|
||||
let errorDetail = "Мы правда не знаем что произошло...";
|
||||
|
||||
const _fetchSource = async (release_id: number, voiceover_id: number) => {
|
||||
const response = await fetch(
|
||||
`${ENDPOINTS.release.episode}/${release_id}/${voiceover_id}`
|
||||
);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
setPlayerError({
|
||||
message: onErrorMsg,
|
||||
detail: errorDetail,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
async function _fetchPlayer(url: string) {
|
||||
const { data, error } = (await tryCatchPlayer(fetch(url))) as any;
|
||||
if (error) {
|
||||
let errorDetail = "Мы правда не знаем что произошло...";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const _fetchKodikManifest = async (url: string) => {
|
||||
const response = await fetch(
|
||||
const data = await _fetchPlayer(
|
||||
`https://anix-player.wah.su/?url=${url}&player=kodik`
|
||||
);
|
||||
const data = await response.json();
|
||||
let lowQualityLink = data.links["360"][0].src;
|
||||
if (lowQualityLink.includes("https://")) {
|
||||
lowQualityLink = lowQualityLink.replace("https://", "//");
|
||||
if (data) {
|
||||
let lowQualityLink = data.links["360"][0].src;
|
||||
if (lowQualityLink.includes("https://")) {
|
||||
lowQualityLink = lowQualityLink.replace("https://", "//");
|
||||
}
|
||||
let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`;
|
||||
let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`;
|
||||
|
||||
if (lowQualityLink.includes("animetvseries")) {
|
||||
let blobTxt = "#EXTM3U\n";
|
||||
|
||||
if (data.links.hasOwnProperty("240")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n";
|
||||
!data.links["240"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["240"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["240"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("360")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n";
|
||||
!data.links["360"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["360"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["360"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("480")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n";
|
||||
!data.links["480"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["480"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["480"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("720")) {
|
||||
blobTxt +=
|
||||
"#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
|
||||
!data.links["720"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["720"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["720"][0].src}\n`);
|
||||
}
|
||||
|
||||
let file = new File([blobTxt], "manifest.m3u8", {
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
manifest = URL.createObjectURL(file);
|
||||
}
|
||||
return { manifest, poster };
|
||||
}
|
||||
let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`;
|
||||
let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`;
|
||||
|
||||
if (lowQualityLink.includes("animetvseries")) {
|
||||
let blobTxt = "#EXTM3U\n";
|
||||
|
||||
if (data.links.hasOwnProperty("240")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n";
|
||||
!data.links["240"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["240"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["240"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("360")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n";
|
||||
!data.links["360"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["360"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["360"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("480")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n";
|
||||
!data.links["480"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["480"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["480"][0].src}\n`);
|
||||
}
|
||||
|
||||
if (data.links.hasOwnProperty("720")) {
|
||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
|
||||
!data.links["720"][0].src.startsWith("https:") ?
|
||||
(blobTxt += `https:${data.links["720"][0].src}\n`)
|
||||
: (blobTxt += `${data.links["720"][0].src}\n`);
|
||||
}
|
||||
|
||||
let file = new File([blobTxt], "manifest.m3u8", {
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
manifest = URL.createObjectURL(file);
|
||||
}
|
||||
return { manifest, poster };
|
||||
return { manifest: null, poster: null };
|
||||
};
|
||||
|
||||
const _fetchAnilibriaManifest = async (url: string) => {
|
||||
const id = url.split("?id=")[1].split("&ep=")[0];
|
||||
const data = await _fetchPlayer(
|
||||
`https://api.anilibria.tv/v3/title?id=${id}`
|
||||
);
|
||||
if (data) {
|
||||
const host = `https://${data.player.host}`;
|
||||
const ep = data.player.list[episode.selected.position];
|
||||
|
||||
const response = await fetch(`https://api.anilibria.tv/v3/title?id=${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
const host = `https://${data.player.host}`;
|
||||
const ep = data.player.list[episode.selected.position];
|
||||
|
||||
const blobTxt = `#EXTM3U\n${ep.hls.sd && `#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n${host}${ep.hls.sd}\n`}${ep.hls.hd && `#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n${host}${ep.hls.hd}\n`}${ep.hls.fhd && `#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n${host}${ep.hls.fhd}\n`}`;
|
||||
let file = new File([blobTxt], "manifest.m3u8", {
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
let manifest = URL.createObjectURL(file);
|
||||
let poster = `https://anixart.libria.fun${ep.preview}`;
|
||||
return { manifest, poster };
|
||||
const blobTxt = `#EXTM3U\n${ep.hls.sd && `#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n${host}${ep.hls.sd}\n`}${ep.hls.hd && `#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n${host}${ep.hls.hd}\n`}${ep.hls.fhd && `#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n${host}${ep.hls.fhd}\n`}`;
|
||||
let file = new File([blobTxt], "manifest.m3u8", {
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
let manifest = URL.createObjectURL(file);
|
||||
let poster = `https://anixart.libria.fun${ep.preview}`;
|
||||
return { manifest, poster };
|
||||
}
|
||||
return { manifest: null, poster: null };
|
||||
};
|
||||
|
||||
const _fetchSibnetManifest = async (url: string) => {
|
||||
const response = await fetch(
|
||||
const data = await _fetchPlayer(
|
||||
`https://sibnet.anix-player.wah.su/?url=${url}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
let manifest = data.video;
|
||||
let poster = data.poster;
|
||||
return { manifest, poster };
|
||||
if (data) {
|
||||
let manifest = data.video;
|
||||
let poster = data.poster;
|
||||
return { manifest, poster };
|
||||
}
|
||||
return { manifest: null, poster: null };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
const vo = await _fetchVoiceover(props.id);
|
||||
const selectedVO =
|
||||
vo.types.find((voiceover: any) => voiceover.name === preferredVO) ||
|
||||
vo.types[0];
|
||||
setVoiceover({
|
||||
selected: selectedVO,
|
||||
available: vo.types,
|
||||
});
|
||||
let url = `${ENDPOINTS.release.episode}/${props.id}`;
|
||||
if (props.token) {
|
||||
url += `?token=${props.token}`;
|
||||
}
|
||||
const vo = await _fetchAPI(
|
||||
url,
|
||||
"Не удалось получить информацию о озвучках",
|
||||
{ 1: "Просмотр запрещён" }
|
||||
);
|
||||
if (vo) {
|
||||
const selectedVO =
|
||||
vo.types.find((voiceover: any) => voiceover.name === preferredVO) ||
|
||||
vo.types[0];
|
||||
setVoiceover({
|
||||
selected: selectedVO,
|
||||
available: vo.types,
|
||||
});
|
||||
}
|
||||
};
|
||||
__getInfo();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
const src = await _fetchSource(props.id, voiceover.selected.id);
|
||||
const selectedSrc =
|
||||
src.sources.find((source: any) => source.name === preferredSource) ||
|
||||
src.sources[0];
|
||||
if (selectedSrc.episodes_count == 0) {
|
||||
const remSources = src.sources.filter(
|
||||
(source: any) => source.id !== selectedSrc.id
|
||||
);
|
||||
let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}`;
|
||||
const src = await _fetchAPI(
|
||||
url,
|
||||
"Не удалось получить информацию о источниках"
|
||||
);
|
||||
if (src) {
|
||||
const selectedSrc =
|
||||
src.sources.find((source: any) => source.name === preferredSource) ||
|
||||
src.sources[0];
|
||||
if (selectedSrc.episodes_count == 0) {
|
||||
const remSources = src.sources.filter(
|
||||
(source: any) => source.id !== selectedSrc.id
|
||||
);
|
||||
setSource({
|
||||
selected: remSources[0],
|
||||
available: remSources,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSource({
|
||||
selected: remSources[0],
|
||||
available: remSources,
|
||||
selected: selectedSrc,
|
||||
available: src.sources,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSource({
|
||||
selected: selectedSrc,
|
||||
available: src.sources,
|
||||
});
|
||||
};
|
||||
if (voiceover.selected) {
|
||||
__getInfo();
|
||||
|
@ -197,34 +248,38 @@ export const ReleasePlayerCustom = (props: {
|
|||
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
const episodes = await _fetchEpisode(
|
||||
props.id,
|
||||
voiceover.selected.id,
|
||||
source.selected.id
|
||||
let url = `${ENDPOINTS.release.episode}/${props.id}/${voiceover.selected.id}/${source.selected.id}`;
|
||||
if (props.token) {
|
||||
url += `?token=${props.token}`;
|
||||
}
|
||||
const episodes = await _fetchAPI(
|
||||
url,
|
||||
"Не удалось получить информацию о эпизодах"
|
||||
);
|
||||
if (episodes) {
|
||||
let anonEpisodesWatched = getAnonEpisodesWatched(
|
||||
props.id,
|
||||
source.selected.id,
|
||||
voiceover.selected.id
|
||||
);
|
||||
let lastEpisodeWatched = Math.max.apply(
|
||||
0,
|
||||
Object.keys(
|
||||
anonEpisodesWatched[props.id][source.selected.id][
|
||||
voiceover.selected.id
|
||||
]
|
||||
)
|
||||
);
|
||||
let selectedEpisode =
|
||||
episodes.episodes.find(
|
||||
(episode: any) => episode.position == lastEpisodeWatched
|
||||
) || episodes.episodes[0];
|
||||
|
||||
let anonEpisodesWatched = getAnonEpisodesWatched(
|
||||
props.id,
|
||||
source.selected.id,
|
||||
voiceover.selected.id
|
||||
);
|
||||
let lastEpisodeWatched = Math.max.apply(
|
||||
0,
|
||||
Object.keys(
|
||||
anonEpisodesWatched[props.id][source.selected.id][
|
||||
voiceover.selected.id
|
||||
]
|
||||
)
|
||||
);
|
||||
let selectedEpisode =
|
||||
episodes.episodes.find(
|
||||
(episode: any) => episode.position == lastEpisodeWatched
|
||||
) || episodes.episodes[0];
|
||||
|
||||
setEpisode({
|
||||
selected: selectedEpisode,
|
||||
available: episodes.episodes,
|
||||
});
|
||||
setEpisode({
|
||||
selected: selectedEpisode,
|
||||
available: episodes.episodes,
|
||||
});
|
||||
}
|
||||
};
|
||||
if (source.selected) {
|
||||
__getInfo();
|
||||
|
@ -237,36 +292,45 @@ export const ReleasePlayerCustom = (props: {
|
|||
const { manifest, poster } = await _fetchKodikManifest(
|
||||
episode.selected.url
|
||||
);
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "hls",
|
||||
});
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "hls",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (source.selected.name == "Libria") {
|
||||
const { manifest, poster } = await _fetchAnilibriaManifest(
|
||||
episode.selected.url
|
||||
);
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "hls",
|
||||
});
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "hls",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (source.selected.name == "Sibnet") {
|
||||
const { manifest, poster } = await _fetchSibnetManifest(
|
||||
episode.selected.url
|
||||
);
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "mp4",
|
||||
});
|
||||
if (manifest) {
|
||||
SetPlayerProps({
|
||||
src: manifest,
|
||||
poster: poster,
|
||||
useCustom: true,
|
||||
type: "mp4",
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
SetPlayerProps({
|
||||
|
@ -275,6 +339,7 @@ export const ReleasePlayerCustom = (props: {
|
|||
useCustom: false,
|
||||
type: null,
|
||||
});
|
||||
setIsLoading(false);
|
||||
};
|
||||
if (episode.selected) {
|
||||
__getInfo();
|
||||
|
@ -282,32 +347,48 @@ export const ReleasePlayerCustom = (props: {
|
|||
}, [episode.selected]);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<VoiceoverSelector
|
||||
availableVoiceover={voiceover.available}
|
||||
voiceover={voiceover.selected}
|
||||
setVoiceover={setVoiceover}
|
||||
release_id={props.id}
|
||||
/>
|
||||
<SourceSelector
|
||||
availableSource={source.available}
|
||||
source={source.selected}
|
||||
setSource={setSource}
|
||||
release_id={props.id}
|
||||
/>
|
||||
</div>
|
||||
{playerProps.useCustom ?
|
||||
<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]">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{voiceover.selected && (
|
||||
<VoiceoverSelector
|
||||
availableVoiceover={voiceover.available}
|
||||
voiceover={voiceover.selected}
|
||||
setVoiceover={setVoiceover}
|
||||
release_id={props.id}
|
||||
/>
|
||||
)}
|
||||
{source.selected && (
|
||||
<SourceSelector
|
||||
availableSource={source.available}
|
||||
source={source.selected}
|
||||
setSource={setSource}
|
||||
release_id={props.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{playerProps.type == "hls" ?
|
||||
<HlsVideo
|
||||
|
@ -334,7 +415,27 @@ export const ReleasePlayerCustom = (props: {
|
|||
></VideoJS>
|
||||
}
|
||||
</MediaThemeSutro>
|
||||
: <iframe src={playerProps.src} className="w-full aspect-video" />}
|
||||
: <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" />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{episode.selected && source.selected && voiceover.selected && (
|
||||
<EpisodeSelector
|
||||
availableEpisodes={episode.available}
|
||||
episode={episode.selected}
|
||||
|
@ -344,8 +445,8 @@ export const ReleasePlayerCustom = (props: {
|
|||
voiceover={voiceover.selected}
|
||||
token={props.token}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export const UserSection = (props: { sectionTitle?: string; content: any }) => {
|
|||
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">
|
||||
<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">
|
||||
{user.login}
|
||||
</h5>
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
import useSWR from "swr";
|
||||
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
const fetcher = (...args: any) =>
|
||||
fetch([...args] as any).then((res) => res.json());
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { usePreferencesStore } from "#/store/preferences";
|
||||
import { BookmarksList } from "#/api/utils";
|
||||
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
@ -35,7 +33,7 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
function useFetchReleases(listName: string) {
|
||||
let url: string;
|
||||
if (preferenceStore.params.skipToCategory.enabled) {
|
||||
return [null];
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (props.profile_id) {
|
||||
|
@ -50,8 +48,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { data } = useSWR(url, fetcher);
|
||||
return [data];
|
||||
const { data, error } = useSWR(url, useSWRfetcher);
|
||||
console.log(data, error);
|
||||
return [data, error];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -61,11 +60,11 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authState, token]);
|
||||
|
||||
const [watchingData] = useFetchReleases("watching");
|
||||
const [plannedData] = useFetchReleases("planned");
|
||||
const [watchedData] = useFetchReleases("watched");
|
||||
const [delayedData] = useFetchReleases("delayed");
|
||||
const [abandonedData] = useFetchReleases("abandoned");
|
||||
const [watchingData, watchingError] = useFetchReleases("watching");
|
||||
const [plannedData, plannedError] = useFetchReleases("planned");
|
||||
const [watchedData, watchedError] = useFetchReleases("watched");
|
||||
const [delayedData, delayedError] = useFetchReleases("delayed");
|
||||
const [abandonedData, abandonedError] = useFetchReleases("abandoned");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -85,9 +84,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
<ReleaseCourusel
|
||||
sectionTitle="Смотрю"
|
||||
showAllLink={
|
||||
!props.profile_id
|
||||
? "/bookmarks/watching"
|
||||
: `/profile/${props.profile_id}/bookmarks/watching`
|
||||
!props.profile_id ?
|
||||
"/bookmarks/watching"
|
||||
: `/profile/${props.profile_id}/bookmarks/watching`
|
||||
}
|
||||
content={watchingData.content}
|
||||
/>
|
||||
|
@ -96,9 +95,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
<ReleaseCourusel
|
||||
sectionTitle="В планах"
|
||||
showAllLink={
|
||||
!props.profile_id
|
||||
? "/bookmarks/planned"
|
||||
: `/profile/${props.profile_id}/bookmarks/planned`
|
||||
!props.profile_id ? "/bookmarks/planned" : (
|
||||
`/profile/${props.profile_id}/bookmarks/planned`
|
||||
)
|
||||
}
|
||||
content={plannedData.content}
|
||||
/>
|
||||
|
@ -107,9 +106,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
<ReleaseCourusel
|
||||
sectionTitle="Просмотрено"
|
||||
showAllLink={
|
||||
!props.profile_id
|
||||
? "/bookmarks/watched"
|
||||
: `/profile/${props.profile_id}/bookmarks/watched`
|
||||
!props.profile_id ? "/bookmarks/watched" : (
|
||||
`/profile/${props.profile_id}/bookmarks/watched`
|
||||
)
|
||||
}
|
||||
content={watchedData.content}
|
||||
/>
|
||||
|
@ -118,9 +117,9 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
<ReleaseCourusel
|
||||
sectionTitle="Отложено"
|
||||
showAllLink={
|
||||
!props.profile_id
|
||||
? "/bookmarks/delayed"
|
||||
: `/profile/${props.profile_id}/bookmarks/delayed`
|
||||
!props.profile_id ? "/bookmarks/delayed" : (
|
||||
`/profile/${props.profile_id}/bookmarks/delayed`
|
||||
)
|
||||
}
|
||||
content={delayedData.content}
|
||||
/>
|
||||
|
@ -131,13 +130,28 @@ export function BookmarksPage(props: { profile_id?: number }) {
|
|||
<ReleaseCourusel
|
||||
sectionTitle="Заброшено"
|
||||
showAllLink={
|
||||
!props.profile_id
|
||||
? "/bookmarks/abandoned"
|
||||
: `/profile/${props.profile_id}/bookmarks/abandoned`
|
||||
!props.profile_id ?
|
||||
"/bookmarks/abandoned"
|
||||
: `/profile/${props.profile_id}/bookmarks/abandoned`
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ import { Spinner } from "#/components/Spinner/Spinner";
|
|||
import { useState, useEffect } from "react";
|
||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||
import { useUserStore } from "../store/auth";
|
||||
import { Dropdown, Button, Tabs } from "flowbite-react";
|
||||
import { Dropdown, Button } from "flowbite-react";
|
||||
import { sort } from "./common";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { BookmarksList } from "#/api/utils";
|
||||
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const DropdownTheme = {
|
||||
|
@ -17,25 +17,10 @@ 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) {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const authState = useUserStore((state) => state.state);
|
||||
const [selectedSort, setSelectedSort] = useState(0);
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const [searchVal, setSearchVal] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -61,7 +46,7 @@ export function BookmarksCategoryPage(props: any) {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -73,7 +58,6 @@ export function BookmarksCategoryPage(props: any) {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -92,9 +76,31 @@ export function BookmarksCategoryPage(props: any) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
<>
|
||||
{!props.profile_id ? (
|
||||
{!props.profile_id ?
|
||||
<form
|
||||
className="flex-1 max-w-full mx-4"
|
||||
onSubmit={(e) => {
|
||||
|
@ -143,9 +149,7 @@ export function BookmarksCategoryPage(props: any) {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
: ""}
|
||||
<div className="m-4 overflow-auto">
|
||||
<Button.Group>
|
||||
<Button
|
||||
|
@ -154,9 +158,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
color="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
props.profile_id
|
||||
? `/profile/${props.profile_id}/bookmarks/watching`
|
||||
: "/bookmarks/watching"
|
||||
props.profile_id ?
|
||||
`/profile/${props.profile_id}/bookmarks/watching`
|
||||
: "/bookmarks/watching"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -168,9 +172,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
color="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
props.profile_id
|
||||
? `/profile/${props.profile_id}/bookmarks/planned`
|
||||
: "/bookmarks/planned"
|
||||
props.profile_id ?
|
||||
`/profile/${props.profile_id}/bookmarks/planned`
|
||||
: "/bookmarks/planned"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -182,9 +186,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
color="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
props.profile_id
|
||||
? `/profile/${props.profile_id}/bookmarks/watched`
|
||||
: "/bookmarks/watched"
|
||||
props.profile_id ?
|
||||
`/profile/${props.profile_id}/bookmarks/watched`
|
||||
: "/bookmarks/watched"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -196,9 +200,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
color="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
props.profile_id
|
||||
? `/profile/${props.profile_id}/bookmarks/delayed`
|
||||
: "/bookmarks/delayed"
|
||||
props.profile_id ?
|
||||
`/profile/${props.profile_id}/bookmarks/delayed`
|
||||
: "/bookmarks/delayed"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -210,9 +214,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
color="light"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
props.profile_id
|
||||
? `/profile/${props.profile_id}/bookmarks/abandoned`
|
||||
: "/bookmarks/abandoned"
|
||||
props.profile_id ?
|
||||
`/profile/${props.profile_id}/bookmarks/abandoned`
|
||||
: "/bookmarks/abandoned"
|
||||
)
|
||||
}
|
||||
>
|
||||
|
@ -236,9 +240,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
|
||||
<span
|
||||
className={`w-6 h-6 iconify ${
|
||||
sort.values[index].value.split("_")[1] == "descending"
|
||||
? sort.descendingIcon
|
||||
: sort.ascendingIcon
|
||||
sort.values[index].value.split("_")[1] == "descending" ?
|
||||
sort.descendingIcon
|
||||
: sort.ascendingIcon
|
||||
}`}
|
||||
></span>
|
||||
{item.name}
|
||||
|
@ -246,20 +250,15 @@ export function BookmarksCategoryPage(props: any) {
|
|||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
{content && content.length > 0 ? (
|
||||
{content && content.length > 0 ?
|
||||
<ReleaseSection content={content} />
|
||||
) : !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">
|
||||
: <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>
|
||||
<p>
|
||||
В списке {props.SectionTitleMapping[props.slug]} пока ничего нет...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
{data &&
|
||||
data[data.length - 1].current_page <
|
||||
data[data.length - 1].total_page_count && (
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
import useSWR from "swr";
|
||||
import { CollectionCourusel } from "#/components/CollectionCourusel/CollectionCourusel";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
const fetcher = (...args: any) =>
|
||||
fetch([...args] as any).then((res) => res.json());
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
@ -25,12 +24,15 @@ export function CollectionsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const { data } = useSWR(url, fetcher);
|
||||
return [data];
|
||||
const { data, error } = useSWR(url, useSWRfetcher);
|
||||
return [data, error];
|
||||
}
|
||||
|
||||
const [userCollections] = useFetchReleases("userCollections");
|
||||
const [favoriteCollections] = useFetchReleases("userFavoriteCollections");
|
||||
const [userCollections, userCollectionsError] =
|
||||
useFetchReleases("userCollections");
|
||||
const [favoriteCollections, favoriteCollectionsError] = useFetchReleases(
|
||||
"userFavoriteCollections"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (userStore.state === "finished" && !userStore.token) {
|
||||
|
@ -114,6 +116,18 @@ export function CollectionsPage() {
|
|||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,20 +8,7 @@ import { useUserStore } from "../store/auth";
|
|||
import { Button } from "flowbite-react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
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();
|
||||
};
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
export function CollectionsFullPage(props: {
|
||||
type: "favorites" | "profile" | "release";
|
||||
|
@ -30,13 +17,12 @@ export function CollectionsFullPage(props: {
|
|||
release_id?: number;
|
||||
}) {
|
||||
const userStore = useUserStore();
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||
if (previousPageData && !previousPageData.content.length) return null;
|
||||
|
||||
let url;
|
||||
let url: string;
|
||||
|
||||
if (props.type == "favorites") {
|
||||
url = `${ENDPOINTS.collection.favoriteCollections}/all/${pageIndex}`;
|
||||
|
@ -55,7 +41,7 @@ export function CollectionsFullPage(props: {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -67,7 +53,6 @@ export function CollectionsFullPage(props: {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -90,26 +75,45 @@ export function CollectionsFullPage(props: {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
<>
|
||||
{content && content.length > 0 ? (
|
||||
{content && content.length > 0 ?
|
||||
<CollectionsSection
|
||||
sectionTitle={props.title}
|
||||
content={content}
|
||||
isMyCollections={
|
||||
props.type == "profile" && userStore.user && props.profile_id == userStore.user.id
|
||||
props.type == "profile" &&
|
||||
userStore.user &&
|
||||
props.profile_id == userStore.user.id
|
||||
}
|
||||
/>
|
||||
) : !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">
|
||||
: <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>
|
||||
<p>Тут пока ничего нет...</p>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
{data &&
|
||||
data[data.length - 1].current_page <
|
||||
data[data.length - 1].total_page_count && (
|
||||
|
|
|
@ -13,34 +13,30 @@ import {
|
|||
FileInput,
|
||||
Label,
|
||||
Modal,
|
||||
useThemeMode,
|
||||
} from "flowbite-react";
|
||||
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
|
||||
import { CropModal } from "#/components/CropModal/CropModal";
|
||||
import { b64toBlob } from "#/api/utils";
|
||||
import { b64toBlob, tryCatchAPI } 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();
|
||||
};
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const CreateCollectionPage = () => {
|
||||
const userStore = useUserStore();
|
||||
const searchParams = useSearchParams();
|
||||
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 [imageUrl, setImageUrl] = useState<string>(null);
|
||||
const [tempImageUrl, setTempImageUrl] = useState<string>(null);
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [collectionInfo, setCollectionInfo] = useState({
|
||||
title: "",
|
||||
|
@ -53,7 +49,14 @@ export const CreateCollectionPage = () => {
|
|||
const [addedReleases, setAddedReleases] = useState([]);
|
||||
const [addedReleasesIds, setAddedReleasesIds] = useState([]);
|
||||
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 mode = searchParams.get("mode") || null;
|
||||
|
@ -120,15 +123,29 @@ export const CreateCollectionPage = () => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userStore.user]);
|
||||
|
||||
const handleFileRead = (e, fileReader) => {
|
||||
const content = fileReader.result;
|
||||
setTempImageUrl(content);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (imageModalProps.croppedImage) {
|
||||
setImageUrl(imageModalProps.croppedImage);
|
||||
setImageModalProps({
|
||||
isOpen: false,
|
||||
isActionsDisabled: false,
|
||||
selectedImage: null,
|
||||
croppedImage: null,
|
||||
});
|
||||
}
|
||||
}, [imageModalProps.croppedImage]);
|
||||
|
||||
const handleFilePreview = (file) => {
|
||||
const handleImagePreview = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = (e) => {
|
||||
handleFileRead(e, fileReader);
|
||||
fileReader.onloadend = () => {
|
||||
const content = fileReader.result;
|
||||
setImageModalProps({
|
||||
...imageModalProps,
|
||||
isOpen: true,
|
||||
selectedImage: content,
|
||||
});
|
||||
e.target.value = "";
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
};
|
||||
|
@ -149,25 +166,50 @@ export const CreateCollectionPage = () => {
|
|||
e.preventDefault();
|
||||
|
||||
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 =
|
||||
mode === "edit"
|
||||
? `${ENDPOINTS.collection.edit}/${collection_id}?token=${userStore.token}`
|
||||
: `${ENDPOINTS.collection.create}?token=${userStore.token}`;
|
||||
mode === "edit" ?
|
||||
`${ENDPOINTS.collection.edit}/${collection_id}?token=${userStore.token}`
|
||||
: `${ENDPOINTS.collection.create}?token=${userStore.token}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...collectionInfo,
|
||||
is_private: isPrivate,
|
||||
private: isPrivate,
|
||||
releases: addedReleasesIds,
|
||||
}),
|
||||
});
|
||||
const { data, error } = await tryCatchAPI(
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...collectionInfo,
|
||||
is_private: isPrivate,
|
||||
private: isPrivate,
|
||||
releases: addedReleasesIds,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.code == 5) {
|
||||
alert("Вы превысили допустимый еженедельный лимит создания коллекций!");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -180,33 +222,92 @@ export const CreateCollectionPage = () => {
|
|||
const formData = new FormData();
|
||||
formData.append("image", blob, "cropped.jpg");
|
||||
formData.append("name", "image");
|
||||
const uploadRes = await fetch(
|
||||
`${ENDPOINTS.collection.editImage}/${data.collection.id}?token=${userStore.token}`,
|
||||
|
||||
const tiid = toast.loading(
|
||||
`Обновление обложки коллекции ${collectionInfo.title}...`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
position: "bottom-center",
|
||||
hideProgressBar: true,
|
||||
closeOnClick: false,
|
||||
pauseOnHover: false,
|
||||
draggable: false,
|
||||
theme: theme.mode == "light" ? "light" : "dark",
|
||||
}
|
||||
);
|
||||
const uploadData = await uploadRes.json();
|
||||
|
||||
const { data: imageData, error } = await tryCatchAPI(
|
||||
fetch(
|
||||
`${ENDPOINTS.collection.editImage}/${data.collection.id}?token=${userStore.token}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
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}`);
|
||||
setIsSending(false);
|
||||
}
|
||||
|
||||
if (
|
||||
collectionInfo.title.length >= 10 &&
|
||||
addedReleasesIds.length >= 1 &&
|
||||
userStore.token
|
||||
) {
|
||||
// setIsSending(true);
|
||||
_createCollection();
|
||||
} else if (collectionInfo.title.length < 10) {
|
||||
alert("Необходимо ввести название коллекции не менее 10 символов");
|
||||
} else if (!userStore.token) {
|
||||
alert("Для создания коллекции необходимо войти в аккаунт");
|
||||
} else if (addedReleasesIds.length < 1) {
|
||||
alert("Необходимо добавить хотя бы один релиз в коллекцию");
|
||||
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;
|
||||
}
|
||||
|
||||
_createCollection();
|
||||
}
|
||||
|
||||
function _deleteRelease(release: any) {
|
||||
|
@ -239,47 +340,49 @@ 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"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center max-w-[595px] h-[inherit] rounded-[inherit] pt-5 pb-6 overflow-hidden">
|
||||
{!imageUrl ? (
|
||||
<>
|
||||
<svg
|
||||
className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 16"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-semibold">Нажмите для загрузки</span>{" "}
|
||||
или перетащите файл
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
PNG или JPG (Макс. 600x337 пикселей)
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imageUrl}
|
||||
className="object-cover w-[inherit] h-[inherit]"
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!imageUrl ?
|
||||
<>
|
||||
<svg
|
||||
className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 16"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-semibold">
|
||||
Нажмите для загрузки
|
||||
</span>{" "}
|
||||
или перетащите файл
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
PNG или JPG (Макс. 600x337 пикселей)
|
||||
</p>
|
||||
</>
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
: <img
|
||||
src={imageUrl}
|
||||
className="object-cover w-[inherit] h-[inherit]"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
<FileInput
|
||||
id="dropzone-file"
|
||||
className="hidden"
|
||||
accept="image/jpg, image/jpeg, image/png"
|
||||
onChange={(e) => {
|
||||
handleFilePreview(e.target.files[0]);
|
||||
setCropModalOpen(true);
|
||||
handleImagePreview(e);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
@ -389,18 +492,15 @@ export const CreateCollectionPage = () => {
|
|||
setReleasesIds={setAddedReleasesIds}
|
||||
/>
|
||||
<CropModal
|
||||
src={tempImageUrl}
|
||||
setSrc={setImageUrl}
|
||||
setTempSrc={setTempImageUrl}
|
||||
// setImageData={setImageData}
|
||||
aspectRatio={600 / 337}
|
||||
guides={false}
|
||||
quality={100}
|
||||
isOpen={cropModalOpen}
|
||||
setIsOpen={setCropModalOpen}
|
||||
forceAspect={true}
|
||||
width={600}
|
||||
height={337}
|
||||
{...imageModalProps}
|
||||
cropParams={{
|
||||
aspectRatio: 600 / 337,
|
||||
forceAspect: true,
|
||||
guides: true,
|
||||
width: 600,
|
||||
height: 337,
|
||||
}}
|
||||
setCropModalProps={setImageModalProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -428,7 +528,7 @@ export const ReleasesEditModal = (props: {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2, revalidateFirstPage: false }
|
||||
);
|
||||
|
||||
|
@ -464,12 +564,31 @@ export const ReleasesEditModal = (props: {
|
|||
|
||||
function _addRelease(release: any) {
|
||||
if (props.releasesIds.length == 100) {
|
||||
alert("Достигнуто максимальное количество релизов в коллекции - 100");
|
||||
toast.error(
|
||||
"Достигнуто максимальное количество релизов в коллекции - 100",
|
||||
{
|
||||
position: "bottom-center",
|
||||
hideProgressBar: true,
|
||||
type: "error",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.releasesIds.includes(release.id)) {
|
||||
alert("Релиз уже добавлен в коллекцию");
|
||||
toast.error("Релиз уже добавлен в коллекцию", {
|
||||
position: "bottom-center",
|
||||
hideProgressBar: true,
|
||||
type: "error",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -494,7 +613,7 @@ export const ReleasesEditModal = (props: {
|
|||
className="max-w-full mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
props.setReleases([]);
|
||||
setContent([]);
|
||||
setQuery(e.target[0].value.trim());
|
||||
}}
|
||||
>
|
||||
|
@ -539,12 +658,12 @@ export const ReleasesEditModal = (props: {
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
<div className="grid grid-cols-2 gap-2 mt-2 md:grid-cols-4">
|
||||
{content.map((release) => {
|
||||
return (
|
||||
<button
|
||||
key={release.id}
|
||||
className=""
|
||||
className="overflow-hidden"
|
||||
onClick={() => _addRelease(release)}
|
||||
>
|
||||
<ReleaseLink type="poster" {...release} isLinkDisabled={true} />
|
||||
|
@ -553,6 +672,12 @@ export const ReleasesEditModal = (props: {
|
|||
})}
|
||||
{content.length == 1 && <div></div>}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center h-full min-h-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{error && <div>Произошла ошибка</div>}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Dropdown, Button } from "flowbite-react";
|
|||
import { sort } from "./common";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
const DropdownTheme = {
|
||||
floating: {
|
||||
|
@ -16,25 +17,10 @@ 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() {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const authState = useUserStore((state) => state.state);
|
||||
const [selectedSort, setSelectedSort] = useState(0);
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const router = useRouter();
|
||||
const [searchVal, setSearchVal] = useState("");
|
||||
|
||||
|
@ -47,7 +33,7 @@ export function FavoritesPage() {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -59,7 +45,6 @@ export function FavoritesPage() {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -156,7 +141,7 @@ export function FavoritesPage() {
|
|||
</div>
|
||||
{content && content.length > 0 ? (
|
||||
<ReleaseSection content={content} />
|
||||
) : !isLoadingEnd || isLoading ? (
|
||||
) : isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
|
|
@ -8,25 +8,12 @@ import { useUserStore } from "../store/auth";
|
|||
import { ENDPOINTS } from "#/api/config";
|
||||
import { Button } from "flowbite-react";
|
||||
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() {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const authState = useUserStore((state) => state.state);
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const router = useRouter();
|
||||
const [searchVal, setSearchVal] = useState("");
|
||||
|
||||
|
@ -39,7 +26,7 @@ export function HistoryPage() {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -51,7 +38,6 @@ export function HistoryPage() {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -136,7 +122,7 @@ export function HistoryPage() {
|
|||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : !isLoadingEnd || isLoading ? (
|
||||
) : isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { setJWT } from "#/api/utils";
|
||||
import { setJWT, tryCatchAPI } from "#/api/utils";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useThemeMode } from "flowbite-react";
|
||||
import { toast } from "react-toastify";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
|
||||
export function LoginPage() {
|
||||
const [login, setLogin] = useState("");
|
||||
|
@ -12,37 +15,78 @@ export function LoginPage() {
|
|||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const redirect = searchParams.get("redirect") || null;
|
||||
const theme = useThemeMode();
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
|
||||
function submit(e) {
|
||||
async function submit(e) {
|
||||
e.preventDefault();
|
||||
fetch("/api/profile/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login: login,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
alert("Ошибка получения пользователя.");
|
||||
}
|
||||
setIsSending(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.auth}?login=${login}&password=${password}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Sign: "9aa5c7af74e8cd70c86f7f9587bde23d",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.profileToken) {
|
||||
userStore.login(data.profile, data.profileToken.token);
|
||||
if (remember) {
|
||||
setJWT(data.profile.id, data.profileToken.token);
|
||||
}
|
||||
router.push("/");
|
||||
} else {
|
||||
alert("Неверные данные.");
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
let message = `Ошибка получения пользователя, code: ${error.code}`
|
||||
if (error.code == 2) {
|
||||
message = "Такого пользователя не существует"
|
||||
}
|
||||
if (error.code == 3) {
|
||||
message = "Неправильно указан логин и/или пароль"
|
||||
}
|
||||
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);
|
||||
if (remember) {
|
||||
setJWT(data.profile.id, data.profileToken.token);
|
||||
}
|
||||
|
||||
toast.update(tid, {
|
||||
render: "Вход успешен!",
|
||||
type: "success",
|
||||
autoClose: 2500,
|
||||
isLoading: false,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -53,7 +97,7 @@ export function LoginPage() {
|
|||
}, [userStore.user]);
|
||||
|
||||
return (
|
||||
<section className="bg-gray-50 dark:bg-gray-900">
|
||||
<section>
|
||||
<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="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||
import { Spinner } from "../components/Spinner/Spinner";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import useSWR from "swr";
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
import { ProfileUser } from "#/components/Profile/Profile.User";
|
||||
import { ProfileBannedBanner } from "#/components/Profile/ProfileBannedBanner";
|
||||
|
@ -16,20 +17,6 @@ import { ProfileReleaseRatings } from "#/components/Profile/Profile.ReleaseRatin
|
|||
import { ProfileReleaseHistory } from "#/components/Profile/Profile.ReleaseHistory";
|
||||
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) => {
|
||||
const authUser = useUserStore();
|
||||
const [user, setUser] = useState(null);
|
||||
|
@ -41,7 +28,7 @@ export const ProfilePage = (props: any) => {
|
|||
if (authUser.token) {
|
||||
url += `?token=${authUser.token}`;
|
||||
}
|
||||
const { data } = useSWR(url, fetcher);
|
||||
const { data, error } = useSWR(url, useSWRfetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
|
@ -50,7 +37,7 @@ export const ProfilePage = (props: any) => {
|
|||
}
|
||||
}, [data]);
|
||||
|
||||
if (!user) {
|
||||
if (!user && !error) {
|
||||
return (
|
||||
<main className="flex items-center justify-center min-h-screen">
|
||||
<Spinner />
|
||||
|
@ -58,6 +45,20 @@ 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 =
|
||||
user.vk_page != "" ||
|
||||
user.tg_page != "" ||
|
||||
|
@ -157,7 +158,11 @@ export const ProfilePage = (props: any) => {
|
|||
{!user.is_stats_hidden && (
|
||||
<div className="flex-col hidden gap-2 xl:flex">
|
||||
{user.votes && user.votes.length > 0 && (
|
||||
<ProfileReleaseRatings ratings={user.votes} token={authUser.token} profile_id={user.id} />
|
||||
<ProfileReleaseRatings
|
||||
ratings={user.votes}
|
||||
token={authUser.token}
|
||||
profile_id={user.id}
|
||||
/>
|
||||
)}
|
||||
{user.history && user.history.length > 0 && (
|
||||
<ProfileReleaseHistory history={user.history} />
|
||||
|
@ -197,7 +202,11 @@ export const ProfilePage = (props: any) => {
|
|||
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
||||
<div className="flex flex-col gap-2 xl:hidden">
|
||||
{user.votes && user.votes.length > 0 && (
|
||||
<ProfileReleaseRatings ratings={user.votes} token={authUser.token} profile_id={user.id} />
|
||||
<ProfileReleaseRatings
|
||||
ratings={user.votes}
|
||||
token={authUser.token}
|
||||
profile_id={user.id}
|
||||
/>
|
||||
)}
|
||||
{user.history && user.history.length > 0 && (
|
||||
<ProfileReleaseHistory history={user.history} />
|
||||
|
@ -207,7 +216,12 @@ export const ProfilePage = (props: any) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ProfileEditModal isOpen={isOpen && isMyProfile} setIsOpen={setIsOpen} token={authUser.token} profile_id={user.id}/>
|
||||
<ProfileEditModal
|
||||
isOpen={isOpen && isMyProfile}
|
||||
setIsOpen={setIsOpen}
|
||||
token={authUser.token}
|
||||
profile_id={user.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,22 +6,11 @@ import { useScrollPosition } from "#/hooks/useScrollPosition";
|
|||
import { useUserStore } from "../store/auth";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
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}) {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||
if (previousPageData && !previousPageData.content.length) return null;
|
||||
|
@ -33,7 +22,7 @@ export function RelatedPage(props: {id: number|string, title: string}) {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 1 }
|
||||
);
|
||||
|
||||
|
@ -45,7 +34,6 @@ export function RelatedPage(props: {id: number|string, title: string}) {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -70,7 +58,7 @@ export function RelatedPage(props: {id: number|string, title: string}) {
|
|||
return <ReleaseLink169Related {...release} key={release.id} _position={index + 1} />
|
||||
})}
|
||||
</div>
|
||||
) : !isLoadingEnd || isLoading ? (
|
||||
) : isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import useSWR from "swr";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
const fetcher = (...args: any) =>
|
||||
fetch([...args] as any).then((res) => res.json());
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
@ -33,7 +32,7 @@ export const ReleasePage = (props: any) => {
|
|||
if (userStore.token) {
|
||||
url += `?token=${userStore.token}`;
|
||||
}
|
||||
const { data, isLoading, error } = useSWR(url, fetcher);
|
||||
const { data, isLoading, error } = useSWR(url, useSWRfetcher);
|
||||
return [data, isLoading, error];
|
||||
}
|
||||
const [data, isLoading, error] = useFetch(props.id);
|
||||
|
@ -49,83 +48,106 @@ export const ReleasePage = (props: any) => {
|
|||
}
|
||||
}, [data]);
|
||||
|
||||
return data ? (
|
||||
<>
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
|
||||
<div className="[grid-column:1] [grid-row:span_2]">
|
||||
<ReleaseInfoBasics
|
||||
image={data.release.image}
|
||||
title={{
|
||||
ru: data.release.title_ru,
|
||||
original: data.release.title_original,
|
||||
}}
|
||||
description={data.release.description}
|
||||
note={data.release.note}
|
||||
release_id={data.release.id}
|
||||
/>
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className="flex items-center justify-center min-h-screen">
|
||||
<Spinner />
|
||||
</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>
|
||||
<div className="[grid-column:2]">
|
||||
<ReleaseInfoInfo
|
||||
country={data.release.country}
|
||||
aired_on_date={data.release.aired_on_date}
|
||||
year={data.release.year}
|
||||
episodes={{
|
||||
total: data.release.episodes_total,
|
||||
released: data.release.episodes_released,
|
||||
}}
|
||||
season={data.release.season}
|
||||
status={data.release.status ? data.release.status.name : "Анонс"}
|
||||
duration={data.release.duration}
|
||||
category={data.release.category.name}
|
||||
broadcast={data.release.broadcast}
|
||||
studio={data.release.studio}
|
||||
author={data.release.author}
|
||||
director={data.release.director}
|
||||
genres={data.release.genres}
|
||||
/>
|
||||
</div>
|
||||
<div className="[grid-column:2]">
|
||||
<ReleaseInfoUserList
|
||||
userList={userList}
|
||||
isFavorite={userFavorite}
|
||||
release_id={data.release.id}
|
||||
</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
|
||||
image={data.release.image}
|
||||
title={{
|
||||
ru: data.release.title_ru,
|
||||
original: data.release.title_original,
|
||||
}}
|
||||
description={data.release.description}
|
||||
note={data.release.note}
|
||||
release_id={data.release.id}
|
||||
/>
|
||||
<ReleaseInfoInfo
|
||||
country={data.release.country}
|
||||
aired_on_date={data.release.aired_on_date}
|
||||
year={data.release.year}
|
||||
episodes={{
|
||||
total: data.release.episodes_total,
|
||||
released: data.release.episodes_released,
|
||||
}}
|
||||
season={data.release.season}
|
||||
status={data.release.status ? data.release.status.name : "Анонс"}
|
||||
duration={data.release.duration}
|
||||
category={data.release.category.name}
|
||||
broadcast={data.release.broadcast}
|
||||
studio={data.release.studio}
|
||||
author={data.release.author}
|
||||
director={data.release.director}
|
||||
genres={data.release.genres}
|
||||
/>
|
||||
<ReleaseInfoUserList
|
||||
userList={userList}
|
||||
isFavorite={userFavorite}
|
||||
release_id={data.release.id}
|
||||
token={userStore.token}
|
||||
profile_id={userStore.user ? userStore.user.id : null}
|
||||
setUserList={setUserList}
|
||||
setIsFavorite={setUserFavorite}
|
||||
collection_count={data.release.collection_count}
|
||||
/>
|
||||
</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.name.toLowerCase() != "анонс" && (
|
||||
<>
|
||||
{preferenceStore.params.experimental.newPlayer ?
|
||||
<ReleasePlayerCustom id={props.id} token={userStore.token} />
|
||||
: <ReleasePlayer id={props.id} />}
|
||||
</>
|
||||
)}
|
||||
<CommentsMain
|
||||
release_id={props.id}
|
||||
token={userStore.token}
|
||||
profile_id={userStore.user ? userStore.user.id : null}
|
||||
setUserList={setUserList}
|
||||
setIsFavorite={setUserFavorite}
|
||||
collection_count={data.release.collection_count}
|
||||
comments={data.release.comments}
|
||||
/>
|
||||
</div>
|
||||
{data.release.status &&
|
||||
data.release.status.name.toLowerCase() != "анонс" && (
|
||||
<div className="[grid-column:1] [grid-row:span_12]">
|
||||
{preferenceStore.params.experimental.newPlayer ? (
|
||||
<ReleasePlayerCustom id={props.id} token={userStore.token} />
|
||||
) : (
|
||||
<ReleasePlayer id={props.id} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.release.status &&
|
||||
data.release.status.name.toLowerCase() != "анонс" && (
|
||||
<div className="[grid-column:2]">
|
||||
<ReleaseInfoRating
|
||||
release_id={props.id}
|
||||
grade={data.release.grade}
|
||||
token={userStore.token}
|
||||
votes={{
|
||||
1: data.release.vote_1_count,
|
||||
2: data.release.vote_2_count,
|
||||
3: data.release.vote_3_count,
|
||||
4: data.release.vote_4_count,
|
||||
5: data.release.vote_5_count,
|
||||
total: data.release.vote_count,
|
||||
user: data.release.your_vote,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="[grid-column:2] [grid-row:span_4]">
|
||||
<div className="flex flex-col gap-2">
|
||||
{data.release.status &&
|
||||
data.release.status.name.toLowerCase() != "анонс" && (
|
||||
<div className="[grid-column:2]">
|
||||
<ReleaseInfoRating
|
||||
release_id={props.id}
|
||||
grade={data.release.grade}
|
||||
token={userStore.token}
|
||||
votes={{
|
||||
1: data.release.vote_1_count,
|
||||
2: data.release.vote_2_count,
|
||||
3: data.release.vote_3_count,
|
||||
4: data.release.vote_4_count,
|
||||
5: data.release.vote_5_count,
|
||||
total: data.release.vote_count,
|
||||
user: data.release.your_vote,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InfoLists
|
||||
completed={data.release.completed_count}
|
||||
planned={data.release.plan_count}
|
||||
|
@ -133,36 +155,18 @@ export const ReleasePage = (props: any) => {
|
|||
delayed={data.release.hold_on_count}
|
||||
watching={data.release.watching_count}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.release.screenshot_images.length > 0 && (
|
||||
<div className="[grid-column:2] [grid-row:span_11]">
|
||||
{data.release.screenshot_images.length > 0 && (
|
||||
<ReleaseInfoScreenshots images={data.release.screenshot_images} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.release.related_releases.length > 0 && (
|
||||
<div className="[grid-column:2] [grid-row:span_2]">
|
||||
)}
|
||||
{data.release.related_releases.length > 0 && (
|
||||
<ReleaseInfoRelated
|
||||
release_id={props.id}
|
||||
related={data.release.related}
|
||||
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 className="flex h-[100dvh] w-full justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,20 +11,7 @@ import { useUserStore } from "../store/auth";
|
|||
import { Button, Dropdown, Modal } from "flowbite-react";
|
||||
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
|
||||
import { UserSection } from "#/components/UserSection/UserSection";
|
||||
|
||||
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();
|
||||
};
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
const ListsMapping = {
|
||||
watching: {
|
||||
|
@ -128,7 +115,7 @@ export function SearchPage() {
|
|||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2, revalidateFirstPage: false }
|
||||
);
|
||||
|
||||
|
@ -174,7 +161,18 @@ export function SearchPage() {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchVal]);
|
||||
|
||||
if (error) return <div>failed to load: {error.message}</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 (
|
||||
<>
|
||||
|
@ -237,39 +235,35 @@ export function SearchPage() {
|
|||
</div>
|
||||
<div className="mt-2">
|
||||
{data && data[0].related && <RelatedSection {...data[0].related} />}
|
||||
{content ? (
|
||||
content.length > 0 ? (
|
||||
{content ?
|
||||
content.length > 0 ?
|
||||
<>
|
||||
{where == "collections" ? (
|
||||
{where == "collections" ?
|
||||
<CollectionsSection
|
||||
sectionTitle="Найденные Коллекции"
|
||||
content={content}
|
||||
/>
|
||||
) : where == "profiles" ? (
|
||||
: where == "profiles" ?
|
||||
<UserSection
|
||||
sectionTitle="Найденные Пользователи"
|
||||
content={content}
|
||||
/>
|
||||
) : (
|
||||
<ReleaseSection
|
||||
: <ReleaseSection
|
||||
sectionTitle="Найденные релизы"
|
||||
content={content}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
<span className="w-24 h-24 iconify-color twemoji--crying-cat"></span>
|
||||
<p>Странно, аниме не найдено, попробуйте другой запрос...</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
isLoading && (
|
||||
: isLoading && (
|
||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
}
|
||||
{!content && !isLoading && !query && (
|
||||
<div className="flex flex-col items-center justify-center min-w-full gap-2 mt-12 text-xl">
|
||||
<span className="w-16 h-16 iconify mdi--arrow-up animate-bounce"></span>
|
||||
|
@ -277,11 +271,13 @@ export function SearchPage() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{data &&
|
||||
data.length > 1 &&
|
||||
(where == "releases"
|
||||
? data[data.length - 1].releases.length == 25
|
||||
: data[data.length - 1].content.length == 25) ? (
|
||||
{(
|
||||
data &&
|
||||
data.length > 1 &&
|
||||
(where == "releases" ?
|
||||
data[data.length - 1].releases.length == 25
|
||||
: data[data.length - 1].content.length == 25)
|
||||
) ?
|
||||
<Button
|
||||
className="w-full"
|
||||
color={"light"}
|
||||
|
@ -292,9 +288,7 @@ export function SearchPage() {
|
|||
<span className="text-lg">Загрузить ещё</span>
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
: ""}
|
||||
<FiltersModal
|
||||
isOpen={filtersModalOpen}
|
||||
setIsOpen={setFiltersModalOpen}
|
||||
|
@ -394,9 +388,7 @@ const FiltersModal = (props: {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{props.isAuth &&
|
||||
where == "list" &&
|
||||
ListsMapping.hasOwnProperty(list) ? (
|
||||
{props.isAuth && where == "list" && ListsMapping.hasOwnProperty(list) ?
|
||||
<div className="my-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-bold dark:text-white">Список</p>
|
||||
|
@ -414,10 +406,8 @@ const FiltersModal = (props: {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{!["profiles", "collections"].includes(where) ? (
|
||||
: ""}
|
||||
{!["profiles", "collections"].includes(where) ?
|
||||
<div className="my-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-bold dark:text-white">Искать по</p>
|
||||
|
@ -435,9 +425,7 @@ const FiltersModal = (props: {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
: ""}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<div className="flex justify-end w-full gap-2">
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useState, useEffect } from "react";
|
|||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||
import { useUserStore } from "../store/auth";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||
|
||||
import { CollectionInfoBasics } from "#/components/CollectionInfo/CollectionInfo.Basics";
|
||||
|
@ -14,24 +13,10 @@ import { InfoLists } from "#/components/InfoLists/InfoLists";
|
|||
import { CollectionInfoControls } from "#/components/CollectionInfo/CollectionInfoControls";
|
||||
import { CommentsMain } from "#/components/Comments/Comments.Main";
|
||||
|
||||
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();
|
||||
};
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
export const ViewCollectionPage = (props: { id: number }) => {
|
||||
const userStore = useUserStore();
|
||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
function useFetchCollectionInfo(type: "info" | "comments") {
|
||||
let url: string;
|
||||
|
@ -46,8 +31,8 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
url += `${type != "info" ? "&" : "?"}token=${userStore.token}`;
|
||||
}
|
||||
|
||||
const { data, isLoading } = useSWR(url, fetcher);
|
||||
return [data, isLoading];
|
||||
const { data, error, isLoading } = useSWR(url, useSWRfetcher);
|
||||
return [data, error, isLoading];
|
||||
}
|
||||
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||
if (previousPageData && !previousPageData.content.length) return null;
|
||||
|
@ -58,14 +43,17 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
return url;
|
||||
};
|
||||
|
||||
const [collectionInfo, collectionInfoIsLoading] =
|
||||
const [collectionInfo, collectionInfoError, collectionInfoIsLoading] =
|
||||
useFetchCollectionInfo("info");
|
||||
const [collectionComments, collectionCommentsIsLoading] =
|
||||
useFetchCollectionInfo("comments");
|
||||
const [
|
||||
collectionComments,
|
||||
collectionCommentsError,
|
||||
collectionCommentsIsLoading,
|
||||
] = useFetchCollectionInfo("comments");
|
||||
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
fetcher,
|
||||
useSWRfetcher,
|
||||
{ initialSize: 2 }
|
||||
);
|
||||
|
||||
|
@ -77,7 +65,6 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
allReleases.push(...data[i].content);
|
||||
}
|
||||
setContent(allReleases);
|
||||
setIsLoadingEnd(true);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -89,14 +76,35 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
<>
|
||||
{collectionInfoIsLoading ? (
|
||||
{collectionInfoIsLoading ?
|
||||
<div className="flex items-center justify-center w-full h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
collectionInfo && (
|
||||
: collectionInfo && (
|
||||
<>
|
||||
<div className="flex flex-col flex-wrap gap-2 px-2 pb-2 sm:flex-row">
|
||||
<CollectionInfoBasics
|
||||
|
@ -138,11 +146,7 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading || !content || !isLoadingEnd ? (
|
||||
<div className="flex items-center justify-center w-full h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
{content && (
|
||||
<ReleaseSection
|
||||
sectionTitle={"Релизов в коллекции: " + data[0].total_count}
|
||||
content={content}
|
||||
|
@ -150,7 +154,7 @@ export const ViewCollectionPage = (props: { id: number }) => {
|
|||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,19 +16,26 @@ export async function generateMetadata(
|
|||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id: string = params.id;
|
||||
const profile: any = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/profile/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
title: SectionTitleMapping[params.slug] + " - " + profile.profile.login,
|
||||
description: profile.profile.status,
|
||||
title:"Закладки Пользователя - " + data.profile.login + " - " + SectionTitleMapping[params.slug],
|
||||
description: "Закладки Пользователя - " + data.profile.login + " - " + SectionTitleMapping[params.slug],
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: profile.profile.avatar, // Must be an absolute URL
|
||||
url: data.profile.avatar, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 600,
|
||||
},
|
||||
|
|
|
@ -1,26 +1,33 @@
|
|||
import { BookmarksPage } from "#/pages/Bookmarks";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id: string = params.id;
|
||||
const profile: any = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/profile/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
title: "Закладки - " + profile.profile.login,
|
||||
description: profile.profile.status,
|
||||
title: "Закладки Пользователя - " + data.profile.login,
|
||||
description: "Закладки Пользователя - " + data.profile.login,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: profile.profile.avatar, // Must be an absolute URL
|
||||
url: data.profile.avatar, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 600,
|
||||
},
|
||||
|
@ -30,5 +37,5 @@ export async function generateMetadata(
|
|||
}
|
||||
|
||||
export default function Index({ params }) {
|
||||
return <BookmarksPage profile_id={params.id}/>;
|
||||
return <BookmarksPage profile_id={params.id} />;
|
||||
}
|
||||
|
|
|
@ -1,43 +1,65 @@
|
|||
import { CollectionsFullPage } from "#/pages/CollectionsFull";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id: string = params.id;
|
||||
const profile: any = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/profile/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
title: "Коллекции - " + profile.profile.login,
|
||||
description: profile.profile.status,
|
||||
title: "Коллекции Пользователя - " + data.profile.login,
|
||||
description: "Коллекции Пользователя - " + data.profile.login,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: profile.profile.avatar, // Must be an absolute URL
|
||||
url: data.profile.avatar, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 600,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default async function Collections({ params }) {
|
||||
const profile: any = await fetchDataViaGet(
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CollectionsFullPage
|
||||
type="profile"
|
||||
title={`Коллекции пользователя ${profile.profile.login}`}
|
||||
title={`Коллекции пользователя: ${data.profile.login}`}
|
||||
profile_id={params.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,26 +1,33 @@
|
|||
import { ProfilePage } from "#/pages/Profile";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id: string = params.id;
|
||||
const profile: any = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/profile/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Профиль - " + profile.profile.login,
|
||||
description: profile.profile.status,
|
||||
title: "Профиль - " + data.profile.login,
|
||||
description: data.profile.status,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: profile.profile.avatar, // Must be an absolute URL
|
||||
url: data.profile.avatar, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 600,
|
||||
},
|
||||
|
|
|
@ -3,12 +3,31 @@ import { fetchDataViaGet } from "#/api/utils";
|
|||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
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> {
|
||||
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 [ 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 {
|
||||
title: "Франшиза - " + firstRelease.release.related.name_ru || firstRelease.release.related.name,
|
||||
description: firstRelease.release.description,
|
||||
|
@ -27,7 +46,25 @@ export async function generateMetadata({ params }, parent: ResolvingMetadata): P
|
|||
|
||||
export default async function Related({ params }) {
|
||||
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 [ related, relatedError ] = await _getData(`https://api.anixart.tv/related/${id}/0`);
|
||||
if (relatedError || related.content.length == 0) {
|
||||
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} />;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,33 @@
|
|||
import { CollectionsFullPage } from "#/pages/CollectionsFull";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id = params.id;
|
||||
const release = await fetchDataViaGet(`https://api.anixart.tv/release/${id}`);
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/release/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: release.release.title_ru + " - в коллекциях",
|
||||
description: release.release.description,
|
||||
title: data.release.title_ru + " - в коллекциях",
|
||||
description: data.release.description,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: release.release.image, // Must be an absolute URL
|
||||
url: data.release.image, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 800,
|
||||
},
|
||||
|
@ -28,13 +37,26 @@ export async function generateMetadata(
|
|||
}
|
||||
|
||||
export default async function Collections({ params }) {
|
||||
const release: any = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`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 (
|
||||
<CollectionsFullPage
|
||||
type="release"
|
||||
title={release.release.title_ru + " в коллекциях"}
|
||||
title={data.release.title_ru + " в коллекциях"}
|
||||
release_id={params.id}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,24 +1,33 @@
|
|||
import { ReleasePage } from "#/pages/Release";
|
||||
import { fetchDataViaGet } from "#/api/utils";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
export const dynamic = 'force-static';
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params },
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id = params.id;
|
||||
const release = await fetchDataViaGet(`https://api.anixart.tv/release/${id}`);
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`https://api.anixart.tv/release/${id}`
|
||||
);
|
||||
const previousOG = (await parent).openGraph;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
title: "Ошибка",
|
||||
description: "Ошибка",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: release.release.title_ru,
|
||||
description: release.release.description,
|
||||
title: data.release.title_ru,
|
||||
description: data.release.description,
|
||||
openGraph: {
|
||||
...previousOG,
|
||||
images: [
|
||||
{
|
||||
url: release.release.image, // Must be an absolute URL
|
||||
url: data.release.image, // Must be an absolute URL
|
||||
width: 600,
|
||||
height: 800,
|
||||
},
|
||||
|
|
|
@ -44,14 +44,16 @@ export const useUserStore = create<userState>((set, get) => ({
|
|||
const jwt = getJWT();
|
||||
if (jwt) {
|
||||
const _checkAuth = async () => {
|
||||
const data = await fetchDataViaGet(
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`${ENDPOINTS.user.profile}/${jwt.user_id}?token=${jwt.jwt}`
|
||||
);
|
||||
if (data && data.is_my_profile) {
|
||||
get().login(data.profile, jwt.jwt);
|
||||
} else {
|
||||
|
||||
if (error || !data.is_my_profile) {
|
||||
get().logout();
|
||||
return;
|
||||
}
|
||||
|
||||
get().login(data.profile, jwt.jwt);
|
||||
};
|
||||
_checkAuth();
|
||||
} else {
|
||||
|
|
|
@ -18,10 +18,13 @@ export default async function middleware(
|
|||
}
|
||||
let path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search;
|
||||
|
||||
const data = await fetchDataViaGet(`${API_URL}/${path}`, isApiV2);
|
||||
const { data, error } = await fetchDataViaGet(
|
||||
`${API_URL}/${path}`,
|
||||
isApiV2
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ message: "Error Fetching Data" }), {
|
||||
if (error) {
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -46,26 +49,33 @@ export default async function middleware(
|
|||
const path = url.pathname.match(/\/api\/proxy\/(.*)/)?.[1] + url.search;
|
||||
|
||||
const ReqContentTypeHeader = request.headers.get("Content-Type") || "";
|
||||
const ReqSignHeader = request.headers.get("Sign") || null;
|
||||
let ResContentTypeHeader = "";
|
||||
let body = null;
|
||||
|
||||
if (ReqContentTypeHeader.split(";")[0] == "multipart/form-data") {
|
||||
ResContentTypeHeader = ReqContentTypeHeader;
|
||||
body = await request.arrayBuffer();
|
||||
} else if (ReqContentTypeHeader == "application/x-www-form-urlencoded") {
|
||||
ResContentTypeHeader = ReqContentTypeHeader;
|
||||
} else {
|
||||
ResContentTypeHeader = "application/json; charset=UTF-8";
|
||||
body = JSON.stringify(await request.json());
|
||||
}
|
||||
|
||||
const data = await fetchDataViaPost(
|
||||
let resHeaders = {};
|
||||
resHeaders["Content-Type"] = ResContentTypeHeader;
|
||||
ReqSignHeader && (resHeaders["Sign"] = ReqSignHeader);
|
||||
|
||||
const { data, error } = await fetchDataViaPost(
|
||||
`${API_URL}/${path}`,
|
||||
body,
|
||||
isApiV2,
|
||||
ResContentTypeHeader
|
||||
resHeaders
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ message: "Error Fetching Data" }), {
|
||||
if (error) {
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -20,6 +20,7 @@
|
|||
"react": "^18",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^11.0.5",
|
||||
"swiper": "^11.1.4",
|
||||
"swr": "^2.2.5",
|
||||
"videojs-video-element": "^1.4.1",
|
||||
|
@ -1671,6 +1672,15 @@
|
|||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
@ -4765,6 +4775,19 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"react": "^18",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^11.0.5",
|
||||
"swiper": "^11.1.4",
|
||||
"swr": "^2.2.5",
|
||||
"videojs-video-element": "^1.4.1",
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
- Исправлено отображение времени года в информации о релизе
|
||||
- Исправлена ошибка когда плеер не может загрузиться
|
||||
|
||||
# 3.3.0 - Update 1
|
||||
## 3.3.0 - Update 1
|
||||
|
||||
- Добавлена Кнопка пропуска опенинга (90 секунд)
|
||||
- Исправлено обрезание квадратных видео в собственном плеере
|
||||
- Изменён вид навигации и добавлена возможность изменять отображение текста на кнопках
|
||||
|
||||
# 3.3.0 - Update 2
|
||||
## 3.3.0 - Update 2
|
||||
|
||||
- Исправлен сброс скорости воспроизведения при смене серии в собственном плеере
|
||||
- Исправлен парсинг ссылок источника kodik для некоторых аниме
|
||||
|
|
17
public/changelog/3.4.0.md
Normal file
17
public/changelog/3.4.0.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 3.4.0
|
||||
|
||||
## Добавлено
|
||||
|
||||
- Добавлены уведомления о действиях пользователя на различных страницах (например: добавление в избранное или друзья).
|
||||
- Добавлен показ ошибок при загрузке своего плеера.
|
||||
|
||||
## Изменено
|
||||
|
||||
- Улучшено отображение ошибок
|
||||
- Улучшено отображение страницы релиза на некоторых релизах
|
||||
- Добавлено больше пространства между иконками в навигации на телефонах
|
||||
|
||||
## Исправлено
|
||||
|
||||
- Вид карточек в окне поиска релизов для добавления в коллекцию
|
||||
- Сброс добавленных релизов при изменении или создании коллекции при поиска другого запроса в окне поиска релизов
|
Loading…
Add table
Reference in a new issue