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