mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-06 00:04:39 +00:00
Merge remote-tracking branch 'origin/feat_player'
This commit is contained in:
parent
bb437fe7ca
commit
25e31a7799
62 changed files with 1508 additions and 701 deletions
18
TODO.md
18
TODO.md
|
@ -12,7 +12,7 @@
|
||||||
- [X] Просмотр последних аниме в списках
|
- [X] Просмотр последних аниме в списках
|
||||||
- [X] Просмотр всех аниме в списках
|
- [X] Просмотр всех аниме в списках
|
||||||
- [X] Сортировка аниме в списках
|
- [X] Сортировка аниме в списках
|
||||||
- [ ] Добавление \ Удаление аниме из списков
|
- [X] Добавление \ Удаление аниме из списков
|
||||||
|
|
||||||
### Поиск
|
### Поиск
|
||||||
|
|
||||||
|
@ -21,12 +21,14 @@
|
||||||
- [ ] Просмотр страницы франшизы
|
- [ ] Просмотр страницы франшизы
|
||||||
- [ ] Фильтры поиска
|
- [ ] Фильтры поиска
|
||||||
- [ ] История поиска
|
- [ ] История поиска
|
||||||
|
- [X] Поиск по тегам со страницы тайтла
|
||||||
|
|
||||||
### Закладки
|
### Закладки
|
||||||
|
|
||||||
- [X] Просмотр всех аниме в списке
|
- [X] Просмотр всех аниме в списке
|
||||||
- [X] Сортировка аниме в списке
|
- [X] Сортировка аниме в списке
|
||||||
- [ ] Добавление \ Удаление аниме из списка
|
- [X] Добавление \ Удаление аниме из списка
|
||||||
|
- [ ] Поиск в списке
|
||||||
|
|
||||||
### Профиль
|
### Профиль
|
||||||
|
|
||||||
|
@ -48,12 +50,16 @@
|
||||||
|
|
||||||
### Страница аниме тайтла
|
### Страница аниме тайтла
|
||||||
|
|
||||||
- [ ] Описание тайтла
|
- [X] Описание тайтла
|
||||||
|
- [ ] Скриншоты тайтла
|
||||||
|
- [ ] Видео тайтла
|
||||||
- [ ] Просмотр тайтла
|
- [ ] Просмотр тайтла
|
||||||
- [ ] Просмотр комментариев
|
- [ ] Просмотр комментариев и комментирование
|
||||||
- [ ] Комментирование
|
|
||||||
- [ ] Сохранение эпизода в историю просмотров
|
- [ ] Сохранение эпизода в историю просмотров
|
||||||
- [ ] Добавление \ Удаление аниме в\из списков закладок и избранных
|
- [X] Добавление \ Удаление аниме в\из списков закладок и избранных
|
||||||
|
- [X] Связанные релизы
|
||||||
|
- [ ] Просмотр страницы всех вязанных релизов
|
||||||
|
- [ ] Оценка тайтла
|
||||||
|
|
||||||
## Баги
|
## Баги
|
||||||
|
|
||||||
|
|
38
app/api/[...endpoint]/route.ts
Normal file
38
app/api/[...endpoint]/route.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextResponse, NextRequest } from "next/server";
|
||||||
|
import { fetchDataViaGet, fetchDataViaPost } from "../utils";
|
||||||
|
import { API_URL } from "../config";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { endpoint: Array<string> } }
|
||||||
|
) {
|
||||||
|
const { endpoint } = params;
|
||||||
|
let API_V2: boolean | string =
|
||||||
|
req.nextUrl.searchParams.get("API_V2") || false;
|
||||||
|
if (API_V2 === "true") {
|
||||||
|
req.nextUrl.searchParams.delete("API_V2");
|
||||||
|
}
|
||||||
|
const query = req.nextUrl.searchParams.toString();
|
||||||
|
const url = `${API_URL}/${endpoint.join("/")}${query ? `?${query}` : ""}`;
|
||||||
|
|
||||||
|
const response = await fetchDataViaGet(url, API_V2);
|
||||||
|
return NextResponse.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { endpoint: Array<string> } }
|
||||||
|
) {
|
||||||
|
const { endpoint } = params;
|
||||||
|
let API_V2: boolean | string =
|
||||||
|
req.nextUrl.searchParams.get("API_V2") || false;
|
||||||
|
if (API_V2 === "true") {
|
||||||
|
req.nextUrl.searchParams.delete("API_V2");
|
||||||
|
}
|
||||||
|
const query = req.nextUrl.searchParams.toString();
|
||||||
|
const url = `${API_URL}/${endpoint.join("/")}${query ? `?${query}` : ""}`;
|
||||||
|
const body = JSON.stringify( await req.json());
|
||||||
|
|
||||||
|
const response = await fetchDataViaPost(url, body, API_V2);
|
||||||
|
return NextResponse.json(response);
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaGet } from "../utils";
|
|
||||||
import { ENDPOINTS } from "../config";
|
|
||||||
import { sort } from "../common";
|
|
||||||
|
|
||||||
const list = {
|
|
||||||
watching: 1,
|
|
||||||
planned: 2,
|
|
||||||
watched: 3,
|
|
||||||
delayed: 4,
|
|
||||||
abandoned: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function GET(request) {
|
|
||||||
const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
|
|
||||||
const listName = request.nextUrl.searchParams.get(["list"]) || null;
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
const sortName =
|
|
||||||
request.nextUrl.searchParams.get(["sort"]) || "adding_descending";
|
|
||||||
|
|
||||||
if (!token || token == "null") {
|
|
||||||
return NextResponse.json({ message: "No token provided" }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!listName || listName == "null") {
|
|
||||||
return NextResponse.json({ message: "No list provided" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!list[listName]) {
|
|
||||||
return NextResponse.json({ message: "Unknown list" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sort[sortName]) {
|
|
||||||
return NextResponse.json({ message: "Unknown sort" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = new URL(`${ENDPOINTS.user.bookmark}/${list[listName]}/${page}`);
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
url.searchParams.set("sort", sort[sortName]);
|
|
||||||
|
|
||||||
const response = await fetchDataViaGet(url.toString());
|
|
||||||
return NextResponse.json(response);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export const sort = {
|
|
||||||
adding_descending: 1,
|
|
||||||
adding_ascending: 2,
|
|
||||||
year_descending: 3,
|
|
||||||
year_ascending: 4,
|
|
||||||
alphabet_descending: 5,
|
|
||||||
alphabet_ascending: 6,
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
export const API_URL = "https://api.anixart.tv";
|
|
||||||
export const USER_AGENT =
|
|
||||||
"AnixartApp/8.2.1-23121216 (Android 11; SDK 30; arm64-v8a;)";
|
|
||||||
|
|
||||||
export const ENDPOINTS = {
|
|
||||||
release: {
|
|
||||||
info: `${API_URL}/release`,
|
|
||||||
episode: `${API_URL}/episode`,
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
profile: `${API_URL}/profile`,
|
|
||||||
bookmark: `${API_URL}/profile/list/all`,
|
|
||||||
history: `${API_URL}/history`,
|
|
||||||
favorite: `${API_URL}/favorite/all`,
|
|
||||||
},
|
|
||||||
filter: `${API_URL}/filter`,
|
|
||||||
auth: `${API_URL}/auth/signIn`,
|
|
||||||
// user: {
|
|
||||||
// history: `${API_URL}/history`,
|
|
||||||
// watching: `${API_URL}/profile/list/all/1`,
|
|
||||||
// planned: `${API_URL}/profile/list/all/2`,
|
|
||||||
// watched: `${API_URL}/profile/list/all/3`,
|
|
||||||
// delayed: `${API_URL}/profile/list/all/4`,
|
|
||||||
// abandoned: `${API_URL}/profile/list/all/5`,
|
|
||||||
// favorite: `${API_URL}/favorite`,
|
|
||||||
// },
|
|
||||||
search: `${API_URL}/search/releases`,
|
|
||||||
statistic: {
|
|
||||||
addHistory: `${API_URL}/history/add`,
|
|
||||||
markWatched: `${API_URL}/episode/watch`,
|
|
||||||
},
|
|
||||||
};
|
|
32
app/api/config.ts
Normal file
32
app/api/config.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
export const API_URL = "https://api.anixart.tv";
|
||||||
|
export const API_PREFIX = "/api";
|
||||||
|
export const USER_AGENT =
|
||||||
|
"AnixartApp/8.2.1-23121216 (Android 11; SDK 30; arm64-v8a;)";
|
||||||
|
|
||||||
|
export const ENDPOINTS = {
|
||||||
|
release: {
|
||||||
|
info: `${API_PREFIX}/release`,
|
||||||
|
episode: `${API_PREFIX}/episode`,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
profile: `${API_PREFIX}/profile`,
|
||||||
|
bookmark: `${API_PREFIX}/profile/list`,
|
||||||
|
history: `${API_PREFIX}/history`,
|
||||||
|
favorite: `${API_PREFIX}/favorite`,
|
||||||
|
},
|
||||||
|
filter: `${API_PREFIX}/filter`,
|
||||||
|
// user: {
|
||||||
|
// history: `${API_PREFIX}/history`,
|
||||||
|
// watching: `${API_PREFIX}/profile/list/all/1`,
|
||||||
|
// planned: `${API_PREFIX}/profile/list/all/2`,
|
||||||
|
// watched: `${API_PREFIX}/profile/list/all/3`,
|
||||||
|
// delayed: `${API_PREFIX}/profile/list/all/4`,
|
||||||
|
// abandoned: `${API_PREFIX}/profile/list/all/5`,
|
||||||
|
// favorite: `${API_PREFIX}/favorite`,
|
||||||
|
// },
|
||||||
|
search: `${API_URL}/search`,
|
||||||
|
statistic: {
|
||||||
|
addHistory: `${API_PREFIX}/history/add`,
|
||||||
|
markWatched: `${API_PREFIX}/episode/watch`,
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,26 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaGet } from "../utils";
|
|
||||||
import { ENDPOINTS } from "../config";
|
|
||||||
import { sort } from "../common";
|
|
||||||
|
|
||||||
|
|
||||||
export async function GET(request) {
|
|
||||||
const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
const sortName = request.nextUrl.searchParams.get(["sort"]) || "adding_descending";
|
|
||||||
|
|
||||||
if (!token || token == "null") {
|
|
||||||
return NextResponse.json({ message: "No token provided" }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sort[sortName]) {
|
|
||||||
return NextResponse.json({ message: "Unknown sort" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = new URL(`${ENDPOINTS.user.favorite}/${page}`);
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
url.searchParams.set("sort", sort[sortName]);
|
|
||||||
|
|
||||||
const response = await fetchDataViaGet(url.toString());
|
|
||||||
return NextResponse.json(response);
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaGet } from "../utils";
|
|
||||||
import { ENDPOINTS } from "../config";
|
|
||||||
|
|
||||||
export async function GET(request) {
|
|
||||||
const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
const sortName = request.nextUrl.searchParams.get(["sort"]) || "adding_descending";
|
|
||||||
|
|
||||||
if (!token || token == "null") {
|
|
||||||
return NextResponse.json({ message: "No token provided" }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = new URL(`${ENDPOINTS.user.history}/${page}`);
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
|
|
||||||
const response = await fetchDataViaGet(url.toString());
|
|
||||||
return NextResponse.json(response);
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaPost } from "../utils";
|
|
||||||
import { ENDPOINTS } from "../config";
|
|
||||||
|
|
||||||
export async function GET(request) {
|
|
||||||
const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
|
|
||||||
const status = request.nextUrl.searchParams.get(["status"]) || null;
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
|
|
||||||
let statusId;
|
|
||||||
if (status == "last" || !status) {
|
|
||||||
statusId = null;
|
|
||||||
} else if (status == "finished") {
|
|
||||||
statusId = 1;
|
|
||||||
} else if (status == "ongoing") {
|
|
||||||
statusId = 2;
|
|
||||||
} else if (status == "announce") {
|
|
||||||
statusId = 3;
|
|
||||||
} else {
|
|
||||||
return NextResponse.json({ message: "Bad status" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = new URL(`${ENDPOINTS.filter}/${page}`);
|
|
||||||
if (token) {
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
country: null,
|
|
||||||
season: null,
|
|
||||||
sort: 0,
|
|
||||||
studio: null,
|
|
||||||
age_ratings: [],
|
|
||||||
category_id: null,
|
|
||||||
end_year: null,
|
|
||||||
episode_duration_from: null,
|
|
||||||
episode_duration_to: null,
|
|
||||||
episodes_from: null,
|
|
||||||
episodes_to: null,
|
|
||||||
genres: [],
|
|
||||||
profile_list_exclusions: [],
|
|
||||||
start_year: null,
|
|
||||||
status_id: statusId,
|
|
||||||
types: [],
|
|
||||||
is_genres_exclude_mode_enabled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetchDataViaPost(url.toString(), data);
|
|
||||||
return NextResponse.json(response);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaGet } from "@/app/api/utils";
|
|
||||||
import { ENDPOINTS } from "@/app/api/config";
|
|
||||||
|
|
||||||
export async function GET(request, params) {
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
let url = new URL(`${ENDPOINTS.user.profile}/${params["params"]["id"]}`);
|
|
||||||
if (token) {
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchDataViaGet(url.toString());
|
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { authorize } from "@/app/api/utils";
|
|
||||||
import { ENDPOINTS } from "@/app/api/config";
|
|
||||||
|
|
||||||
export async function POST(request) {
|
|
||||||
const response = await authorize(ENDPOINTS.auth, 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);
|
|
||||||
}
|
|
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);
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
import { fetchDataViaPost } from "../utils";
|
|
||||||
import { ENDPOINTS } from "../config";
|
|
||||||
|
|
||||||
export async function GET(request) {
|
|
||||||
const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
|
|
||||||
const query = request.nextUrl.searchParams.get(["q"]) || null;
|
|
||||||
const token = request.nextUrl.searchParams.get(["token"]) || null;
|
|
||||||
let url = new URL(`${ENDPOINTS.search}/${page}`);
|
|
||||||
if (token) {
|
|
||||||
url.searchParams.set("token", token);
|
|
||||||
}
|
|
||||||
const data = { query, searchBy: 0 };
|
|
||||||
|
|
||||||
const response = await fetchDataViaPost(url.toString(), data, true);
|
|
||||||
if (!response) {
|
|
||||||
return NextResponse.json({ message: "Bad request" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(response);
|
|
||||||
}
|
|
39
app/api/search/route.ts
Normal file
39
app/api/search/route.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { fetchDataViaPost } from "../utils";
|
||||||
|
import { ENDPOINTS } from "../config";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const page = parseInt(request.nextUrl.searchParams.get("page")) || 0;
|
||||||
|
const query = request.nextUrl.searchParams.get("q") || null;
|
||||||
|
const token = request.nextUrl.searchParams.get("token") || null;
|
||||||
|
|
||||||
|
const where = request.nextUrl.searchParams.get("where") || "releases"
|
||||||
|
const searchBy = parseInt(request.nextUrl.searchParams.get("searchBy")) || 0
|
||||||
|
const list = parseInt(request.nextUrl.searchParams.get("list")) || null
|
||||||
|
|
||||||
|
let url: URL;
|
||||||
|
|
||||||
|
if (where == "releases") {
|
||||||
|
url = new URL(`${ENDPOINTS.search}/releases/${page}`);
|
||||||
|
} else if (where == "list") {
|
||||||
|
if (!list) { return NextResponse.json({ message: "List ID required" }, { status: 400 })}
|
||||||
|
if (!token) { return NextResponse.json({ message: "token required" }, { status: 400 })}
|
||||||
|
url = new URL(`${ENDPOINTS.search}/profile/list/${list}/${page}`);
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
url.searchParams.set("token", token);
|
||||||
|
}
|
||||||
|
const data = { query, searchBy };
|
||||||
|
|
||||||
|
const response = await fetchDataViaPost(
|
||||||
|
url.toString(),
|
||||||
|
JSON.stringify(data),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!response) {
|
||||||
|
return NextResponse.json({ message: "Bad request" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(response);
|
||||||
|
}
|
126
app/api/utils.js
126
app/api/utils.js
|
@ -1,126 +0,0 @@
|
||||||
import { USER_AGENT } from "./config";
|
|
||||||
export const HEADERS = {
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"Content-Type": "application/json; charset=UTF-8",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchDataViaGet = async (url, API_V2) => {
|
|
||||||
if (API_V2) {
|
|
||||||
HEADERS["API-Version"] = "v2";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: HEADERS,
|
|
||||||
});
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error("Error fetching data");
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchDataViaPost = async (url, body, API_V2) => {
|
|
||||||
if (API_V2) {
|
|
||||||
HEADERS["API-Version"] = "v2";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: HEADERS,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error("Error fetching data");
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const authorize = async (url, data) => {
|
|
||||||
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, jwt) {
|
|
||||||
const data = { jwt: jwt, user_id: user_id };
|
|
||||||
localStorage.setItem("JWT", JSON.stringify(data));
|
|
||||||
}
|
|
||||||
export function getJWT() {
|
|
||||||
const data = localStorage.getItem("JWT");
|
|
||||||
return JSON.parse(data);
|
|
||||||
}
|
|
||||||
export function removeJWT() {
|
|
||||||
localStorage.removeItem("JWT");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function numberDeclension(number, one, two, five) {
|
|
||||||
if (number > 10 && [11, 12, 13, 14].includes(number % 100)) return five;
|
|
||||||
let last_num = number % 10;
|
|
||||||
if (last_num == 1) return one;
|
|
||||||
if ([2, 3, 4].includes(last_num)) return two;
|
|
||||||
if ([5, 6, 7, 8, 9, 0].includes(last_num)) return five;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unixToDate(unix) {
|
|
||||||
const date = new Date(unix * 1000);
|
|
||||||
return date.toLocaleString("ru-RU");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sinceUnixDate(unixInSeconds) {
|
|
||||||
const unix = Math.floor(unixInSeconds * 1000);
|
|
||||||
const date = new Date(unix);
|
|
||||||
const currentDate = new Date().valueOf();
|
|
||||||
const dateDifferenceSeconds = new Date(currentDate - unix) / 1000;
|
|
||||||
|
|
||||||
const minutes = Math.floor(dateDifferenceSeconds / 60)
|
|
||||||
const hours = Math.floor(dateDifferenceSeconds / 3600);
|
|
||||||
const days = Math.floor(dateDifferenceSeconds / 86400);
|
|
||||||
|
|
||||||
const minutesName = numberDeclension(minutes, "минута", "минуты", "минут");
|
|
||||||
const hoursName = numberDeclension(hours, "час", "часа", "часов");
|
|
||||||
const daysName = numberDeclension(days, "день", "дня", "дней");
|
|
||||||
|
|
||||||
if (dateDifferenceSeconds < 60) return "менее минуты назад";
|
|
||||||
if (dateDifferenceSeconds < 3600)
|
|
||||||
return `${minutes} ${minutesName} назад`;
|
|
||||||
if (dateDifferenceSeconds < 86400)
|
|
||||||
return `${hours} ${hoursName} назад`;
|
|
||||||
if (dateDifferenceSeconds < 2592000)
|
|
||||||
return `${days} ${daysName} назад`;
|
|
||||||
|
|
||||||
return date.toLocaleString("ru-RU").split(",")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function minutesToTime(min) {
|
|
||||||
const d = Math.floor(min / 1440); // 60*24
|
|
||||||
const h = Math.floor((min - d * 1440) / 60);
|
|
||||||
const m = Math.round(min % 60);
|
|
||||||
|
|
||||||
var dDisplay = d > 0 ? `${d} ${numberDeclension(d, "день", "дня", "дней")}, ` : "";
|
|
||||||
var hDisplay = h > 0 ? `${h} ${numberDeclension(h, "час", "часа", "часов")}, ` : "";
|
|
||||||
var mDisplay = m > 0 ? `${m} ${numberDeclension(m, "минута", "минуты", "минут")}` : "";
|
|
||||||
return dDisplay + hDisplay + mDisplay;
|
|
||||||
}
|
|
249
app/api/utils.ts
Normal file
249
app/api/utils.ts
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
import { USER_AGENT, ENDPOINTS } from "./config";
|
||||||
|
export const HEADERS = {
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchDataViaGet = async (
|
||||||
|
url: string,
|
||||||
|
API_V2: string | boolean = false
|
||||||
|
) => {
|
||||||
|
if (API_V2) {
|
||||||
|
HEADERS["API-Version"] = "v2";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: HEADERS,
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Error fetching data");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchDataViaPost = async (
|
||||||
|
url: string,
|
||||||
|
body: string,
|
||||||
|
API_V2: string | boolean = false
|
||||||
|
) => {
|
||||||
|
if (API_V2) {
|
||||||
|
HEADERS["API-Version"] = "v2";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: HEADERS,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("Error fetching data");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(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) {
|
||||||
|
const data = { jwt: jwt, user_id: user_id };
|
||||||
|
localStorage.setItem("JWT", JSON.stringify(data));
|
||||||
|
}
|
||||||
|
export function getJWT() {
|
||||||
|
const data = localStorage.getItem("JWT");
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
export function removeJWT() {
|
||||||
|
localStorage.removeItem("JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberDeclension(
|
||||||
|
number: number,
|
||||||
|
one: string,
|
||||||
|
two: string,
|
||||||
|
five: string
|
||||||
|
) {
|
||||||
|
if (number > 10 && [11, 12, 13, 14].includes(number % 100)) return five;
|
||||||
|
let last_num = number % 10;
|
||||||
|
if (last_num == 1) return one;
|
||||||
|
if ([2, 3, 4].includes(last_num)) return two;
|
||||||
|
if ([5, 6, 7, 8, 9, 0].includes(last_num)) return five;
|
||||||
|
}
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
"янв.",
|
||||||
|
"фев.",
|
||||||
|
"мар.",
|
||||||
|
"апр.",
|
||||||
|
"мая",
|
||||||
|
"июня",
|
||||||
|
"июля",
|
||||||
|
"авг.",
|
||||||
|
"сен.",
|
||||||
|
"окт.",
|
||||||
|
"ноя.",
|
||||||
|
"дек.",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function unixToDate(unix: number) {
|
||||||
|
const date = new Date(unix * 1000);
|
||||||
|
return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSeasonFromUnix = (unix: number) => {
|
||||||
|
const date = new Date(unix * 1000);
|
||||||
|
const month = date.getMonth();
|
||||||
|
if (month >= 3 && month <= 5) return "весна";
|
||||||
|
if (month >= 6 && month <= 8) return "лето";
|
||||||
|
if (month >= 9 && month <= 11) return "осень";
|
||||||
|
return "зима";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sinceUnixDate(unixInSeconds: number) {
|
||||||
|
const unix = Math.floor(unixInSeconds * 1000);
|
||||||
|
const date = new Date(unix);
|
||||||
|
const currentDate = new Date().valueOf();
|
||||||
|
const dateDifferenceSeconds = new Date(currentDate - unix).getTime() / 1000;
|
||||||
|
|
||||||
|
const minutes = Math.floor(dateDifferenceSeconds / 60);
|
||||||
|
const hours = Math.floor(dateDifferenceSeconds / 3600);
|
||||||
|
const days = Math.floor(dateDifferenceSeconds / 86400);
|
||||||
|
|
||||||
|
const minutesName = numberDeclension(minutes, "минута", "минуты", "минут");
|
||||||
|
const hoursName = numberDeclension(hours, "час", "часа", "часов");
|
||||||
|
const daysName = numberDeclension(days, "день", "дня", "дней");
|
||||||
|
|
||||||
|
if (dateDifferenceSeconds < 60) return "менее минуты назад";
|
||||||
|
if (dateDifferenceSeconds < 3600) return `${minutes} ${minutesName} назад`;
|
||||||
|
if (dateDifferenceSeconds < 86400) return `${hours} ${hoursName} назад`;
|
||||||
|
if (dateDifferenceSeconds < 2592000) return `${days} ${daysName} назад`;
|
||||||
|
|
||||||
|
return date.getDate() + " " + months[date.getMonth()] + " " + date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minutesToTime(min: number) {
|
||||||
|
const d = Math.floor(min / 1440); // 60*24
|
||||||
|
const h = Math.floor((min - d * 1440) / 60);
|
||||||
|
const m = Math.round(min % 60);
|
||||||
|
|
||||||
|
var dDisplay =
|
||||||
|
d > 0 ? `${d} ${numberDeclension(d, "день", "дня", "дней")}, ` : "";
|
||||||
|
var hDisplay =
|
||||||
|
h > 0 ? `${h} ${numberDeclension(h, "час", "часа", "часов")}, ` : "";
|
||||||
|
var mDisplay =
|
||||||
|
m > 0 ? `${m} ${numberDeclension(m, "минута", "минуты", "минут")}` : "";
|
||||||
|
return dDisplay + hDisplay + mDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusList: Record<string, null | number> = {
|
||||||
|
last: null,
|
||||||
|
finished: 1,
|
||||||
|
ongoing: 2,
|
||||||
|
announce: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function _FetchHomePageReleases(
|
||||||
|
status: string,
|
||||||
|
token: string | null,
|
||||||
|
page: string | number = 0
|
||||||
|
) {
|
||||||
|
let statusId: null | number = null;
|
||||||
|
let categoryId: null | number = null;
|
||||||
|
if (status == "films") {
|
||||||
|
categoryId = 2;
|
||||||
|
} else {
|
||||||
|
statusId = StatusList[status];
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
country: null,
|
||||||
|
season: null,
|
||||||
|
sort: 0,
|
||||||
|
studio: null,
|
||||||
|
age_ratings: [],
|
||||||
|
category_id: categoryId,
|
||||||
|
end_year: null,
|
||||||
|
episode_duration_from: null,
|
||||||
|
episode_duration_to: null,
|
||||||
|
episodes_from: null,
|
||||||
|
episodes_to: null,
|
||||||
|
genres: [],
|
||||||
|
profile_list_exclusions: [],
|
||||||
|
start_year: null,
|
||||||
|
status_id: statusId,
|
||||||
|
types: [],
|
||||||
|
is_genres_exclude_mode_enabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
url = `${ENDPOINTS.filter}/${page}`;
|
||||||
|
if (token) {
|
||||||
|
url += `?token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Object = fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("Error fetching data");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data: Object) => {
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BookmarksList = {
|
||||||
|
watching: 1,
|
||||||
|
planned: 2,
|
||||||
|
watched: 3,
|
||||||
|
delayed: 4,
|
||||||
|
abandoned: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SortList = {
|
||||||
|
adding_descending: 1,
|
||||||
|
adding_ascending: 2,
|
||||||
|
year_descending: 3,
|
||||||
|
year_ascending: 4,
|
||||||
|
alphabet_descending: 5,
|
||||||
|
alphabet_ascending: 6,
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { BookmarksCategoryPage } from "@/app/pages/BookmarksCategory";
|
import { BookmarksCategoryPage } from "#/pages/BookmarksCategory";
|
||||||
|
|
||||||
const SectionTitleMapping = {
|
const SectionTitleMapping = {
|
||||||
watching: "Смотрю",
|
watching: "Смотрю",
|
|
@ -2,7 +2,7 @@ export const metadata = {
|
||||||
title: "Закладки",
|
title: "Закладки",
|
||||||
};
|
};
|
||||||
|
|
||||||
import { BookmarksPage } from "@/app/pages/Bookmarks";
|
import { BookmarksPage } from "#/pages/Bookmarks";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return <BookmarksPage />;
|
return <BookmarksPage />;
|
|
@ -1,12 +0,0 @@
|
||||||
export const Chip = (props) => {
|
|
||||||
return (
|
|
||||||
<div className={`rounded-sm ${props.bg_color || "bg-gray-500"}`}>
|
|
||||||
<p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
|
|
||||||
{props.name}
|
|
||||||
{props.name && props.devider ? props.devider : " "}
|
|
||||||
{props.name_2}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
16
app/components/Chip/Chip.tsx
Normal file
16
app/components/Chip/Chip.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export const Chip = (props: {
|
||||||
|
name?: string;
|
||||||
|
name_2?: string;
|
||||||
|
devider?: string;
|
||||||
|
bg_color?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-sm ${props.bg_color || "bg-gray-500"}`}>
|
||||||
|
<p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
|
||||||
|
{props.name}
|
||||||
|
{props.name && props.devider ? props.devider : " "}
|
||||||
|
{props.name_2}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useUserStore } from "@/app/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { Dropdown } from "flowbite-react";
|
import { Dropdown } from "flowbite-react";
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const userStore = useUserStore((state) => state);
|
const userStore: any = useUserStore((state) => state);
|
||||||
|
|
||||||
const isNotAuthorizedStyle = "text-gray-700";
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -107,7 +106,7 @@ export const Navbar = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dropdown.Item className="text-sm md:text-base">
|
<Dropdown.Item className="text-sm md:text-base">
|
||||||
<Link href="/profile" className="flex items-center gap-1">
|
<Link href={`/profile/${userStore.user.id}`} className="flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
className={`iconify ${pathname == `/profile/${userStore.user.id}` ? "font-bold mdi--user" : "mdi--user-outline"} w-4 h-4 sm:w-6 sm:h-6`}
|
className={`iconify ${pathname == `/profile/${userStore.user.id}` ? "font-bold mdi--user" : "mdi--user-outline"} w-4 h-4 sm:w-6 sm:h-6`}
|
||||||
></span>
|
></span>
|
|
@ -1,7 +1,7 @@
|
||||||
import { numberDeclension } from "@/app/api/utils";
|
import { numberDeclension } from "#/api/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const RelatedSection = (props) => {
|
export const RelatedSection = (props: any) => {
|
||||||
const declension = numberDeclension(
|
const declension = numberDeclension(
|
||||||
props.release_count,
|
props.release_count,
|
||||||
"релиз",
|
"релиз",
|
|
@ -9,9 +9,13 @@ import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import { Navigation } from "swiper/modules";
|
import { Navigation } from "swiper/modules";
|
||||||
|
|
||||||
export const ReleaseCourusel = (props) => {
|
export const ReleaseCourusel = (props: {
|
||||||
|
sectionTitle: string;
|
||||||
|
showAllLink?: string;
|
||||||
|
content: any;
|
||||||
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = {
|
const options: any = {
|
||||||
direction: "horizontal",
|
direction: "horizontal",
|
||||||
spaceBetween: 8,
|
spaceBetween: 8,
|
||||||
allowTouchMove: true,
|
allowTouchMove: true,
|
||||||
|
@ -39,12 +43,14 @@ export const ReleaseCourusel = (props) => {
|
||||||
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
||||||
{props.sectionTitle}
|
{props.sectionTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
{props.showAllLink && (
|
||||||
<Link href={props.showAllLink}>
|
<Link href={props.showAllLink}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="hidden text-xl font-bold sm:block">Показать все</p>
|
<p className="hidden text-xl font-bold sm:block">Показать все</p>
|
||||||
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<div className="swiper">
|
<div className="swiper">
|
||||||
|
@ -65,11 +71,15 @@ export const ReleaseCourusel = (props) => {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`swiper-button-prev ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-left aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
className={`swiper-button-prev ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-left aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
||||||
style={{ "--swiper-navigation-size": "64px" }}
|
style={
|
||||||
|
{ "--swiper-navigation-size": "64px" } as React.CSSProperties
|
||||||
|
}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`swiper-button-next ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-right aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
className={`swiper-button-next ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-right aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
||||||
style={{ "--swiper-navigation-size": "64px" }}
|
style={
|
||||||
|
{ "--swiper-navigation-size": "64px" } as React.CSSProperties
|
||||||
|
}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
21
app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx
Normal file
21
app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
// const searchBy = {
|
||||||
|
// title: 0,
|
||||||
|
// studio: 1,
|
||||||
|
// director: 2,
|
||||||
|
// author: 3,
|
||||||
|
// genre: 4
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: сделать какую-нибудь анимацию на ссылке при наведении и фокусе
|
||||||
|
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string | number | null }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="underline"
|
||||||
|
href={`/search?q=${props.title}&searchBy=${props.searchBy}`}
|
||||||
|
>
|
||||||
|
{props.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,9 +1,7 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { sinceUnixDate } from "@/app/api/utils";
|
import { sinceUnixDate } from "#/api/utils";
|
||||||
import { Chip } from "@/app/components/Chip/Chip";
|
import { Chip } from "#/components/Chip/Chip";
|
||||||
|
|
||||||
export const ReleaseLink = (props) => {
|
|
||||||
const grade = props.grade.toFixed(1);
|
|
||||||
const profile_lists = {
|
const profile_lists = {
|
||||||
// 0: "Не смотрю",
|
// 0: "Не смотрю",
|
||||||
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
||||||
|
@ -13,6 +11,8 @@ export const ReleaseLink = (props) => {
|
||||||
5: { name: "Брошено", bg_color: "bg-red-500" },
|
5: { name: "Брошено", bg_color: "bg-red-500" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ReleaseLink169 = (props: any) => {
|
||||||
|
const grade = props.grade.toFixed(1);
|
||||||
const profile_list_status = props.profile_list_status;
|
const profile_list_status = props.profile_list_status;
|
||||||
let user_list = null;
|
let user_list = null;
|
||||||
if (profile_list_status != null || profile_list_status != 0) {
|
if (profile_list_status != null || profile_list_status != 0) {
|
||||||
|
@ -79,6 +79,7 @@ export const ReleaseLink = (props) => {
|
||||||
devider=", "
|
devider=", "
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{props.category && <Chip name={props.category.name} />}
|
||||||
{props.is_favorite && (
|
{props.is_favorite && (
|
||||||
<div className="flex items-center justify-center bg-pink-500 rounded-sm">
|
<div className="flex items-center justify-center bg-pink-500 rounded-sm">
|
||||||
<span className="w-3 px-4 py-2.5 text-white sm:px-4 sm:py-3 xl:px-6 xl:py-4 iconify mdi--heart"></span>
|
<span className="w-3 px-4 py-2.5 text-white sm:px-4 sm:py-3 xl:px-6 xl:py-4 iconify mdi--heart"></span>
|
82
app/components/ReleaseLink/ReleaseLink.Poster.tsx
Normal file
82
app/components/ReleaseLink/ReleaseLink.Poster.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { sinceUnixDate } from "#/api/utils";
|
||||||
|
import { Chip } from "#/components/Chip/Chip";
|
||||||
|
|
||||||
|
const profile_lists = {
|
||||||
|
// 0: "Не смотрю",
|
||||||
|
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
||||||
|
2: { name: "В планах", bg_color: "bg-purple-500" },
|
||||||
|
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
|
||||||
|
4: { name: "Отложено", bg_color: "bg-yellow-500" },
|
||||||
|
5: { name: "Брошено", bg_color: "bg-red-500" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReleaseLinkPoster = (props: any) => {
|
||||||
|
const grade = props.grade.toFixed(1);
|
||||||
|
const profile_list_status = props.profile_list_status;
|
||||||
|
let user_list = null;
|
||||||
|
if (profile_list_status != null || profile_list_status != 0) {
|
||||||
|
user_list = profile_lists[profile_list_status];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link href={`/release/${props.id}`}>
|
||||||
|
<div className="flex flex-col w-full h-full gap-4 lg:flex-row">
|
||||||
|
<div
|
||||||
|
className="relative w-full h-64 gap-8 p-4 overflow-hidden bg-white bg-center bg-no-repeat bg-cover border border-gray-200 rounded-lg shadow-md lg:min-w-[300px] lg:min-h-[385px] lg:max-w-[300px] lg:max-h-[385px] lg:bg-top dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${props.image})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<Chip
|
||||||
|
bg_color={
|
||||||
|
props.grade.toFixed(1) == 0
|
||||||
|
? "hidden"
|
||||||
|
: props.grade.toFixed(1) < 2
|
||||||
|
? "bg-red-500"
|
||||||
|
: props.grade.toFixed(1) < 3
|
||||||
|
? "bg-orange-500"
|
||||||
|
: props.grade.toFixed(1) < 4
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-green-500"
|
||||||
|
}
|
||||||
|
name={props.grade.toFixed(1)}
|
||||||
|
/>
|
||||||
|
{props.status ? (
|
||||||
|
<Chip name={props.status.name} />
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
name={
|
||||||
|
props.status_id == 1
|
||||||
|
? "Завершено"
|
||||||
|
: props.status_id == 2
|
||||||
|
? "Онгоинг"
|
||||||
|
: "Анонс"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Chip
|
||||||
|
name={props.episodes_released && props.episodes_released}
|
||||||
|
name_2={
|
||||||
|
props.episodes_total ? props.episodes_total + " эп." : "? эп."
|
||||||
|
}
|
||||||
|
devider="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute flex flex-col gap-2 text-white bottom-4">
|
||||||
|
{props.title_ru && (
|
||||||
|
<p className="text-xl font-bold text-white md:text-2xl">
|
||||||
|
{props.title_ru}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{props.title_original && (
|
||||||
|
<p className="text-sm text-gray-300 md:text-base">
|
||||||
|
{props.title_original}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
13
app/components/ReleaseLink/ReleaseLink.tsx
Normal file
13
app/components/ReleaseLink/ReleaseLink.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ReleaseLink169 } from "./ReleaseLink.16_9";
|
||||||
|
import { ReleaseLinkPoster } from "./ReleaseLink.Poster";
|
||||||
|
|
||||||
|
export const ReleaseLink = (props: any) => {
|
||||||
|
const type = props.type || "16_9";
|
||||||
|
|
||||||
|
if (type == "16_9") {
|
||||||
|
return <ReleaseLink169 {...props} />;
|
||||||
|
}
|
||||||
|
if (type == "poster") {
|
||||||
|
return <ReleaseLinkPoster {...props} />;
|
||||||
|
}
|
||||||
|
};
|
174
app/components/ReleasePlayer/ReleasePlayer.tsx
Normal file
174
app/components/ReleasePlayer/ReleasePlayer.tsx
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { Card, Dropdown, Button } from "flowbite-react";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const DropdownTheme = {
|
||||||
|
floating: {
|
||||||
|
target:
|
||||||
|
"w-full md:w-[256px] bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonThemeInactive =
|
||||||
|
"bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800";
|
||||||
|
const ButtonThemeActive =
|
||||||
|
"bg-blue-800 dark:bg-blue-600 disabled:opacity-100 dark:disabled:opacity-100";
|
||||||
|
|
||||||
|
async function _fetch(url: string) {
|
||||||
|
const data = fetch(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error("Error fetching data");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlayer = (props: { id: number }) => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const [voiceoverInfo, setVoiceoverInfo] = useState(null);
|
||||||
|
const [selectedVoiceover, setSelectedVoiceover] = useState(null);
|
||||||
|
const [sourcesInfo, setSourcesInfo] = useState(null);
|
||||||
|
const [selectedSource, setSelectedSource] = useState(null);
|
||||||
|
const [episodeInfo, setEpisodeInfo] = useState(null);
|
||||||
|
const [selectedEpisode, setSelectedEpisode] = useState(null);
|
||||||
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _fetchInfo() {
|
||||||
|
const voiceover = await _fetch(
|
||||||
|
`${ENDPOINTS.release.episode}/${props.id}`
|
||||||
|
);
|
||||||
|
setVoiceoverInfo(voiceover.types);
|
||||||
|
setSelectedVoiceover(voiceover.types[0]);
|
||||||
|
}
|
||||||
|
_fetchInfo();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _fetchInfo() {
|
||||||
|
const sources = await _fetch(
|
||||||
|
`${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}`
|
||||||
|
);
|
||||||
|
setSourcesInfo(sources.sources);
|
||||||
|
setSelectedSource(sources.sources[0]);
|
||||||
|
}
|
||||||
|
if (selectedVoiceover) {
|
||||||
|
_fetchInfo();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedVoiceover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _fetchInfo(url: string) {
|
||||||
|
const episodes = await _fetch(url);
|
||||||
|
|
||||||
|
setEpisodeInfo(episodes.episodes);
|
||||||
|
setSelectedEpisode(episodes.episodes[0]);
|
||||||
|
}
|
||||||
|
if (selectedSource) {
|
||||||
|
let url = `${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}`;
|
||||||
|
if (token) {
|
||||||
|
url = `${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`;
|
||||||
|
}
|
||||||
|
_fetchInfo(url);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedSource, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _fetchInfo() {
|
||||||
|
_fetch(`${ENDPOINTS.statistic.addHistory}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`);
|
||||||
|
_fetch(`${ENDPOINTS.statistic.markWatched}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`);
|
||||||
|
}
|
||||||
|
if (selectedEpisode && !isFirstLoad && token) {
|
||||||
|
_fetchInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstLoad) {
|
||||||
|
setIsFirstLoad(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedEpisode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
{!voiceoverInfo || !sourcesInfo || !episodeInfo ? (
|
||||||
|
<div className="flex items-center justify-center aspect-video">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Dropdown
|
||||||
|
label={`Озвучка: ${selectedVoiceover.name}`}
|
||||||
|
theme={DropdownTheme}
|
||||||
|
>
|
||||||
|
{voiceoverInfo.map((voiceover: any) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={voiceover.id}
|
||||||
|
onClick={() => setSelectedVoiceover(voiceover)}
|
||||||
|
>
|
||||||
|
{voiceover.name}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
label={`Плеер: ${selectedSource.name}`}
|
||||||
|
theme={DropdownTheme}
|
||||||
|
>
|
||||||
|
{sourcesInfo.map((source: any) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={source.id}
|
||||||
|
onClick={() => setSelectedSource(source)}
|
||||||
|
>
|
||||||
|
{source.name}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="aspect-video">
|
||||||
|
<iframe
|
||||||
|
allowFullScreen={true}
|
||||||
|
src={selectedEpisode.url}
|
||||||
|
className="w-full h-full rounded-md"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2 p-2 overflow-x-auto scrollbar-thin">
|
||||||
|
{episodeInfo.map((episode: any) => (
|
||||||
|
<Button
|
||||||
|
className={`text-center min-w-fit ${
|
||||||
|
selectedEpisode.position === episode.position
|
||||||
|
? ButtonThemeActive
|
||||||
|
: ButtonThemeInactive
|
||||||
|
}`}
|
||||||
|
key={episode.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEpisode(episode);
|
||||||
|
episode.is_watched = true;
|
||||||
|
}}
|
||||||
|
disabled={selectedEpisode.position === episode.position}
|
||||||
|
>
|
||||||
|
{episode.position} серия
|
||||||
|
{episode.is_watched && (
|
||||||
|
<span className="w-5 h-5 ml-2 iconify material-symbols--check-circle"></span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
|
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
|
||||||
|
|
||||||
export const ReleaseSection = (props) => {
|
export const ReleaseSection = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
{props.sectionTitle && (
|
{props.sectionTitle && (
|
|
@ -2,7 +2,7 @@ export const metadata = {
|
||||||
title: "Избранное",
|
title: "Избранное",
|
||||||
};
|
};
|
||||||
|
|
||||||
import { FavoritesPage } from "@/app/pages/Favorites";
|
import { FavoritesPage } from "#/pages/Favorites";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return <FavoritesPage />;
|
return <FavoritesPage />;
|
|
@ -2,7 +2,7 @@ export const metadata = {
|
||||||
title: "История",
|
title: "История",
|
||||||
};
|
};
|
||||||
|
|
||||||
import { HistoryPage } from "@/app/pages/History";
|
import { HistoryPage } from "#/pages/History";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return <HistoryPage />;
|
return <HistoryPage />;
|
|
@ -1,4 +1,4 @@
|
||||||
import { IndexCategoryPage } from "@/app/pages/IndexCategory";
|
import { IndexCategoryPage } from "#/pages/IndexCategory";
|
||||||
|
|
||||||
const SectionTitleMapping = {
|
const SectionTitleMapping = {
|
||||||
last: "Последние релизы",
|
last: "Последние релизы",
|
|
@ -1,5 +1,5 @@
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { App } from "@/app/App";
|
import { App } from "./App";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: {
|
title: {
|
|
@ -1,4 +1,4 @@
|
||||||
import { LoginPage } from "@/app/pages/Login";
|
import { LoginPage } from "#/pages/Login";
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
return <LoginPage />;
|
return <LoginPage />;
|
||||||
}
|
}
|
|
@ -1,16 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { ReleaseCourusel } from "@/app/components/ReleaseCourusel/ReleaseCourusel";
|
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
const fetcher = (...args) => fetch(...args).then((res) => res.json());
|
const fetcher = (...args: any) =>
|
||||||
import { useUserStore } from "@/app/store/auth";
|
fetch([...args] as any).then((res) => res.json());
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { BookmarksList } from "#/api/utils";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
|
||||||
export function BookmarksPage() {
|
export function BookmarksPage() {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
|
|
||||||
function useFetchReleases(list) {
|
function useFetchReleases(listName: string) {
|
||||||
let url;
|
let url: string;
|
||||||
url = `/api/bookmarks?list=${list}&token=${token}`;
|
|
||||||
|
if (token) {
|
||||||
|
url = `${ENDPOINTS.user.bookmark}/all/${BookmarksList[listName]}/0?token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = useSWR(url, fetcher);
|
const { data } = useSWR(url, fetcher);
|
||||||
return [data];
|
return [data];
|
||||||
|
@ -58,9 +64,7 @@ export function BookmarksPage() {
|
||||||
content={watchedData.content}
|
content={watchedData.content}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{delayedData &&
|
{delayedData && delayedData.content && delayedData.content.length > 0 && (
|
||||||
delayedData.content &&
|
|
||||||
delayedData.content.length > 0 && (
|
|
||||||
<ReleaseCourusel
|
<ReleaseCourusel
|
||||||
sectionTitle="Отложено"
|
sectionTitle="Отложено"
|
||||||
showAllLink="/bookmarks/delayed"
|
showAllLink="/bookmarks/delayed"
|
|
@ -1,34 +1,37 @@
|
||||||
"use client";
|
"use client";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "@/app/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { Dropdown } from "flowbite-react";
|
import { Dropdown } from "flowbite-react";
|
||||||
import { sort } from "./common";
|
import { sort } from "./common";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { BookmarksList, SortList } from "#/api/utils";
|
||||||
|
|
||||||
const fetcher = async (url) => {
|
const fetcher = async (url: string) => {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = new Error("An error occurred while fetching the data.");
|
const error = new Error(`An error occurred while fetching the data. status: ${res.status}`);
|
||||||
error.info = await res.json();
|
error.message = await res.json();
|
||||||
error.status = res.status;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BookmarksCategoryPage(props) {
|
export function BookmarksCategoryPage(props: any) {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
const [selectedSort, setSelectedSort] = useState(0);
|
const [selectedSort, setSelectedSort] = useState(0);
|
||||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
return `/api/bookmarks?list=${props.slug}&page=${pageIndex}&token=${token}&sort=${sort.values[selectedSort].value}`;
|
if (token) {
|
||||||
|
return `${ENDPOINTS.user.bookmark}/all/${BookmarksList[props.slug]}/${pageIndex}?token=${token}&sort=${sort.values[selectedSort].id}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
@ -1,20 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "@/app/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { Dropdown } from "flowbite-react";
|
import { Dropdown } from "flowbite-react";
|
||||||
import { sort } from "./common";
|
import { sort } from "./common";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
|
||||||
const fetcher = async (url) => {
|
const fetcher = async (url: string) => {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = new Error("An error occurred while fetching the data.");
|
const error = new Error(`An error occurred while fetching the data. status: ${res.status}`);
|
||||||
error.info = await res.json();
|
error.message = await res.json();
|
||||||
error.status = res.status;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +26,11 @@ export function FavoritesPage() {
|
||||||
const [selectedSort, setSelectedSort] = useState(0);
|
const [selectedSort, setSelectedSort] = useState(0);
|
||||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
return `/api/favorites?page=${pageIndex}&token=${token}&sort=${sort.values[selectedSort].value}`;
|
if (token) {
|
||||||
|
return `${ENDPOINTS.user.favorite}/all/${pageIndex}?token=${token}&sort=${sort.values[selectedSort].id}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
@ -1,18 +1,18 @@
|
||||||
"use client";
|
"use client";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "@/app/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
|
||||||
const fetcher = async (url) => {
|
const fetcher = async (url: string) => {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = new Error("An error occurred while fetching the data.");
|
const error = new Error(`An error occurred while fetching the data. status: ${res.status}`);
|
||||||
error.info = await res.json();
|
error.message = await res.json();
|
||||||
error.status = res.status;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +23,11 @@ export function HistoryPage() {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
return `/api/history?page=${pageIndex}&token=${token}`;
|
if (token) {
|
||||||
|
return `${ENDPOINTS.user.history}/${pageIndex}?token=${token}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
@ -1,64 +0,0 @@
|
||||||
"use client";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { ReleaseCourusel } from "@/app/components/ReleaseCourusel/ReleaseCourusel";
|
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
|
||||||
const fetcher = (...args) => fetch(...args).then((res) => res.json());
|
|
||||||
import { useUserStore } from "@/app/store/auth";
|
|
||||||
|
|
||||||
export function IndexPage() {
|
|
||||||
const userStore = useUserStore((state) => state);
|
|
||||||
const token = userStore.token;
|
|
||||||
|
|
||||||
function useFetchReleases(status) {
|
|
||||||
let url;
|
|
||||||
|
|
||||||
url = `/api/home?status=${status}`;
|
|
||||||
if (token) {
|
|
||||||
url += `&token=${token}`;
|
|
||||||
}
|
|
||||||
const { data } = useSWR(url, fetcher);
|
|
||||||
return [data];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [lastReleasesData] = useFetchReleases("last");
|
|
||||||
const [finishedReleasesData] = useFetchReleases("finished");
|
|
||||||
const [ongoingReleasesData] = useFetchReleases("ongoing");
|
|
||||||
const [announceReleasesData] = useFetchReleases("announce");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="container flex flex-col pt-2 pb-16 mx-auto sm:pt-4 sm:pb-0">
|
|
||||||
{lastReleasesData ? (
|
|
||||||
<ReleaseCourusel
|
|
||||||
sectionTitle="Последние релизы"
|
|
||||||
showAllLink="/home/last"
|
|
||||||
content={lastReleasesData.content}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{finishedReleasesData && (
|
|
||||||
<ReleaseCourusel
|
|
||||||
sectionTitle="Завершенные релизы"
|
|
||||||
showAllLink="/home/finished"
|
|
||||||
content={finishedReleasesData.content}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{ongoingReleasesData && (
|
|
||||||
<ReleaseCourusel
|
|
||||||
sectionTitle="В эфире"
|
|
||||||
showAllLink="/home/ongoing"
|
|
||||||
content={ongoingReleasesData.content}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{announceReleasesData && (
|
|
||||||
<ReleaseCourusel
|
|
||||||
sectionTitle="Анонсированные релизы"
|
|
||||||
showAllLink="/home/announce"
|
|
||||||
content={announceReleasesData.content}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
94
app/pages/Index.tsx
Normal file
94
app/pages/Index.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
"use client";
|
||||||
|
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { _FetchHomePageReleases } from "#/api/utils";
|
||||||
|
|
||||||
|
export function IndexPage() {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [lastReleasesData, setLastReleasesData] = useState(null);
|
||||||
|
const [ongoingReleasesData, setOngoingReleasesData] = useState(null);
|
||||||
|
const [finishedReleasesData, setFinishedReleasesData] = useState(null);
|
||||||
|
const [announceReleasesData, setAnnounceReleasesData] = useState(null);
|
||||||
|
const [filmsReleasesData, setFilmsReleasesData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _loadReleases() {
|
||||||
|
setIsLoading(true);
|
||||||
|
setLastReleasesData(null);
|
||||||
|
setOngoingReleasesData(null);
|
||||||
|
setFinishedReleasesData(null);
|
||||||
|
setAnnounceReleasesData(null);
|
||||||
|
setFilmsReleasesData(null);
|
||||||
|
|
||||||
|
const lastReleases = await _FetchHomePageReleases("last", token);
|
||||||
|
const ongoingReleases = await _FetchHomePageReleases("ongoing", token);
|
||||||
|
const finishedReleases = await _FetchHomePageReleases("finished", token);
|
||||||
|
const announceReleases = await _FetchHomePageReleases("announce", token);
|
||||||
|
const filmsReleases = await _FetchHomePageReleases("films", token);
|
||||||
|
|
||||||
|
setLastReleasesData(lastReleases);
|
||||||
|
setOngoingReleasesData(ongoingReleases);
|
||||||
|
setFinishedReleasesData(finishedReleases);
|
||||||
|
setAnnounceReleasesData(announceReleases);
|
||||||
|
setFilmsReleasesData(filmsReleases);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
_loadReleases();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container flex flex-col pt-2 pb-20 mx-auto sm:pt-4 sm:pb-0">
|
||||||
|
{lastReleasesData ? (
|
||||||
|
<ReleaseCourusel
|
||||||
|
sectionTitle="Последние релизы"
|
||||||
|
showAllLink="/home/last"
|
||||||
|
content={lastReleasesData.content}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center min-w-full min-h-screen">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{finishedReleasesData && (
|
||||||
|
<ReleaseCourusel
|
||||||
|
sectionTitle="Завершенные релизы"
|
||||||
|
showAllLink="/home/finished"
|
||||||
|
content={finishedReleasesData.content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{ongoingReleasesData && (
|
||||||
|
<ReleaseCourusel
|
||||||
|
sectionTitle="В эфире"
|
||||||
|
showAllLink="/home/ongoing"
|
||||||
|
content={ongoingReleasesData.content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{announceReleasesData && (
|
||||||
|
<ReleaseCourusel
|
||||||
|
sectionTitle="Анонсированные релизы"
|
||||||
|
showAllLink="/home/announce"
|
||||||
|
content={announceReleasesData.content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filmsReleasesData && (
|
||||||
|
<ReleaseCourusel
|
||||||
|
sectionTitle="Фильмы"
|
||||||
|
showAllLink="/home/films"
|
||||||
|
content={filmsReleasesData.content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
!lastReleasesData &&
|
||||||
|
!finishedReleasesData &&
|
||||||
|
!ongoingReleasesData &&
|
||||||
|
!announceReleasesData && (
|
||||||
|
<div className="flex items-center justify-center min-w-full min-h-screen">
|
||||||
|
<h1 className="text-2xl">Ошибка загрузки контента...</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,91 +0,0 @@
|
||||||
"use client";
|
|
||||||
import useSWRInfinite from "swr/infinite";
|
|
||||||
import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
|
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useScrollPosition } from "@/app/hooks/useScrollPosition";
|
|
||||||
import { useUserStore } from "../store/auth";
|
|
||||||
|
|
||||||
const fetcher = async (url) => {
|
|
||||||
const res = await fetch(url);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const error = new Error("An error occurred while fetching the data.");
|
|
||||||
error.info = await res.json();
|
|
||||||
error.status = res.status;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export function IndexCategoryPage(props) {
|
|
||||||
const userStore = useUserStore((state) => state);
|
|
||||||
const [isLoadingEnd, setIsLoadingEnd] = useState(false);
|
|
||||||
const token = userStore.token;
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
|
||||||
if (token) {
|
|
||||||
return `/api/home?status=${props.slug}&page=${pageIndex}&token=${token}`;
|
|
||||||
}
|
|
||||||
return `/api/home?status=${props.slug}&page=${pageIndex}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
||||||
getKey,
|
|
||||||
fetcher,
|
|
||||||
{ initialSize: 2, revalidateFirstPage: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const [content, setContent] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
let allReleases = [];
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
allReleases.push(...data[i].content);
|
|
||||||
}
|
|
||||||
setContent(allReleases);
|
|
||||||
setIsLoadingEnd(true);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const scrollPosition = useScrollPosition();
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollPosition >= 98 && scrollPosition <= 99) {
|
|
||||||
setSize(size + 1);
|
|
||||||
}
|
|
||||||
}, [scrollPosition]);
|
|
||||||
|
|
||||||
if (error) return <div>failed to load</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="container pt-2 pb-16 mx-auto sm:pt-4 sm:pb-0">
|
|
||||||
{content && content.length > 0 ? (
|
|
||||||
<ReleaseSection
|
|
||||||
sectionTitle={props.SectionTitleMapping[props.slug]}
|
|
||||||
content={content}
|
|
||||||
/>
|
|
||||||
) : !isLoadingEnd ? (
|
|
||||||
<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>
|
|
||||||
<p>
|
|
||||||
В списке {props.SectionTitleMapping[props.slug]} пока ничего нет...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data && data[data.length - 1].content.length == 25 && (
|
|
||||||
<button
|
|
||||||
className="mx-auto w-[calc(100%-10rem)] border border-black rounded-lg p-2 mb-6 flex items-center justify-center gap-2 hover:bg-black hover:text-white transition"
|
|
||||||
onClick={() => setSize(size + 1)}
|
|
||||||
>
|
|
||||||
<span className="w-10 h-10 iconify mdi--plus"> </span>
|
|
||||||
<span className="text-lg">Загрузить ещё</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
77
app/pages/IndexCategory.tsx
Normal file
77
app/pages/IndexCategory.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
"use client";
|
||||||
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
|
import { useUserStore } from "../store/auth";
|
||||||
|
import { _FetchHomePageReleases } from "#/api/utils";
|
||||||
|
|
||||||
|
export function IndexCategoryPage(props) {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _loadInitialReleases() {
|
||||||
|
setIsLoading(true);
|
||||||
|
setContent(null);
|
||||||
|
|
||||||
|
const data: any = await _FetchHomePageReleases(props.slug, token, page);
|
||||||
|
|
||||||
|
setContent(data.content);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadInitialReleases();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function _loadNextReleasesPage() {
|
||||||
|
const data: any = await _FetchHomePageReleases(props.slug, token, page);
|
||||||
|
const newContent = [...content, ...data.content];
|
||||||
|
setContent(newContent);
|
||||||
|
}
|
||||||
|
if (content) {
|
||||||
|
_loadNextReleasesPage();
|
||||||
|
}
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition();
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollPosition == 98) {
|
||||||
|
setPage(page + 1);
|
||||||
|
}
|
||||||
|
}, [scrollPosition]);
|
||||||
|
|
||||||
|
// if (error) return <div>failed to load</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container pt-2 pb-16 mx-auto sm:pt-4 sm:pb-0">
|
||||||
|
{content && content.length > 0 ? (
|
||||||
|
<ReleaseSection
|
||||||
|
sectionTitle={props.SectionTitleMapping[props.slug]}
|
||||||
|
content={content}
|
||||||
|
/>
|
||||||
|
) : 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>
|
||||||
|
<p>
|
||||||
|
В списке {props.SectionTitleMapping[props.slug]} пока ничего нет...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="mx-auto w-[calc(100%-10rem)] border border-black rounded-lg p-2 mb-6 flex items-center justify-center gap-2 hover:bg-black hover:text-white transition"
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
>
|
||||||
|
<span className="w-10 h-10 iconify mdi--plus"> </span>
|
||||||
|
<span className="text-lg">Загрузить ещё</span>
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useUserStore } from "@/app/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { setJWT } from "@/app/api/utils";
|
import { setJWT } from "#/api/utils";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const [login, setLogin] = useState("");
|
const [login, setLogin] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [remember, setRemember] = useState(false);
|
const [remember, setRemember]: any = useState(false);
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -68,17 +68,17 @@ export function LoginPage() {
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
Эл. почта
|
Логин или эл. почта
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="text"
|
||||||
name="email"
|
name="email"
|
||||||
id="email"
|
id="email"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
value={login}
|
value={login}
|
||||||
onChange={(e) => setLogin(e.target.value)}
|
onChange={(e) => setLogin(e.target.value)}
|
||||||
required=""
|
required={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -94,7 +94,7 @@ export function LoginPage() {
|
||||||
id="password"
|
id="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
required=""
|
required={true}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
@ -107,7 +107,7 @@ export function LoginPage() {
|
||||||
aria-describedby="remember"
|
aria-describedby="remember"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"
|
className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"
|
||||||
required=""
|
required={true}
|
||||||
value={remember}
|
value={remember}
|
||||||
onChange={(e) => setRemember(e.target.checked)}
|
onChange={(e) => setRemember(e.target.checked)}
|
||||||
/>
|
/>
|
|
@ -1,14 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useUserStore } from "@/app/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchDataViaGet } from "../api/utils";
|
import { fetchDataViaGet } from "../api/utils";
|
||||||
import { Spinner } from "../components/Spinner/Spinner";
|
import { Spinner } from "../components/Spinner/Spinner";
|
||||||
import { Avatar, Card, Button, Table } from "flowbite-react";
|
import { Avatar, Card, Button, Table } from "flowbite-react";
|
||||||
import { Chip } from "../components/Chip/Chip";
|
import { Chip } from "../components/Chip/Chip";
|
||||||
import { unixToDate, minutesToTime } from "../api/utils";
|
import { unixToDate, minutesToTime } from "../api/utils";
|
||||||
import { ReleaseLink } from "../components/ReleaseLink/ReleaseLink";
|
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||||
|
|
||||||
export const ProfilePage = (props) => {
|
export const ProfilePage = (props: any) => {
|
||||||
const authUser = useUserStore((state) => state);
|
const authUser = useUserStore((state) => state);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [isMyProfile, setIsMyProfile] = useState(false);
|
const [isMyProfile, setIsMyProfile] = useState(false);
|
||||||
|
@ -74,7 +74,7 @@ export const ProfilePage = (props) => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container flex flex-col gap-4 px-4 pt-4 pb-32 mx-auto overflow-hidden xl:flex-row sm:pb-4">
|
<main className="container flex flex-col gap-4 px-4 pt-4 pb-32 mx-auto overflow-hidden sm:pb-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Card className="max-w-full">
|
<Card className="max-w-full">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
@ -111,12 +111,9 @@ export const ProfilePage = (props) => {
|
||||||
>
|
>
|
||||||
{socials.map((social) => {
|
{socials.map((social) => {
|
||||||
if (!social.nickname) return null;
|
if (!social.nickname) return null;
|
||||||
if (social.name == "discord") return (
|
if (social.name == "discord")
|
||||||
<Button
|
return (
|
||||||
color="light"
|
<Button color="light" key={social.name} as="a">
|
||||||
key={social.name}
|
|
||||||
as="a"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`iconify-color h-4 w-4 sm:h-6 sm:w-6 ${social.icon}`}
|
className={`iconify-color h-4 w-4 sm:h-6 sm:w-6 ${social.icon}`}
|
||||||
|
@ -124,7 +121,7 @@ export const ProfilePage = (props) => {
|
||||||
{social.nickname}
|
{social.nickname}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
color="light"
|
color="light"
|
||||||
|
@ -278,15 +275,11 @@ export const ProfilePage = (props) => {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="px-4 py-2 bg-white border border-gray-200 rounded-lg shadow-md dark:border-gray-700 dark:bg-gray-800">
|
||||||
<Card className="w-full max-w-full min-w-full">
|
<ReleaseCourusel
|
||||||
<h1>Недавно просмотренные</h1>
|
sectionTitle="Недавно просмотренные"
|
||||||
<div className="grid justify-center sm:grid-cols-[repeat(auto-fit,minmax(300px,1fr))] grid-cols-[100%] gap-2 min-w-full">
|
content={user.history}
|
||||||
{user.history.map((release) => {
|
/>
|
||||||
return <ReleaseLink key={release.id} {...release} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
359
app/pages/Release.tsx
Normal file
359
app/pages/Release.tsx
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
const fetcher = (...args: any) =>
|
||||||
|
fetch([...args] as any).then((res) => res.json());
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { Card, Dropdown, Button } from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { unixToDate, getSeasonFromUnix, minutesToTime } from "#/api/utils";
|
||||||
|
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
|
||||||
|
import { ReleasePlayer } from "#/components/ReleasePlayer/ReleasePlayer";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { Table } from "flowbite-react";
|
||||||
|
import { ReleaseInfoSearchLink } from "#/components/ReleaseInfo/ReleaseInfo.SearchLink";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const lists = [
|
||||||
|
{ list: 0, name: "Не смотрю" },
|
||||||
|
{ list: 1, name: "Смотрю" },
|
||||||
|
{ list: 2, name: "В планах" },
|
||||||
|
{ list: 3, name: "Просмотрено" },
|
||||||
|
{ list: 4, name: "Отложено" },
|
||||||
|
{ list: 5, name: "Брошено" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const weekDay = [
|
||||||
|
"_",
|
||||||
|
"каждый понедельник",
|
||||||
|
"каждый вторник",
|
||||||
|
"каждую среду",
|
||||||
|
"каждый четверг",
|
||||||
|
"каждую пятницу",
|
||||||
|
"каждую субботу",
|
||||||
|
"каждое воскресенье",
|
||||||
|
];
|
||||||
|
|
||||||
|
const YearSeason = ["_", "Зима", "Весна", "Лето", "Осень"];
|
||||||
|
|
||||||
|
const DropdownTheme = {
|
||||||
|
floating: {
|
||||||
|
target:
|
||||||
|
"flex-1 bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReleasePage = (props: any) => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const [userList, setUserList] = useState(0);
|
||||||
|
const [userFavorite, setUserFavorite] = useState(false);
|
||||||
|
|
||||||
|
function useFetch(id: number) {
|
||||||
|
let url: string;
|
||||||
|
|
||||||
|
url = `/api/release/${id}`;
|
||||||
|
if (token) {
|
||||||
|
url += `?token=${token}`;
|
||||||
|
}
|
||||||
|
const { data, isLoading, error } = useSWR(url, fetcher);
|
||||||
|
return [data, isLoading, error];
|
||||||
|
}
|
||||||
|
const [data, isLoading, error] = useFetch(props.id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
const el = document.getElementById("note");
|
||||||
|
if (el) {
|
||||||
|
el.innerHTML = data.release.note;
|
||||||
|
}
|
||||||
|
setUserList(data.release.profile_list_status || 0);
|
||||||
|
setUserFavorite(data.release.is_favorite);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
function _addToFavorite() {
|
||||||
|
if (data && token) {
|
||||||
|
setUserFavorite(!userFavorite);
|
||||||
|
if (userFavorite) {
|
||||||
|
fetch(
|
||||||
|
`${ENDPOINTS.user.favorite}/delete/${data.release.id}?token=${token}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fetch(
|
||||||
|
`${ENDPOINTS.user.favorite}/add/${data.release.id}?token=${token}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _addToList(list: number) {
|
||||||
|
if (data && token) {
|
||||||
|
setUserList(list);
|
||||||
|
fetch(
|
||||||
|
`${ENDPOINTS.user.bookmark}/add/${list}/${data.release.id}?token=${token}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? (
|
||||||
|
<main className="container px-4 pt-4 pb-24 mx-auto sm:pb-4">
|
||||||
|
<div className="grid grid-cols-[100%] lg:grid-cols-[70%_30%] gap-2 justify-center">
|
||||||
|
<div className="[grid-column:1] flex flex-col gap-2">
|
||||||
|
<Card className="lg:[grid-column:1]">
|
||||||
|
<div className="flex flex-col w-full h-full gap-4 lg:flex-row">
|
||||||
|
<img
|
||||||
|
className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700"
|
||||||
|
src={data.release.image}
|
||||||
|
alt=""
|
||||||
|
></img>
|
||||||
|
<div className="flex flex-col max-w-2xl gap-2 text-sm md:text-base">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{data.release.title_ru && (
|
||||||
|
<p className="text-xl font-bold text-black md:text-2xl">
|
||||||
|
{data.release.title_ru}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{data.release.title_original && (
|
||||||
|
<p className="text-sm text-gray-500 md:text-base">
|
||||||
|
{data.release.title_original}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data.release.note && (
|
||||||
|
<div className="py-2 bg-blue-100 border-l-4 border-blue-700 rounded-md ">
|
||||||
|
<div id="note" className="ml-2"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data.release.description && <p>{data.release.description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
{data.release.status.name.toLowerCase() != "анонс" && (
|
||||||
|
<ReleasePlayer id={props.id} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="[grid-column:1] lg:[grid-column:2] flex flex-col gap-2">
|
||||||
|
<Card className="order-2 lg:order-1">
|
||||||
|
<Table>
|
||||||
|
<Table.Body>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
{data.release.country ? (
|
||||||
|
data.release.country.toLowerCase() == "япония" ? (
|
||||||
|
<span className="w-8 h-8 iconify-color twemoji--flag-for-japan"></span>
|
||||||
|
) : (
|
||||||
|
<span className="w-8 h-8 iconify-color twemoji--flag-for-china"></span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="w-8 h-8 iconify-color twemoji--flag-for-united-nations "></span>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
|
{data.release.country && data.release.country}
|
||||||
|
{(data.release.aired_on_date != 0 || data.release.year) &&
|
||||||
|
", "}
|
||||||
|
{data.release.aired_on_date != 0 &&
|
||||||
|
`${getSeasonFromUnix(data.release.aired_on_date)} `}
|
||||||
|
{data.release.year && `${data.release.year} г.`}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
<span className="w-8 h-8 iconify-color mdi--animation-play "></span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
|
{data.release.episodes_released
|
||||||
|
? data.release.episodes_released
|
||||||
|
: "?"}
|
||||||
|
{"/"}
|
||||||
|
{data.release.episodes_total
|
||||||
|
? data.release.episodes_total + " эп. "
|
||||||
|
: "? эп. "}
|
||||||
|
{data.release.duration != 0 &&
|
||||||
|
`по ${minutesToTime(data.release.duration)}`}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
<span className="w-8 h-8 iconify-color mdi--calendar "></span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{data.release.category.name}
|
||||||
|
{", "}
|
||||||
|
{data.release.broadcast == 0
|
||||||
|
? data.release.status.name.toLowerCase()
|
||||||
|
: `выходит ${weekDay[data.release.broadcast]}`}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
<span className="w-8 h-8 iconify-color mdi--people "></span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{data.release.studio && (
|
||||||
|
<>
|
||||||
|
{"Студия: "}
|
||||||
|
{data.release.studio
|
||||||
|
.split(", ")
|
||||||
|
.map((studio: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && ", "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={studio}
|
||||||
|
searchBy={1}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{(data.release.author || data.release.director) && ", "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data.release.author && (
|
||||||
|
<>
|
||||||
|
{"Автор: "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={data.release.author}
|
||||||
|
searchBy={3}
|
||||||
|
/>
|
||||||
|
{data.release.director && ", "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{data.release.director && (
|
||||||
|
<>
|
||||||
|
{"Режиссёр: "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={data.release.director}
|
||||||
|
searchBy={2}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
<span className="w-8 h-8 iconify-color mdi--tag "></span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{data.release.genres &&
|
||||||
|
data.release.genres
|
||||||
|
.split(", ")
|
||||||
|
.map((genre: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{index > 0 && ", "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={genre}
|
||||||
|
searchBy={4}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{data.release.status.name.toLowerCase() == "анонс" && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell className="py-0">
|
||||||
|
<span className="w-8 h-8 iconify-color mdi--clock "></span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
|
{data.release.aired_on_date != 0 ? (
|
||||||
|
unixToDate(data.release.aired_on_date)
|
||||||
|
) : data.release.year ? (
|
||||||
|
<>
|
||||||
|
{data.release.season && data.release.season != 0
|
||||||
|
? `${YearSeason[data.release.season]} `
|
||||||
|
: ""}
|
||||||
|
{data.release.year && `${data.release.year} г.`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Скоро"
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
{token && (
|
||||||
|
<Card className="order-1 lg:order-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Dropdown
|
||||||
|
label={lists[userList].name}
|
||||||
|
dismissOnClick={true}
|
||||||
|
theme={DropdownTheme}
|
||||||
|
>
|
||||||
|
{lists.map((list) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={list.list}
|
||||||
|
onClick={() => _addToList(list.list)}
|
||||||
|
>
|
||||||
|
{list.name}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||||
|
onClick={() => {
|
||||||
|
_addToFavorite();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`iconify w-6 h-6 ${
|
||||||
|
userFavorite ? "mdi--heart" : "mdi--heart-outline"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{data.release.related_releases.length > 0 && (
|
||||||
|
<Card className="order-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between py-2 border-b-2 border-black">
|
||||||
|
<h1>Связанные релизы</h1>
|
||||||
|
{data.release.related && (
|
||||||
|
<Link href={`/related/${data.release.related.id}`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<p className="hidden sm:block">Показать все</p>
|
||||||
|
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 mt-2">
|
||||||
|
{data.release.related_releases.map((release) => {
|
||||||
|
if (release.id == data.release.id) return null;
|
||||||
|
return <ReleaseLink key={release.id} {...release} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
) : (
|
||||||
|
<main className="flex h-[100dvh] w-full justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
/* <Chip
|
||||||
|
bg_color={
|
||||||
|
data.release.grade.toFixed(1) == 0
|
||||||
|
? "hidden"
|
||||||
|
: data.release.grade.toFixed(1) < 2
|
||||||
|
? "bg-red-500"
|
||||||
|
: data.release.grade.toFixed(1) < 3
|
||||||
|
? "bg-orange-500"
|
||||||
|
: data.release.grade.toFixed(1) < 4
|
||||||
|
? "bg-yellow-500"
|
||||||
|
: "bg-green-500"
|
||||||
|
}
|
||||||
|
name={data.release.grade.toFixed(1)}
|
||||||
|
/> */
|
||||||
|
}
|
|
@ -1,21 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
import useSWRInfinite from "swr/infinite";
|
import useSWRInfinite from "swr/infinite";
|
||||||
import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
import { RelatedSection } from "@/app/components/RelatedSection/RelatedSection";
|
import { RelatedSection } from "#/components/RelatedSection/RelatedSection";
|
||||||
import { Spinner } from "@/app/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "@/app/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
|
|
||||||
const fetcher = async (url) => {
|
const fetcher = async (url: string) => {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = new Error("An error occurred while fetching the data.");
|
const error = new Error(`An error occurred while fetching the data. status: ${res.status}`);
|
||||||
error.info = await res.json();
|
error.message = await res.json();
|
||||||
error.status = res.status;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,18 +25,34 @@ export function SearchPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [query, setQuery] = useState(searchParams.get("q") || null);
|
const [query, setQuery] = useState(searchParams.get("q") || null);
|
||||||
|
const where = searchParams.get("where") || null
|
||||||
|
const searchBy = searchParams.get("searchBy") || null
|
||||||
|
const list = searchParams.get("list") || null
|
||||||
|
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
|
|
||||||
const getKey = (pageIndex, previousPageData) => {
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
if (previousPageData && !previousPageData.releases.length) return null;
|
if (previousPageData && !previousPageData.releases.length) return null;
|
||||||
|
|
||||||
const url = new URL("/api/search", window.location.origin);
|
const url = new URL("/api/search", window.location.origin);
|
||||||
url.searchParams.set("page", pageIndex);
|
url.searchParams.set("page", pageIndex.toString());
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
url.searchParams.set("token", token);
|
url.searchParams.set("token", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (where) {
|
||||||
|
url.searchParams.set("where", where);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchBy) {
|
||||||
|
url.searchParams.set("searchBy", searchBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list) {
|
||||||
|
url.searchParams.set("list", list);
|
||||||
|
}
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
url.searchParams.set("q", query);
|
url.searchParams.set("q", query);
|
||||||
return url.toString();
|
return url.toString();
|
|
@ -5,26 +5,32 @@ export const sort = {
|
||||||
{
|
{
|
||||||
name: "По добавлению новых",
|
name: "По добавлению новых",
|
||||||
value: "adding_descending",
|
value: "adding_descending",
|
||||||
|
id: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "По добавлению старых",
|
name: "По добавлению старых",
|
||||||
value: "adding_ascending",
|
value: "adding_ascending",
|
||||||
|
id: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "По дате выхода новых",
|
name: "По дате выхода новых",
|
||||||
value: "year_descending",
|
value: "year_descending",
|
||||||
|
id: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "По дате выхода старых",
|
name: "По дате выхода старых",
|
||||||
value: "year_ascending",
|
value: "year_ascending",
|
||||||
|
id: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "По алфавиту А-Я",
|
name: "По алфавиту А-Я",
|
||||||
value: "alphabet_descending",
|
value: "alphabet_descending",
|
||||||
|
id: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "По алфавиту Я-А",
|
name: "По алфавиту Я-А",
|
||||||
value: "alphabet_ascending",
|
value: "alphabet_ascending",
|
||||||
|
id: 6
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
|
@ -1,17 +0,0 @@
|
||||||
import { ProfilePage } from "@/app/pages/Profile";
|
|
||||||
import { fetchDataViaGet } from "@/app/api/utils";
|
|
||||||
import { ENDPOINTS } from "@/app/api/config";
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }) {
|
|
||||||
const id = params.id
|
|
||||||
const profile = await fetchDataViaGet(`${ENDPOINTS.user.profile}/${id}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: "Профиль " + profile.profile.login,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Search({ params }) {
|
|
||||||
const id = params.id
|
|
||||||
return <ProfilePage id={id} />;
|
|
||||||
}
|
|
16
app/profile/[id]/page.tsx
Normal file
16
app/profile/[id]/page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ProfilePage } from "#/pages/Profile";
|
||||||
|
import { fetchDataViaGet } from "#/api/utils";
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }) {
|
||||||
|
const id:string = params.id;
|
||||||
|
const profile: any = await fetchDataViaGet(`https://api.anixart.tv/profile/${id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: "Профиль " + profile.profile.login,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Profile({ params }) {
|
||||||
|
const id: string = params.id;
|
||||||
|
return <ProfilePage id={id} />;
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
"use client"
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { getJWT } from "../api/utils";
|
|
||||||
|
|
||||||
export default function myProfile() {
|
|
||||||
const user = getJWT()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return router.push("/login")
|
|
||||||
} else {
|
|
||||||
return router.push(`/profile/${user.user_id}`)
|
|
||||||
}
|
|
||||||
}
|
|
16
app/release/[id]/page.tsx
Normal file
16
app/release/[id]/page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { ReleasePage } from "#/pages/Release";
|
||||||
|
import { fetchDataViaGet } from "#/api/utils";
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }) {
|
||||||
|
const id = params.id
|
||||||
|
const release = await fetchDataViaGet(`https://api.anixart.tv/release/${id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: release.release.title_ru,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Search({ params }) {
|
||||||
|
const id = params.id
|
||||||
|
return <ReleasePage id={id} />;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { SearchPage } from "@/app/pages/Search";
|
import { SearchPage } from "#/pages/Search";
|
||||||
|
|
||||||
export async function generateMetadata({ searchParams }) {
|
export async function generateMetadata({ searchParams }) {
|
||||||
const query = searchParams.q;
|
const query = searchParams.q;
|
|
@ -1,13 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { getJWT, setJWT, removeJWT, fetchDataViaGet } from "@/app/api/utils";
|
import { getJWT, removeJWT, fetchDataViaGet } from "#/api/utils";
|
||||||
|
|
||||||
export const useUserStore = create((set, get) => ({
|
interface userState {
|
||||||
|
isAuth: boolean
|
||||||
|
user: Object | null
|
||||||
|
token: string | null
|
||||||
|
login: (user: Object, token: string) => void
|
||||||
|
logout: () => void
|
||||||
|
checkAuth: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<userState>((set, get) => ({
|
||||||
isAuth: false,
|
isAuth: false,
|
||||||
user: null,
|
user: null,
|
||||||
token: null,
|
token: null,
|
||||||
|
|
||||||
login: (user, token) => {
|
login: (user: Object, token: string) => {
|
||||||
set({ isAuth: true, user: user, token: token });
|
set({ isAuth: true, user: user, token: token });
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
39
package-lock.json
generated
39
package-lock.json
generated
|
@ -23,6 +23,8 @@
|
||||||
"@iconify-json/mdi": "^1.1.67",
|
"@iconify-json/mdi": "^1.1.67",
|
||||||
"@iconify-json/twemoji": "^1.1.15",
|
"@iconify-json/twemoji": "^1.1.15",
|
||||||
"@iconify/tailwind": "^1.1.1",
|
"@iconify/tailwind": "^1.1.1",
|
||||||
|
"@types/node": "20.14.12",
|
||||||
|
"@types/react": "18.3.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
@ -604,6 +606,31 @@
|
||||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.14.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
|
||||||
|
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
|
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||||
|
"devOptional": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||||
|
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
@ -1276,6 +1303,12 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"devOptional": true
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
|
@ -4875,6 +4908,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
"@iconify-json/mdi": "^1.1.67",
|
"@iconify-json/mdi": "^1.1.67",
|
||||||
"@iconify-json/twemoji": "^1.1.15",
|
"@iconify-json/twemoji": "^1.1.15",
|
||||||
"@iconify/tailwind": "^1.1.1",
|
"@iconify/tailwind": "^1.1.1",
|
||||||
|
"@types/node": "20.14.12",
|
||||||
|
"@types/react": "18.3.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
|
42
tsconfig.json
Normal file
42
tsconfig.json
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"incremental": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": "app",
|
||||||
|
"paths": {
|
||||||
|
"#/components/*": ["components/*"],
|
||||||
|
"#/api/*": ["api/*"],
|
||||||
|
"#/store/*": ["store/*"],
|
||||||
|
"#/hooks/*": ["hooks/*"],
|
||||||
|
"#/pages/*": ["pages/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue