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