mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-05 05:55:36 +05:00
Compare commits
32 commits
be0731cfba
...
a4ecc27874
Author | SHA1 | Date | |
---|---|---|---|
a4ecc27874 | |||
7e99062c0c | |||
284e262527 | |||
7f007480ed | |||
88664a86d1 | |||
09ddb71e15 | |||
0f1c61b765 | |||
2a2343fed3 | |||
819d336540 | |||
777fb5b82b | |||
d3b198c6bc | |||
7d15eef691 | |||
3d08603bc3 | |||
bf24cd1063 | |||
e067336605 | |||
28b7ea2d6c | |||
a615af836b | |||
fd0ce8cb94 | |||
56334893b4 | |||
05cb74b7f2 | |||
b25bb4d6e9 | |||
01e2903e7b | |||
5d2a4cbe67 | |||
61baffd295 | |||
bfb361a0a8 | |||
93205fdb4e | |||
8d2800c2f2 | |||
6b84a312f7 | |||
48345244f3 | |||
c636c843ed | |||
b93aeeed04 | |||
052e649012 |
65 changed files with 3196 additions and 673 deletions
|
@ -2,8 +2,9 @@
|
||||||
"$schema": "https://unpkg.com/flowbite-react/schema.json",
|
"$schema": "https://unpkg.com/flowbite-react/schema.json",
|
||||||
"components": [],
|
"components": [],
|
||||||
"dark": true,
|
"dark": true,
|
||||||
"prefix": "",
|
|
||||||
"path": "src/components",
|
"path": "src/components",
|
||||||
|
"prefix": "",
|
||||||
|
"rsc": true,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"rsc": true
|
"version": 3
|
||||||
}
|
}
|
22
.flowbite-react/init.tsx
Normal file
22
.flowbite-react/init.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore-all lint: auto-generated file
|
||||||
|
|
||||||
|
// This file is auto-generated by the flowbite-react CLI.
|
||||||
|
// Do not edit this file directly.
|
||||||
|
// Instead, edit the .flowbite-react/config.json file.
|
||||||
|
|
||||||
|
import { StoreInit } from "flowbite-react/store/init";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const CONFIG = {
|
||||||
|
dark: true,
|
||||||
|
prefix: "",
|
||||||
|
version: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ThemeInit() {
|
||||||
|
return <StoreInit {...CONFIG} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeInit.displayName = "ThemeInit";
|
|
@ -226,7 +226,9 @@ app.get("/*path", async (req, res) => {
|
||||||
if (
|
if (
|
||||||
!apiResponse ||
|
!apiResponse ||
|
||||||
!apiResponse.ok ||
|
!apiResponse.ok ||
|
||||||
apiResponse.headers.get("content-type") != "application/json"
|
(apiResponse.headers.get("content-type") != "application/json" &&
|
||||||
|
apiResponse.headers.get("content-type") !=
|
||||||
|
"application/json;charset=UTF-8")
|
||||||
) {
|
) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to fetch: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
`Failed to fetch: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
||||||
|
@ -277,7 +279,7 @@ app.post("/*path", async (req, res) => {
|
||||||
"application/json",
|
"application/json",
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
"multipart/form-data",
|
"multipart/form-data",
|
||||||
"x-unknown/unknown"
|
"x-unknown/unknown",
|
||||||
];
|
];
|
||||||
|
|
||||||
const isSupported = supportedContentTypes.includes(
|
const isSupported = supportedContentTypes.includes(
|
||||||
|
@ -335,7 +337,9 @@ app.post("/*path", async (req, res) => {
|
||||||
if (
|
if (
|
||||||
!apiResponse ||
|
!apiResponse ||
|
||||||
!apiResponse.ok ||
|
!apiResponse.ok ||
|
||||||
apiResponse.headers.get("content-type") != "application/json"
|
(apiResponse.headers.get("content-type") != "application/json" &&
|
||||||
|
apiResponse.headers.get("content-type") !=
|
||||||
|
"application/json;charset=UTF-8")
|
||||||
) {
|
) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to post: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
`Failed to post: '${url.protocol}//${url.hostname}${url.pathname}', Path probably doesn't exist`
|
||||||
|
|
14
app/App.tsx
14
app/App.tsx
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { useUserStore } from "./store/auth";
|
import { useUserStore } from "./store/auth";
|
||||||
import { usePreferencesStore } from "./store/preferences";
|
import { usePreferencesStore } from "./store/preferences";
|
||||||
import { Navbar } from "./components/Navbar/NavbarUpdate";
|
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
@ -14,6 +13,9 @@ import {
|
||||||
import { Spinner } from "./components/Spinner/Spinner";
|
import { Spinner } from "./components/Spinner/Spinner";
|
||||||
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
||||||
import { Bounce, ToastContainer } from "react-toastify";
|
import { Bounce, ToastContainer } from "react-toastify";
|
||||||
|
import { NavBarPc } from "./components/Navbar/NavBarPc";
|
||||||
|
import { NavBarMobile } from "./components/Navbar/NavBarMobile";
|
||||||
|
import { SettingsModal } from "./components/SettingsModal/SettingsModal";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ export const App = (props) => {
|
||||||
const [showChangelog, setShowChangelog] = useState(false);
|
const [showChangelog, setShowChangelog] = useState(false);
|
||||||
const [currentVersion, setCurrentVersion] = useState("");
|
const [currentVersion, setCurrentVersion] = useState("");
|
||||||
const [previousVersions, setPreviousVersions] = useState([]);
|
const [previousVersions, setPreviousVersions] = useState([]);
|
||||||
|
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function _checkVersion() {
|
async function _checkVersion() {
|
||||||
|
@ -68,8 +71,8 @@ export const App = (props) => {
|
||||||
<body
|
<body
|
||||||
className={`${inter.className} overflow-x-hidden dark:bg-[#0d1117] dark:text-white`}
|
className={`${inter.className} overflow-x-hidden dark:bg-[#0d1117] dark:text-white`}
|
||||||
>
|
>
|
||||||
<Navbar />
|
<NavBarPc setIsSettingModalOpen={setIsSettingModalOpen} />
|
||||||
<main className="container px-2 pt-4 pb-24 mx-auto sm:pb-0">
|
<main className="container px-2 pt-4 pb-24 mx-auto lg:pb-0">
|
||||||
{props.children}
|
{props.children}
|
||||||
</main>
|
</main>
|
||||||
<ChangelogModal
|
<ChangelogModal
|
||||||
|
@ -123,6 +126,11 @@ export const App = (props) => {
|
||||||
theme="colored"
|
theme="colored"
|
||||||
transition={Bounce}
|
transition={Bounce}
|
||||||
/>
|
/>
|
||||||
|
<NavBarMobile setIsSettingModalOpen={setIsSettingModalOpen} />
|
||||||
|
<SettingsModal
|
||||||
|
isOpen={isSettingModalOpen}
|
||||||
|
setIsOpen={setIsSettingModalOpen}
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const CURRENT_APP_VERSION = "3.8.0";
|
export const CURRENT_APP_VERSION = "3.9.0";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
|
|
||||||
const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null;
|
const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null;
|
||||||
|
@ -51,6 +51,7 @@ export const ENDPOINTS = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filter: `${API_PREFIX}/filter`,
|
filter: `${API_PREFIX}/filter`,
|
||||||
|
filterTypes: `${API_PREFIX}/type/all`,
|
||||||
search: {
|
search: {
|
||||||
profileList: `${API_PREFIX}/search/profile/list`,
|
profileList: `${API_PREFIX}/search/profile/list`,
|
||||||
profileHistory: `${API_PREFIX}/search/history`,
|
profileHistory: `${API_PREFIX}/search/history`,
|
||||||
|
@ -74,5 +75,13 @@ export const ENDPOINTS = {
|
||||||
releaseInCollections: `${API_PREFIX}/collection/all/release`,
|
releaseInCollections: `${API_PREFIX}/collection/all/release`,
|
||||||
userCollections: `${API_PREFIX}/collection/all/profile`,
|
userCollections: `${API_PREFIX}/collection/all/profile`,
|
||||||
favoriteCollections: `${API_PREFIX}/collectionFavorite`,
|
favoriteCollections: `${API_PREFIX}/collectionFavorite`,
|
||||||
|
},
|
||||||
|
discover: {
|
||||||
|
interesting: `${API_PREFIX}/discover/interesting`,
|
||||||
|
discussing: `${API_PREFIX}/discover/discussing`,
|
||||||
|
watching: `${API_PREFIX}/discover/watching`,
|
||||||
|
recommendations: `${API_PREFIX}/discover/recommendations`,
|
||||||
|
collections: `${API_PREFIX}/collection/all`,
|
||||||
|
schedule: `${API_PREFIX}/schedule`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
576
app/api/utils.ts
576
app/api/utils.ts
|
@ -248,99 +248,519 @@ export function sinceUnixDate(unixInSeconds: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function minutesToTime(
|
export function minutesToTime(min: number) {
|
||||||
min: number,
|
const seconds = min * 60;
|
||||||
type?: "full" | "daysOnly" | "daysHours"
|
const epoch = new Date(0);
|
||||||
) {
|
const date = new Date(seconds * 1000);
|
||||||
const d = Math.floor(min / 1440); // 60*24
|
|
||||||
const h = Math.floor((min - d * 1440) / 60);
|
|
||||||
const m = Math.round(min % 60);
|
|
||||||
|
|
||||||
var dDisplay =
|
const diffInMinutes =
|
||||||
d > 0 ? `${d} ${numberDeclension(d, "день", "дня", "дней")}` : "";
|
new Date(date.getTime() - epoch.getTime()).getTime() / 1000 / 60;
|
||||||
var hDisplay =
|
|
||||||
h > 0 ? `${h} ${numberDeclension(h, "час", "часа", "часов")}` : "";
|
|
||||||
var mDisplay =
|
|
||||||
m > 0 ? `${m} ${numberDeclension(m, "минута", "минуты", "минут")}` : "";
|
|
||||||
|
|
||||||
if (type == "daysOnly") {
|
let days = Math.floor(diffInMinutes / 1440);
|
||||||
if (d > 0) return dDisplay;
|
if (days < 0) days = 0;
|
||||||
return "? дней";
|
const daysToMinutes = days * 1440;
|
||||||
} else if (type == "daysHours") {
|
|
||||||
if (d > 0 && h > 0) return dDisplay + ", " + hDisplay;
|
let hours = Math.floor((diffInMinutes - daysToMinutes) / 60);
|
||||||
if (h > 0) return hDisplay;
|
if (hours < 0) hours = 0;
|
||||||
if (m > 0) return mDisplay;
|
const hoursToMinutes = hours * 60;
|
||||||
} else {
|
|
||||||
return `${d > 0 ? dDisplay : ""}${h > 0 ? ", " + hDisplay : ""}${m > 0 ? ", " + mDisplay : ""}`;
|
let minutes = diffInMinutes - daysToMinutes - hoursToMinutes;
|
||||||
}
|
if (minutes < 0) minutes = 0;
|
||||||
|
|
||||||
|
const dayDisplay =
|
||||||
|
days > 0 ? `${days} ${numberDeclension(days, "день", "дня", "дней")}` : "";
|
||||||
|
const hourDisplay =
|
||||||
|
hours > 0 ?
|
||||||
|
`${hours} ${numberDeclension(hours, "час", "часа", "часов")}`
|
||||||
|
: "";
|
||||||
|
const minuteDisplay =
|
||||||
|
minutes > 0 ?
|
||||||
|
`${minutes} ${numberDeclension(minutes, "минута", "минуты", "минут")}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (days > 0 && hours > 0 && minutes > 0)
|
||||||
|
return `${dayDisplay}, ${hourDisplay}, ${minuteDisplay}`;
|
||||||
|
if (days > 0 && hours > 0) return `${dayDisplay}, ${hourDisplay}`;
|
||||||
|
if (days > 0 && minutes > 0) return `${dayDisplay}, ${minuteDisplay}`;
|
||||||
|
if (hours > 0 && minutes > 0) return `${hourDisplay}, ${minuteDisplay}`;
|
||||||
|
if (days > 0) return dayDisplay;
|
||||||
|
if (hours > 0) return hourDisplay;
|
||||||
|
if (minutes > 0) return minuteDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusList: Record<string, null | number> = {
|
export const FilterCountry = ["Япония", "Китай", "Южная Корея"];
|
||||||
last: null,
|
export const FilterCategoryIdToString: Record<number, string> = {
|
||||||
finished: 1,
|
1: "Сериал",
|
||||||
ongoing: 2,
|
2: "Полнометражный фильм",
|
||||||
announce: 3,
|
3: "OVA",
|
||||||
|
4: "Дорама",
|
||||||
};
|
};
|
||||||
|
export const FilterGenre = {
|
||||||
export async function _FetchHomePageReleases(
|
uncategorized: {
|
||||||
status: string,
|
name: "Нет категории",
|
||||||
token: string | null,
|
genres: [
|
||||||
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,
|
audience: {
|
||||||
|
name: "Аудитория",
|
||||||
|
genres: [
|
||||||
|
"детское",
|
||||||
|
"дзёсей",
|
||||||
|
"сэйнэн",
|
||||||
|
"сёдзё",
|
||||||
|
"сёдзё-ай",
|
||||||
|
"сёнен",
|
||||||
|
"сёнен-ай",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
name: "Тематика",
|
||||||
|
genres: [
|
||||||
|
"CGDCT",
|
||||||
|
"антропоморфизм",
|
||||||
|
"боевые искусства",
|
||||||
|
"вампиры",
|
||||||
|
"взрослые персонажи",
|
||||||
|
"видеоигры",
|
||||||
|
"военное",
|
||||||
|
"выживание",
|
||||||
|
"гарем",
|
||||||
|
"гонки",
|
||||||
|
"городское фэнтези",
|
||||||
|
"гэг-юмор",
|
||||||
|
"детектив",
|
||||||
|
"жестокость",
|
||||||
|
"забота о детях",
|
||||||
|
"злодейка",
|
||||||
|
"игра с высокими ставками",
|
||||||
|
"идолы (жен.)",
|
||||||
|
"идолы (муж.)",
|
||||||
|
"изобразительное искусство",
|
||||||
|
"исполнительское искусство",
|
||||||
|
"исторический",
|
||||||
|
"исэкай",
|
||||||
|
"иясикэй",
|
||||||
|
"командный спорт",
|
||||||
|
"космос",
|
||||||
|
"кроссдрессинг",
|
||||||
|
"культура отаку",
|
||||||
|
"любовный многоугольник",
|
||||||
|
"магическая смена пола",
|
||||||
|
"махо-сёдзё",
|
||||||
|
"медицина",
|
||||||
|
"меха",
|
||||||
|
"мифология",
|
||||||
|
"музыка",
|
||||||
|
"образовательное",
|
||||||
|
"организованная преступность",
|
||||||
|
"пародия",
|
||||||
|
"питомцы",
|
||||||
|
"психологическое",
|
||||||
|
"путешествие во времени",
|
||||||
|
"работа",
|
||||||
|
"реверс-гарем",
|
||||||
|
"реинкарнация",
|
||||||
|
"романтический подтекст",
|
||||||
|
"самураи",
|
||||||
|
"спортивные единоборства",
|
||||||
|
"стратегические игры",
|
||||||
|
"супер сила",
|
||||||
|
"удостоено наград",
|
||||||
|
"хулиганы",
|
||||||
|
"школа",
|
||||||
|
"шоу-бизнес",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const FilterProfileListIdToString: Record<number, string> = {
|
||||||
|
0: "Избранное",
|
||||||
|
1: "Смотрю",
|
||||||
|
2: "В планах",
|
||||||
|
3: "Просмотрено",
|
||||||
|
4: "Отложено",
|
||||||
|
5: "Брошено",
|
||||||
|
};
|
||||||
|
export const FilterStudio = [
|
||||||
|
"A-1 Pictures",
|
||||||
|
"A.C.G.T",
|
||||||
|
"ACTAS, Inc",
|
||||||
|
"ACiD FiLM",
|
||||||
|
"AIC A.S.T.A",
|
||||||
|
"AIC PLUS",
|
||||||
|
"AIC Spirits",
|
||||||
|
"AIC",
|
||||||
|
"Animac",
|
||||||
|
"ANIMATE",
|
||||||
|
"Aniplex",
|
||||||
|
"ARMS",
|
||||||
|
"Artland",
|
||||||
|
"ARTMIC Studios",
|
||||||
|
"Asahi Production",
|
||||||
|
"Asia-Do",
|
||||||
|
"ASHI",
|
||||||
|
"Asread",
|
||||||
|
"Asmik Ace",
|
||||||
|
"Aubeck",
|
||||||
|
"BM Entertainment",
|
||||||
|
"Bandai Visua",
|
||||||
|
"Barnum Studio",
|
||||||
|
"Bee Train",
|
||||||
|
"BeSTACK",
|
||||||
|
"Blender Foundation",
|
||||||
|
"Bones",
|
||||||
|
"Brains Base",
|
||||||
|
"Bridge",
|
||||||
|
"Cinema Citrus",
|
||||||
|
"Chaos Project",
|
||||||
|
"Cherry Lips",
|
||||||
|
"David Production",
|
||||||
|
"Daume",
|
||||||
|
"Doumu",
|
||||||
|
"Dax International",
|
||||||
|
"DLE INC",
|
||||||
|
"Digital Frontier",
|
||||||
|
"Digital Works",
|
||||||
|
"Diomedea",
|
||||||
|
"DIRECTIONS Inc",
|
||||||
|
"Dogakobo",
|
||||||
|
"Dofus",
|
||||||
|
"Encourage Films",
|
||||||
|
"Feel",
|
||||||
|
"Fifth Avenue",
|
||||||
|
"Five Ways",
|
||||||
|
"Fuji TV",
|
||||||
|
"Foursome",
|
||||||
|
"GRAM Studio",
|
||||||
|
"G&G Entertainment",
|
||||||
|
"Gainax",
|
||||||
|
"GANSIS",
|
||||||
|
"Gathering",
|
||||||
|
"Gonzino",
|
||||||
|
"Gonzo",
|
||||||
|
"GoHands",
|
||||||
|
"Green Bunny",
|
||||||
|
"Group TAC",
|
||||||
|
"Hal Film Maker",
|
||||||
|
"Hasbro Studios",
|
||||||
|
"h.m.p",
|
||||||
|
"Himajin",
|
||||||
|
"Hoods Entertainment",
|
||||||
|
"Idea Factory",
|
||||||
|
"J.C.Staff",
|
||||||
|
"KANSAI",
|
||||||
|
"Kaname Production",
|
||||||
|
"Kitty Films",
|
||||||
|
"Knack",
|
||||||
|
"Kokusai Eigasha",
|
||||||
|
"KSS (студия)",
|
||||||
|
"Kyoto Animation",
|
||||||
|
"Lemon Heart",
|
||||||
|
"LMD",
|
||||||
|
"Madhouse Studios",
|
||||||
|
"Magic Bus",
|
||||||
|
"Manglobe Inc.",
|
||||||
|
"Manpuku Jinja",
|
||||||
|
"MAPPA",
|
||||||
|
"Milky",
|
||||||
|
"Minamimachi Bugyosho",
|
||||||
|
"Media Blasters",
|
||||||
|
"Mook Animation",
|
||||||
|
"Moonrock",
|
||||||
|
"MOVIC",
|
||||||
|
"Mushi Productions",
|
||||||
|
"Natural High",
|
||||||
|
"Nippon Animation",
|
||||||
|
"Nomad",
|
||||||
|
"Lerche",
|
||||||
|
"OB Planning",
|
||||||
|
"Office AO",
|
||||||
|
"Ordet",
|
||||||
|
"Oriental Light and Magic",
|
||||||
|
"OLM Inc.",
|
||||||
|
"P.A. Works",
|
||||||
|
"Palm Studio",
|
||||||
|
"Pastel",
|
||||||
|
"Phoenix Entertainment",
|
||||||
|
"Picture Magic",
|
||||||
|
"Pink",
|
||||||
|
"Pink Pineapple",
|
||||||
|
"Planet",
|
||||||
|
"Plum",
|
||||||
|
"PPM",
|
||||||
|
"Primastea",
|
||||||
|
"Production I.G",
|
||||||
|
"Project No.9",
|
||||||
|
"Radix",
|
||||||
|
"Rikuentai",
|
||||||
|
"Robot",
|
||||||
|
"Satelight",
|
||||||
|
"Seven",
|
||||||
|
"Seven Arcs",
|
||||||
|
"Shaft",
|
||||||
|
"Silver Link",
|
||||||
|
"Shinei Animation",
|
||||||
|
"Shogakukan Music & Digital Entertainment",
|
||||||
|
"Soft on Demand",
|
||||||
|
"Starchild Records",
|
||||||
|
"Studio 9 Maiami",
|
||||||
|
"Studio Tulip",
|
||||||
|
"Studio 4°C",
|
||||||
|
"Studio e.go!",
|
||||||
|
"Studio A.P.P.P",
|
||||||
|
"Studio Barcelona",
|
||||||
|
"Studio Blanc",
|
||||||
|
"Studio Comet",
|
||||||
|
"Studio Deen",
|
||||||
|
"Studio Fantasia",
|
||||||
|
"Studio Flag",
|
||||||
|
"Studio Gallop",
|
||||||
|
"Studio Ghibli",
|
||||||
|
"Studio Guts",
|
||||||
|
"Studio Gokumi",
|
||||||
|
"Studio Rikka",
|
||||||
|
"Studio Hibari",
|
||||||
|
"Studio Junio",
|
||||||
|
"Studio Khara",
|
||||||
|
"Studio Live",
|
||||||
|
"Studio Matrix",
|
||||||
|
"Studio Pierrot",
|
||||||
|
"Studio Egg",
|
||||||
|
"Sunrise",
|
||||||
|
"Synergy SP",
|
||||||
|
"Synergy Japan",
|
||||||
|
"Tatsunoko Production",
|
||||||
|
"Tele-Cartoon Japan",
|
||||||
|
"Telecom Animation Film",
|
||||||
|
"Tezuka Productions",
|
||||||
|
"The Answer Studio",
|
||||||
|
"TMS",
|
||||||
|
"TNK",
|
||||||
|
"Toei Animation",
|
||||||
|
"Tokyo Kids",
|
||||||
|
"TYO Animations",
|
||||||
|
"Transarts",
|
||||||
|
"Triangle Staff",
|
||||||
|
"Trinet Entertainment",
|
||||||
|
"Ufotable",
|
||||||
|
"Vega Entertainment",
|
||||||
|
"Victor Entertainment",
|
||||||
|
"Viewworks",
|
||||||
|
"White Fox",
|
||||||
|
"Wonder Farm",
|
||||||
|
"XEBEC-M2",
|
||||||
|
"Xebec",
|
||||||
|
"Yumeta Company",
|
||||||
|
"Zexcs",
|
||||||
|
"Zuiyo Eizo",
|
||||||
|
"8bit",
|
||||||
|
];
|
||||||
|
export const FilterSource = [
|
||||||
|
"Оригинал",
|
||||||
|
"Манга",
|
||||||
|
"Веб-манга",
|
||||||
|
"Енкома",
|
||||||
|
"Ранобэ",
|
||||||
|
"Новелла",
|
||||||
|
"Веб-новелла",
|
||||||
|
"Визуальная новелла",
|
||||||
|
"Игра",
|
||||||
|
"Карточная игра",
|
||||||
|
"Книга",
|
||||||
|
"Книга с картинками",
|
||||||
|
"Музыка",
|
||||||
|
"Радио",
|
||||||
|
"Более одного",
|
||||||
|
"Другое",
|
||||||
|
];
|
||||||
|
export const FilterYear = Array.from({ length: 200 }, (_, i) => 1900 + i); // 1900-2100 years around now
|
||||||
|
export const FilterSeasonIdToString = {
|
||||||
|
1: "Зима",
|
||||||
|
2: "Весна",
|
||||||
|
3: "Лето",
|
||||||
|
4: "Осень",
|
||||||
|
};
|
||||||
|
export const FilterEpisodeCount = [
|
||||||
|
{
|
||||||
|
name: "Неважно",
|
||||||
episodes_from: null,
|
episodes_from: null,
|
||||||
episodes_to: null,
|
episodes_to: null,
|
||||||
genres: [],
|
},
|
||||||
profile_list_exclusions: [],
|
{
|
||||||
start_year: null,
|
name: "От 1 до 12",
|
||||||
status_id: statusId,
|
episodes_from: 1,
|
||||||
types: [],
|
episodes_to: 12,
|
||||||
is_genres_exclude_mode_enabled: false,
|
},
|
||||||
};
|
{
|
||||||
|
name: "От 13 до 25",
|
||||||
|
episodes_from: 13,
|
||||||
|
episodes_to: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "От 26 до 100",
|
||||||
|
episodes_from: 26,
|
||||||
|
episodes_to: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Больше 100",
|
||||||
|
episodes_from: 100,
|
||||||
|
episodes_to: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const FilterEpisodeDuration = [
|
||||||
|
{
|
||||||
|
name: "Неважно",
|
||||||
|
episode_duration_from: null,
|
||||||
|
episode_duration_to: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "До 10 минут",
|
||||||
|
episode_duration_from: 1,
|
||||||
|
episode_duration_to: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "До 30 минут",
|
||||||
|
episode_duration_from: 1,
|
||||||
|
episode_duration_to: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Более 30 минут",
|
||||||
|
episode_duration_from: 30,
|
||||||
|
episode_duration_to: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const FilterStatusIdToString = {
|
||||||
|
1: "Вышел",
|
||||||
|
2: "Выходит",
|
||||||
|
3: "Анонс",
|
||||||
|
};
|
||||||
|
export const FilterAgeRatingToString = {
|
||||||
|
1: "0+",
|
||||||
|
2: "6+",
|
||||||
|
3: "12+",
|
||||||
|
4: "16+",
|
||||||
|
5: "18+",
|
||||||
|
};
|
||||||
|
export const FilterSortToString = {
|
||||||
|
0: "По дате добавления",
|
||||||
|
1: "По рейтингу",
|
||||||
|
2: "По годам",
|
||||||
|
3: "По популярности",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Filter = {
|
||||||
|
country: null | string;
|
||||||
|
category_id: null | number;
|
||||||
|
genres: string[];
|
||||||
|
is_genres_exclude_mode_enabled: boolean;
|
||||||
|
profile_list_exclusions: number[];
|
||||||
|
types: number[]; // fetched from /type/all
|
||||||
|
studio: null | string;
|
||||||
|
source: null | string;
|
||||||
|
start_year: null | number;
|
||||||
|
end_year: null | number;
|
||||||
|
season: null | number;
|
||||||
|
episodes_from: null | number;
|
||||||
|
episodes_to: null | number;
|
||||||
|
episode_duration_from: null | number;
|
||||||
|
episode_duration_to: null | number;
|
||||||
|
status_id: null | number;
|
||||||
|
age_ratings: number[];
|
||||||
|
sort: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterDefault: Filter = {
|
||||||
|
country: null,
|
||||||
|
season: null,
|
||||||
|
sort: 0,
|
||||||
|
source: null,
|
||||||
|
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: [],
|
||||||
|
is_genres_exclude_mode_enabled: false,
|
||||||
|
profile_list_exclusions: [],
|
||||||
|
start_year: null,
|
||||||
|
status_id: null,
|
||||||
|
types: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function FetchFilter(
|
||||||
|
{
|
||||||
|
country,
|
||||||
|
category_id,
|
||||||
|
genres,
|
||||||
|
is_genres_exclude_mode_enabled,
|
||||||
|
profile_list_exclusions,
|
||||||
|
types,
|
||||||
|
studio,
|
||||||
|
source,
|
||||||
|
start_year,
|
||||||
|
end_year,
|
||||||
|
season,
|
||||||
|
episodes_from,
|
||||||
|
episodes_to,
|
||||||
|
episode_duration_from,
|
||||||
|
episode_duration_to,
|
||||||
|
status_id,
|
||||||
|
age_ratings,
|
||||||
|
sort,
|
||||||
|
}: Filter,
|
||||||
|
page: number,
|
||||||
|
token: null | string
|
||||||
|
) {
|
||||||
let url: string;
|
let url: string;
|
||||||
url = `${ENDPOINTS.filter}/${page}`;
|
url = `${ENDPOINTS.filter}/${page}`;
|
||||||
if (token) {
|
if (token) {
|
||||||
url += `?token=${token}`;
|
url += `?token=${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: Object = fetch(url, {
|
const { data, error } = await fetchDataViaPost(
|
||||||
method: "POST",
|
url,
|
||||||
headers: HEADERS,
|
JSON.stringify({
|
||||||
body: JSON.stringify(body),
|
country,
|
||||||
})
|
category_id,
|
||||||
.then((response) => {
|
genres,
|
||||||
if (response.ok) {
|
is_genres_exclude_mode_enabled,
|
||||||
return response.json();
|
profile_list_exclusions,
|
||||||
} else {
|
types,
|
||||||
throw new Error("Error fetching data");
|
studio,
|
||||||
}
|
source,
|
||||||
|
start_year,
|
||||||
|
end_year,
|
||||||
|
season,
|
||||||
|
episodes_from,
|
||||||
|
episodes_to,
|
||||||
|
episode_duration_from,
|
||||||
|
episode_duration_to,
|
||||||
|
status_id,
|
||||||
|
age_ratings,
|
||||||
|
sort,
|
||||||
})
|
})
|
||||||
.then((data: Object) => {
|
);
|
||||||
return data;
|
|
||||||
})
|
return [data, error];
|
||||||
.catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BookmarksList = {
|
export const BookmarksList = {
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) and (min-width: 1024px) {
|
||||||
|
.swiper {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.section:hover .swiper-button {
|
.section:hover .swiper-button {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
|
|
@ -55,7 +55,7 @@ export const CollectionCourusel = (props: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<div className="swiper">
|
<div className={`swiper ${Styles.swiper}`}>
|
||||||
<div className="swiper-wrapper">
|
<div className="swiper-wrapper">
|
||||||
{props.isMyCollections && (
|
{props.isMyCollections && (
|
||||||
<div className="swiper-slide" style={{ width: "fit-content" }}>
|
<div className="swiper-slide" style={{ width: "fit-content" }}>
|
||||||
|
|
|
@ -5,60 +5,43 @@ import Image from "next/image";
|
||||||
export const CollectionLink = (props: any) => {
|
export const CollectionLink = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`/collection/${props.id}`}>
|
<Link href={`/collection/${props.id}`}>
|
||||||
<div className="w-full aspect-video group">
|
<div className="relative w-full overflow-hidden rounded-lg group aspect-video">
|
||||||
<div
|
<Image
|
||||||
className="relative w-full h-full overflow-hidden bg-center bg-no-repeat bg-cover rounded-sm group-hover:animate-bg_zoom animate-bg_zoom_rev group-hover:[background-size:110%] "
|
src={props.image}
|
||||||
style={{
|
fill={true}
|
||||||
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%)`,
|
alt={""}
|
||||||
}}
|
className="-z-[1] object-cover inset-0 absolute w-full h-full group-hover:scale-110 transition-all duration-300 ease-in-out"
|
||||||
>
|
/>
|
||||||
<Image
|
<div className="absolute inset-0 bg-gradient-to-t from-black to-transparent"></div>
|
||||||
src={props.image}
|
<div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-2 top-2">
|
||||||
fill={true}
|
<Chip
|
||||||
alt={props.title || ""}
|
icon_name="material-symbols--favorite"
|
||||||
className="-z-[1] object-cover"
|
name_2={props.favorites_count}
|
||||||
sizes="
|
|
||||||
(max-width: 768px) 300px,
|
|
||||||
(max-width: 1024px) 600px,
|
|
||||||
900px
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-2 top-2">
|
{props.comment_count && (
|
||||||
<Chip
|
<Chip
|
||||||
icon_name="material-symbols--favorite"
|
icon_name="material-symbols--comment"
|
||||||
name_2={props.favorites_count}
|
name_2={props.comment_count}
|
||||||
/>
|
/>
|
||||||
{props.comment_count && (
|
)}
|
||||||
<Chip
|
{props.is_private && (
|
||||||
icon_name="material-symbols--comment"
|
<div className="flex items-center justify-center bg-yellow-400 rounded-sm">
|
||||||
name_2={props.comment_count}
|
<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--lock"></span>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{props.is_private && (
|
|
||||||
<div className="flex items-center justify-center bg-yellow-400 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--lock"></span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.is_favorite && (
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="absolute bottom-0 left-0 p-2 lg:translate-y-[100%] group-hover:lg:translate-y-0 transition-transform">
|
|
||||||
<div className="transition-transform lg:-translate-y-[calc(100%_+_1rem)] group-hover:lg:translate-y-0">
|
|
||||||
<p className="text-sm font-bold text-white md:text-base lg:text-lg xl:text-xl">
|
|
||||||
{props.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{props.description && (
|
)}
|
||||||
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
|
{props.is_favorite && (
|
||||||
{`${props.description.slice(0, 125)}${
|
<div className="flex items-center justify-center bg-pink-500 rounded-sm">
|
||||||
props.description.length > 125 ? "..." : ""
|
<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>
|
||||||
}`}
|
</div>
|
||||||
</p>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="absolute transition-transform bottom-2 left-2 lg:translate-y-full group-hover:lg:translate-y-0">
|
||||||
|
<p className="text-sm font-bold text-white transition-transform md:text-base lg:text-lg xl:text-xl line-clamp-2 lg:-translate-y-full group-hover:lg:translate-y-0">
|
||||||
|
{props.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm line-clamp-2 hidden sm:[display:-webkit-box]">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const CollectionsSection = (props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<div className="grid justify-center sm:grid-cols-[repeat(auto-fit,minmax(400px,1fr))] grid-cols-[100%] gap-2">
|
<div className="grid justify-center grid-cols-2 gap-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{props.isMyCollections && <AddCollectionLink />}
|
{props.isMyCollections && <AddCollectionLink />}
|
||||||
{props.content.map((collection) => {
|
{props.content.map((collection) => {
|
||||||
return (
|
return (
|
||||||
|
@ -25,7 +25,6 @@ export const CollectionsSection = (props: {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{props.content.length == 1 && !props.isMyCollections && <div></div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
31
app/components/Discovery/CollectionsOfTheWeek.tsx
Normal file
31
app/components/Discovery/CollectionsOfTheWeek.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { CollectionCourusel } from "../CollectionCourusel/CollectionCourusel";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
|
||||||
|
export const CollectionsOfTheWeek = () => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
`${ENDPOINTS.discover.collections}/-1?previous_page=0&where=2&sort=4${token ? `&token=${token}` : ""}`,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return <></>;
|
||||||
|
if (isLoading) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollectionCourusel
|
||||||
|
sectionTitle="Коллекции недели"
|
||||||
|
showAllLink={`/discovery/collections?sort=week_popular`}
|
||||||
|
content={data.content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
50
app/components/Discovery/DiscussingToday.tsx
Normal file
50
app/components/Discovery/DiscussingToday.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PosterWithStuff } from "../ReleasePoster/PosterWithStuff";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export const DiscussingToday = () => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
`${ENDPOINTS.discover.discussing}${token ? `?token=${token}` : ""}`,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return <></>;
|
||||||
|
if (isLoading) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between px-4 py-2 border-b-2 border-black dark:border-white">
|
||||||
|
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
||||||
|
Обсуждаемое сегодня
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 my-4 overflow-auto">
|
||||||
|
{data.content.map((item) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={`discover-discussing-${item.id}`}
|
||||||
|
href={`/release/${item.id}`}
|
||||||
|
className="min-w-[256px]"
|
||||||
|
>
|
||||||
|
<PosterWithStuff
|
||||||
|
settings={{ showDescription: false, showGenres: false }}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
21
app/components/Discovery/InterestingCarousel.module.css
Normal file
21
app/components/Discovery/InterestingCarousel.module.css
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.swiper-button:global(.swiper-button-disabled) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) and (min-width: 1024px) {
|
||||||
|
.swiper {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.swiper:hover .swiper-button {
|
||||||
|
display: flex !important;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
}
|
78
app/components/Discovery/InterestingCarousel.tsx
Normal file
78
app/components/Discovery/InterestingCarousel.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
import { Navigation } from "swiper/modules";
|
||||||
|
import Styles from "./InterestingCarousel.module.css";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const InterestingCarousel = () => {
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
ENDPOINTS.discover.interesting,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return <></>;
|
||||||
|
if (isLoading) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation]}
|
||||||
|
spaceBetween={8}
|
||||||
|
slidesPerView={"auto"}
|
||||||
|
direction={"horizontal"}
|
||||||
|
rewind={true}
|
||||||
|
navigation={{
|
||||||
|
nextEl: ".swiper-button-next",
|
||||||
|
prevEl: ".swiper-button-prev",
|
||||||
|
}}
|
||||||
|
allowTouchMove={true}
|
||||||
|
className={Styles.swiper}
|
||||||
|
>
|
||||||
|
{data.content.map((item) => {
|
||||||
|
return (
|
||||||
|
<SwiperSlide
|
||||||
|
key={`discover-interesting-${item.id}`}
|
||||||
|
style={{ maxWidth: "fit-content" }}
|
||||||
|
>
|
||||||
|
<Link href={`/release/${item.action}`}>
|
||||||
|
<div className="relative w-[300px] md:w-[480px] aspect-video rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={item.image}
|
||||||
|
alt=""
|
||||||
|
fill={true}
|
||||||
|
className="absolute inset-0 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black to-transparent"></div>
|
||||||
|
<div className="absolute bottom-2 left-2 right-2">
|
||||||
|
<p className="text-xl font-bold">{item.title}</p>
|
||||||
|
<p>{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<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`}
|
||||||
|
style={{ "--swiper-navigation-size": "64px" } as React.CSSProperties}
|
||||||
|
></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`}
|
||||||
|
style={{ "--swiper-navigation-size": "64px" } as React.CSSProperties}
|
||||||
|
></div>
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
106
app/components/Discovery/Modal/FiltersAgeRatingModal.tsx
Normal file
106
app/components/Discovery/Modal/FiltersAgeRatingModal.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FilterAgeRatingToString } from "#/api/utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
ageRatings: number[];
|
||||||
|
setAgeRatings: (lists: number[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersAgeRatingModal = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
ageRatings,
|
||||||
|
setAgeRatings,
|
||||||
|
}: Props) => {
|
||||||
|
const [newAgeRatings, setNewAgeRatings] = useState(ageRatings);
|
||||||
|
|
||||||
|
function toggleRating(number: number) {
|
||||||
|
if (newAgeRatings.includes(number)) {
|
||||||
|
setNewAgeRatings(newAgeRatings.filter((rating) => rating != number));
|
||||||
|
} else {
|
||||||
|
setNewAgeRatings([...newAgeRatings, number]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNewAgeRatings(ageRatings);
|
||||||
|
}, [ageRatings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible>
|
||||||
|
<ModalHeader>Выберите списки</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{Object.entries(FilterAgeRatingToString).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
key={`filter-age-rating-${value}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-age-rating-${value}`}
|
||||||
|
onChange={() => toggleRating(Number(key))}
|
||||||
|
checked={newAgeRatings.includes(Number(key))}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`filter-age-rating-${value}`}>{value}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAgeRatings([]);
|
||||||
|
setNewAgeRatings([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAgeRatings(newAgeRatings);
|
||||||
|
setNewAgeRatings([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
newAgeRatings.length !=
|
||||||
|
Object.keys(FilterAgeRatingToString).length
|
||||||
|
) {
|
||||||
|
setNewAgeRatings(
|
||||||
|
Object.keys(FilterAgeRatingToString).map((key) => Number(key))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNewAgeRatings([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="light"
|
||||||
|
>
|
||||||
|
{newAgeRatings.length >= Object.keys(FilterAgeRatingToString).length ?
|
||||||
|
"Снять все"
|
||||||
|
: "Выбрать все"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
141
app/components/Discovery/Modal/FiltersGenreModal.tsx
Normal file
141
app/components/Discovery/Modal/FiltersGenreModal.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FilterGenre } from "#/api/utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ToggleSwitch,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
genres: string[];
|
||||||
|
exclusionMode: boolean;
|
||||||
|
save: (genres, exclusionMode) => void;
|
||||||
|
};
|
||||||
|
export const FiltersGenreModal = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
genres,
|
||||||
|
exclusionMode,
|
||||||
|
save,
|
||||||
|
}: Props) => {
|
||||||
|
const [newGenres, setNewGenres] = useState(genres);
|
||||||
|
const [newExclusionMode, setNewExclusionMode] = useState(exclusionMode);
|
||||||
|
|
||||||
|
const genresLength =
|
||||||
|
FilterGenre.uncategorized.genres.length +
|
||||||
|
FilterGenre.audience.genres.length +
|
||||||
|
FilterGenre.theme.genres.length;
|
||||||
|
|
||||||
|
function toggleGenre(string: string) {
|
||||||
|
if (newGenres.includes(string)) {
|
||||||
|
setNewGenres(newGenres.filter((genre) => genre != string));
|
||||||
|
} else {
|
||||||
|
setNewGenres([...newGenres, string]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNewGenres(genres);
|
||||||
|
setNewExclusionMode(exclusionMode);
|
||||||
|
}, [genres, exclusionMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible size="6xl">
|
||||||
|
<ModalHeader>Жанры</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{Object.entries(FilterGenre).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div key={`filter-genre-category-${value.name}`} className="mb-4">
|
||||||
|
<p className="mb-2">{value.name}</p>
|
||||||
|
{value.genres.map((genre) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
key={`filter-genre-category-${value.name}-${genre}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-genre-category-${value.name}-genre-${genre}`}
|
||||||
|
onChange={() => toggleGenre(genre)}
|
||||||
|
checked={newGenres.includes(genre)}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`filter-genre-category-${value.name}-genre-${genre}`}
|
||||||
|
>
|
||||||
|
{genre}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<div className="flex justify-between w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="mb-1 font-bold">Режим исключения</p>
|
||||||
|
<p className="text-sm text-gray-400 dark:text-gray-300 max-w-52">
|
||||||
|
Фильтр будет искать релизы не содержащие ни один из указанных
|
||||||
|
выше жанров
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
color="blue"
|
||||||
|
onChange={() => setNewExclusionMode(!newExclusionMode)}
|
||||||
|
checked={newExclusionMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
save([], false);
|
||||||
|
setNewGenres([]);
|
||||||
|
setNewExclusionMode(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
save(newGenres, newExclusionMode);
|
||||||
|
setNewGenres([]);
|
||||||
|
setNewExclusionMode(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (newGenres.length != genresLength) {
|
||||||
|
setNewGenres(Object.entries(FilterGenre).map(([key, value]) => value.genres.map((genre) => genre)).flat());
|
||||||
|
} else {
|
||||||
|
setNewGenres([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="light"
|
||||||
|
>
|
||||||
|
{newGenres.length >= genresLength ? "Снять все" : "Выбрать все"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
107
app/components/Discovery/Modal/FiltersListExcludeModal.tsx
Normal file
107
app/components/Discovery/Modal/FiltersListExcludeModal.tsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FilterProfileListIdToString } from "#/api/utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
lists: number[];
|
||||||
|
setLists: (lists: number[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersListExcludeModal = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
lists,
|
||||||
|
setLists,
|
||||||
|
}: Props) => {
|
||||||
|
const [newList, setNewList] = useState(lists);
|
||||||
|
|
||||||
|
function toggleList(number: number) {
|
||||||
|
if (newList.includes(number)) {
|
||||||
|
setNewList(newList.filter((list) => list != number));
|
||||||
|
} else {
|
||||||
|
setNewList([...newList, number]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNewList(lists);
|
||||||
|
}, [lists]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible>
|
||||||
|
<ModalHeader>Выберите списки</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{Object.entries(FilterProfileListIdToString).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
key={`filter-list-exclude-${value}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-list-exclude-${value}`}
|
||||||
|
onChange={() => toggleList(Number(key))}
|
||||||
|
checked={newList.includes(Number(key))}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`filter-list-exclude-${value}`}>{value}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setLists([]);
|
||||||
|
setNewList([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setLists(newList);
|
||||||
|
setNewList([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
newList.length != Object.keys(FilterProfileListIdToString).length
|
||||||
|
) {
|
||||||
|
setNewList(
|
||||||
|
Object.keys(FilterProfileListIdToString).map((key) =>
|
||||||
|
Number(key)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNewList([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="light"
|
||||||
|
>
|
||||||
|
{newList.length >= Object.keys(FilterProfileListIdToString).length ?
|
||||||
|
"Снять все"
|
||||||
|
: "Выбрать все"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
565
app/components/Discovery/Modal/FiltersModal.tsx
Normal file
565
app/components/Discovery/Modal/FiltersModal.tsx
Normal file
|
@ -0,0 +1,565 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Filter,
|
||||||
|
FilterAgeRatingToString,
|
||||||
|
FilterCategoryIdToString,
|
||||||
|
FilterCountry,
|
||||||
|
FilterDefault,
|
||||||
|
FilterEpisodeCount,
|
||||||
|
FilterEpisodeDuration,
|
||||||
|
FilterProfileListIdToString,
|
||||||
|
FilterSeasonIdToString,
|
||||||
|
FilterSortToString,
|
||||||
|
FilterSource,
|
||||||
|
FilterStatusIdToString,
|
||||||
|
FilterStudio,
|
||||||
|
FilterYear,
|
||||||
|
tryCatchAPI,
|
||||||
|
} from "#/api/utils";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FiltersGenreModal } from "./FiltersGenreModal";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { FiltersListExcludeModal } from "./FiltersListExcludeModal";
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { FiltersTypesModal } from "./FiltersTypesModal";
|
||||||
|
import { FiltersAgeRatingModal } from "./FiltersAgeRatingModal";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (value: boolean) => void;
|
||||||
|
filter?: Filter;
|
||||||
|
setFilter?: (filter: Filter) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersModal = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
}: ModalProps) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [newFilter, setNewFilter] = useState(filter || FilterDefault);
|
||||||
|
const [isGenreModalOpen, setIsGenreModalOpen] = useState(false);
|
||||||
|
const [isListExcludeModalOpen, setIsListExcludeModalOpen] = useState(false);
|
||||||
|
const [isTypeModalOpen, setIsTypeModalOpen] = useState(false);
|
||||||
|
const [isAgeRatingModalOpen, setIsAgeRatingModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [types, setTypes] = useState([]);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const { data, error } = await tryCatchAPI(fetch(ENDPOINTS.filterTypes));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setError(error);
|
||||||
|
} else {
|
||||||
|
setTypes(data.types);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function saveGenres(genres, is_genres_exclude_mode_enabled) {
|
||||||
|
setNewFilter({ ...newFilter, genres, is_genres_exclude_mode_enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFilter() {
|
||||||
|
const _filter = JSON.stringify(newFilter);
|
||||||
|
if (setFilter) {
|
||||||
|
setFilter(newFilter);
|
||||||
|
} else {
|
||||||
|
router.push(`/discovery/filter?filter=${_filter}`);
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
show={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
size="4xl"
|
||||||
|
dismissible
|
||||||
|
>
|
||||||
|
<ModalHeader>Фильтр</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Страна</p>
|
||||||
|
<Dropdown
|
||||||
|
label={newFilter.country || "Неважно"}
|
||||||
|
color="blue"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-country-none`}
|
||||||
|
onClick={() => setNewFilter({ ...newFilter, country: null })}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{FilterCountry.map((item) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-country-${item}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({ ...newFilter, country: item })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Категория</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
newFilter.category_id ?
|
||||||
|
FilterCategoryIdToString[newFilter.category_id]
|
||||||
|
: "Неважно"
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-category-none`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({ ...newFilter, category_id: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{Object.entries(FilterCategoryIdToString).map(
|
||||||
|
([key, value]) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-category-${key}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
category_id: Number(key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Жанры</p>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
className="w-full min-h-10 h-fit"
|
||||||
|
onClick={() => setIsGenreModalOpen(true)}
|
||||||
|
>
|
||||||
|
{newFilter.genres.length > 0 ?
|
||||||
|
newFilter.genres.join(", ")
|
||||||
|
: "Неважно"}
|
||||||
|
</Button>
|
||||||
|
<p className="text-sm">
|
||||||
|
Будет искать релизы, содержащие каждый из указанных жанров.
|
||||||
|
Рекомендуется выбирать не более 3 жанров
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{userStore.isAuth ?
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Исключить закладки</p>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
className="w-full min-h-10 h-fit"
|
||||||
|
onClick={() => setIsListExcludeModalOpen(true)}
|
||||||
|
>
|
||||||
|
{newFilter.profile_list_exclusions.length > 0 ?
|
||||||
|
newFilter.profile_list_exclusions
|
||||||
|
.map((id) => FilterProfileListIdToString[id])
|
||||||
|
.join(", ")
|
||||||
|
: "Неважно"}
|
||||||
|
</Button>
|
||||||
|
<p className="text-sm">
|
||||||
|
Исключит из выдачи релизы, входящие в указанные закладки
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
: ""}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Варианты озвучек</p>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
className="w-full min-h-10 h-fit"
|
||||||
|
onClick={() => setIsTypeModalOpen(true)}
|
||||||
|
>
|
||||||
|
{error ?
|
||||||
|
error.message
|
||||||
|
: newFilter.types.length > 0 ?
|
||||||
|
newFilter.types
|
||||||
|
.map((type) => types.find((t) => t.id === type).name)
|
||||||
|
.join(", ")
|
||||||
|
: "Неважно"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Студия</p>
|
||||||
|
<Dropdown
|
||||||
|
label={newFilter.studio ? newFilter.studio : "Неважно"}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-studio-none`}
|
||||||
|
onClick={() => setNewFilter({ ...newFilter, studio: null })}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{FilterStudio.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-studio-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
studio: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Первоисточник</p>
|
||||||
|
<Dropdown
|
||||||
|
label={newFilter.source ? newFilter.source : "Неважно"}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-source-none`}
|
||||||
|
onClick={() => setNewFilter({ ...newFilter, source: null })}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{FilterSource.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-source-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
source: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Года</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p>С</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
newFilter.start_year ? newFilter.start_year : "Неважно"
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="mr-2 overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-year-start-none`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({ ...newFilter, start_year: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{FilterYear.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-year-start-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
start_year: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
<p>По</p>
|
||||||
|
<Dropdown
|
||||||
|
label={newFilter.end_year ? newFilter.end_year : "Неважно"}
|
||||||
|
color="blue"
|
||||||
|
className="mr-2 overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-year-end-none`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({ ...newFilter, end_year: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{FilterYear.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-year-end-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
end_year: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
<p>Сезон</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
newFilter.season ?
|
||||||
|
FilterSeasonIdToString[newFilter.season]
|
||||||
|
: "Неважно"
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="mr-2 overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-year-end-none`}
|
||||||
|
onClick={() => setNewFilter({ ...newFilter, season: null })}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{Object.entries(FilterSeasonIdToString).map(
|
||||||
|
([key, value]) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-season-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
season: Number(key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Эпизодов</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
FilterEpisodeCount.find(
|
||||||
|
(episode) =>
|
||||||
|
episode.episodes_from === newFilter.episodes_from &&
|
||||||
|
episode.episodes_to === newFilter.episodes_to
|
||||||
|
).name
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
{FilterEpisodeCount.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-episode-count-${value.name}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
episodes_from: value.episodes_from,
|
||||||
|
episodes_to: value.episodes_to,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value.name}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Длительность эпизода</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
FilterEpisodeDuration.find(
|
||||||
|
(episode) =>
|
||||||
|
episode.episode_duration_from ===
|
||||||
|
newFilter.episode_duration_from &&
|
||||||
|
episode.episode_duration_to ===
|
||||||
|
newFilter.episode_duration_to
|
||||||
|
).name
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
{FilterEpisodeDuration.map((value) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-episode-duration-${value.name}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
episode_duration_from:
|
||||||
|
value.episode_duration_from,
|
||||||
|
episode_duration_to: value.episode_duration_to,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value.name}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Статус</p>
|
||||||
|
<Dropdown
|
||||||
|
label={
|
||||||
|
newFilter.status_id ?
|
||||||
|
FilterStatusIdToString[newFilter.status_id]
|
||||||
|
: "Неважно"
|
||||||
|
}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-status-none`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({ ...newFilter, status_id: null })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Неважно
|
||||||
|
</DropdownItem>
|
||||||
|
{Object.entries(FilterStatusIdToString).map(
|
||||||
|
([key, value]) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-status-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
status_id: Number(key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Возрастное ограничение</p>
|
||||||
|
<Button
|
||||||
|
color={"blue"}
|
||||||
|
className="w-full min-h-10 h-fit"
|
||||||
|
onClick={() => setIsAgeRatingModalOpen(true)}
|
||||||
|
>
|
||||||
|
{newFilter.age_ratings.length > 0 ?
|
||||||
|
newFilter.age_ratings
|
||||||
|
.map((age_rating) => FilterAgeRatingToString[age_rating])
|
||||||
|
.join(", ")
|
||||||
|
: "Неважно"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>Сортировка</p>
|
||||||
|
<Dropdown
|
||||||
|
label={FilterSortToString[newFilter.sort]}
|
||||||
|
color="blue"
|
||||||
|
className="w-full overflow-y-auto max-h-64"
|
||||||
|
>
|
||||||
|
{Object.entries(FilterSortToString).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`filter-modal-sort-${value}`}
|
||||||
|
onClick={() =>
|
||||||
|
setNewFilter({
|
||||||
|
...newFilter,
|
||||||
|
sort: Number(key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="blue" onClick={saveFilter}>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
<FiltersGenreModal
|
||||||
|
isOpen={isGenreModalOpen}
|
||||||
|
setIsOpen={setIsGenreModalOpen}
|
||||||
|
genres={newFilter.genres}
|
||||||
|
exclusionMode={newFilter.is_genres_exclude_mode_enabled}
|
||||||
|
save={saveGenres}
|
||||||
|
/>
|
||||||
|
<FiltersListExcludeModal
|
||||||
|
isOpen={isListExcludeModalOpen}
|
||||||
|
setIsOpen={setIsListExcludeModalOpen}
|
||||||
|
lists={newFilter.profile_list_exclusions}
|
||||||
|
setLists={(profile_list_exclusions) =>
|
||||||
|
setNewFilter({ ...newFilter, profile_list_exclusions })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FiltersTypesModal
|
||||||
|
isOpen={isTypeModalOpen}
|
||||||
|
setIsOpen={setIsTypeModalOpen}
|
||||||
|
typesData={types}
|
||||||
|
types={newFilter.types}
|
||||||
|
setTypes={(types) => setNewFilter({ ...newFilter, types })}
|
||||||
|
/>
|
||||||
|
<FiltersAgeRatingModal
|
||||||
|
isOpen={isAgeRatingModalOpen}
|
||||||
|
setIsOpen={setIsAgeRatingModalOpen}
|
||||||
|
ageRatings={newFilter.age_ratings}
|
||||||
|
setAgeRatings={(age_ratings) =>
|
||||||
|
setNewFilter({ ...newFilter, age_ratings })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
100
app/components/Discovery/Modal/FiltersTypesModal.tsx
Normal file
100
app/components/Discovery/Modal/FiltersTypesModal.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
typesData: any[];
|
||||||
|
types: number[];
|
||||||
|
setTypes: (types: number[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FiltersTypesModal = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
typesData,
|
||||||
|
types,
|
||||||
|
setTypes,
|
||||||
|
}: Props) => {
|
||||||
|
const [newTypes, setNewTypes] = useState(types);
|
||||||
|
|
||||||
|
function toggleType(number: number) {
|
||||||
|
if (newTypes.includes(number)) {
|
||||||
|
setNewTypes(newTypes.filter((list) => list != number));
|
||||||
|
} else {
|
||||||
|
setNewTypes([...newTypes, number]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNewTypes(types);
|
||||||
|
}, [types]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible>
|
||||||
|
<ModalHeader>Выберите списки</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{typesData.map((item) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
key={`filter-types-${item.name}`}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`filter-types-${item.name}`}
|
||||||
|
onChange={() => toggleType(Number(item.id))}
|
||||||
|
checked={newTypes.includes(Number(item.id))}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`filter-types-${item.name}`}>{item.name}</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setTypes([]);
|
||||||
|
setNewTypes([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
Сбросить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setTypes(newTypes);
|
||||||
|
setNewTypes([]);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
Применить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (newTypes.length != typesData.length) {
|
||||||
|
setNewTypes(typesData.map((item) => Number(item.id)));
|
||||||
|
} else {
|
||||||
|
setNewTypes([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
color="light"
|
||||||
|
>
|
||||||
|
{newTypes.length >= typesData.length ? "Снять все" : "Выбрать все"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
34
app/components/Discovery/Modal/PopularFilters.ts
Normal file
34
app/components/Discovery/Modal/PopularFilters.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { FilterDefault } from "#/api/utils";
|
||||||
|
|
||||||
|
export const TabOngoing = {
|
||||||
|
id: "ongoing",
|
||||||
|
name: "Онгоинги",
|
||||||
|
filter: {
|
||||||
|
...FilterDefault,
|
||||||
|
sort: 3,
|
||||||
|
episodes_from: 1,
|
||||||
|
episodes_to: 48,
|
||||||
|
status_id: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabFinished = {
|
||||||
|
id: "finished",
|
||||||
|
name: "Завершённые",
|
||||||
|
filter: { ...FilterDefault, sort: 3, status_id: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabFilms = {
|
||||||
|
id: "films",
|
||||||
|
name: "Фильмы",
|
||||||
|
filter: { ...FilterDefault, sort: 3, category_id: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabOVA = {
|
||||||
|
id: "ova",
|
||||||
|
name: "OVA",
|
||||||
|
filter: { ...FilterDefault, sort: 3, category_id: 3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tabs = [TabOngoing, TabFinished, TabFilms, TabOVA];
|
||||||
|
export const tabsId = { ongoing: 0, finished: 1, films: 2, ova: 3 };
|
82
app/components/Discovery/Modal/PopularModal.tsx
Normal file
82
app/components/Discovery/Modal/PopularModal.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { FetchFilter, useSWRfetcher } from "#/api/utils";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
import { tabs, tabsId } from "./PopularFilters";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalHeader,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLinkUpdate";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
type ModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopularModal = ({ isOpen, setIsOpen }: ModalProps) => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const [tab, setTab] = useState<
|
||||||
|
"ongoing" | "finished" | "films" | "ova" | string
|
||||||
|
>("ongoing");
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContent(null);
|
||||||
|
async function _loadReleases() {
|
||||||
|
const [data, error] = await FetchFilter(
|
||||||
|
tabs[tabsId[tab]].filter,
|
||||||
|
0,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
if (!error) {
|
||||||
|
setContent(data.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loadReleases();
|
||||||
|
}, [tab, token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
size="7xl"
|
||||||
|
dismissible
|
||||||
|
>
|
||||||
|
<ModalHeader>Популярное</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="flex items-center justify-between w-full gap-2">
|
||||||
|
<ButtonGroup>
|
||||||
|
{tabs.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={`tabs-popular-${item.name}`}
|
||||||
|
color={tab === item.id ? "blue" : "light"}
|
||||||
|
onClick={() => setTab(item.id)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2 mt-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{content ?
|
||||||
|
content.map((release) => {
|
||||||
|
return (
|
||||||
|
<div key={release.id} className="w-full h-full">
|
||||||
|
<ReleaseLink {...release} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: <Spinner />}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
71
app/components/Discovery/Modal/ScheduleModal.tsx
Normal file
71
app/components/Discovery/Modal/ScheduleModal.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { Modal, ModalBody, ModalHeader } from "flowbite-react";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||||
|
type ModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ScheduleModal = ({ isOpen, setIsOpen }: ModalProps) => {
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
ENDPOINTS.discover.schedule,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
size="7xl"
|
||||||
|
dismissible
|
||||||
|
>
|
||||||
|
<ModalHeader>Расписание</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{!error ?
|
||||||
|
isLoading ?
|
||||||
|
<div className="flex items-center justify-center w-full h-64">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: <div className="flex flex-col gap-4">
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.monday}
|
||||||
|
sectionTitle={"Понедельник"}
|
||||||
|
/>
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.tuesday}
|
||||||
|
sectionTitle={"Вторник"}
|
||||||
|
/>
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.wednesday}
|
||||||
|
sectionTitle={"Среда"}
|
||||||
|
/>
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.thursday}
|
||||||
|
sectionTitle={"Четверг"}
|
||||||
|
/>
|
||||||
|
<ReleaseCourusel content={data.friday} sectionTitle={"Пятница"} />
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.saturday}
|
||||||
|
sectionTitle={"Суббота"}
|
||||||
|
/>
|
||||||
|
<ReleaseCourusel
|
||||||
|
content={data.sunday}
|
||||||
|
sectionTitle={"Воскресенье"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
: "Ошибка загрузки"}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
26
app/components/Discovery/RecommendedCarousel.tsx
Normal file
26
app/components/Discovery/RecommendedCarousel.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { ReleaseCourusel } from "../ReleaseCourusel/ReleaseCourusel";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export const RecommendedCarousel = () => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
token ? `${ENDPOINTS.discover.recommendations}/-1?previous_page=-1&token=${token}` : null,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!token) return <></>;
|
||||||
|
if (error) return <></>;
|
||||||
|
if (isLoading) return <></>;
|
||||||
|
|
||||||
|
return <ReleaseCourusel content={data.content} sectionTitle={"Рекомендации"} showAllLink={"/discovery/recommendations"} />;
|
||||||
|
};
|
25
app/components/Discovery/WatchingNowCarousel.tsx
Normal file
25
app/components/Discovery/WatchingNowCarousel.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { ReleaseCourusel } from "../ReleaseCourusel/ReleaseCourusel";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export const WatchingNowCarousel = () => {
|
||||||
|
const token = useUserStore((state) => state.token);
|
||||||
|
const { data, isLoading, error } = useSWR(
|
||||||
|
`${ENDPOINTS.discover.watching}/0${token ? `?token=${token}` : ""}`,
|
||||||
|
useSWRfetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return <></>;
|
||||||
|
if (isLoading) return <></>;
|
||||||
|
|
||||||
|
return <ReleaseCourusel content={data.content} sectionTitle={"Смотрят сейчас"} showAllLink={"/discovery/watching"} />;
|
||||||
|
};
|
175
app/components/Navbar/NavBarMobile.tsx
Normal file
175
app/components/Navbar/NavBarMobile.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Dropdown,
|
||||||
|
DropdownDivider,
|
||||||
|
DropdownItem,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { usePreferencesStore } from "#/store/preferences";
|
||||||
|
|
||||||
|
const NavbarItems = [
|
||||||
|
{
|
||||||
|
title: "Домашняя",
|
||||||
|
icon: "mdi--home",
|
||||||
|
href: "/",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Поиск",
|
||||||
|
icon: "mdi--search",
|
||||||
|
href: "/search",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Закладки",
|
||||||
|
icon: "mdi--bookmark-multiple",
|
||||||
|
href: "/bookmarks",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const FifthButton = {
|
||||||
|
favorites: {
|
||||||
|
title: "Избранное",
|
||||||
|
icon: "mdi--favorite",
|
||||||
|
href: "/favorites",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
title: "Коллекции",
|
||||||
|
icon: "mdi--collections-bookmark",
|
||||||
|
href: "/collections",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
title: "История",
|
||||||
|
icon: "mdi--history",
|
||||||
|
href: "/history",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
discovery: {
|
||||||
|
title: "Обзор",
|
||||||
|
icon: "mdi--compass",
|
||||||
|
href: "/discovery",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NavBarMobile = (props: { setIsSettingModalOpen: any }) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const preferenceStore = usePreferencesStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<footer className="fixed bottom-0 left-0 right-0 z-50 block w-full h-[70px] font-medium text-white bg-black rounded-t-lg lg:hidden">
|
||||||
|
<div className="flex items-center justify-center h-full gap-4">
|
||||||
|
{NavbarItems.map((item) => {
|
||||||
|
if (item.auth && !userStore.isAuth) return;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
key={`navbar-mobile-${item.title}`}
|
||||||
|
className="flex flex-col items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<span className={`iconify ${item.icon} w-6 h-6`}></span>
|
||||||
|
<span className="text-sm">{item.title}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{preferenceStore.flags.showFifthButton ?
|
||||||
|
<>
|
||||||
|
{(
|
||||||
|
!userStore.isAuth &&
|
||||||
|
FifthButton[preferenceStore.flags.showFifthButton].auth
|
||||||
|
) ?
|
||||||
|
<></>
|
||||||
|
: <Link
|
||||||
|
href={FifthButton[preferenceStore.flags.showFifthButton].href}
|
||||||
|
key={`navbar-mobile-fifthbutton-${FifthButton[preferenceStore.flags.showFifthButton].title}`}
|
||||||
|
className="flex flex-col items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`iconify ${FifthButton[preferenceStore.flags.showFifthButton].icon} w-6 h-6`}
|
||||||
|
></span>
|
||||||
|
<span className="text-sm">
|
||||||
|
{FifthButton[preferenceStore.flags.showFifthButton].title}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
: ""}
|
||||||
|
<Dropdown
|
||||||
|
arrowIcon={false}
|
||||||
|
inline
|
||||||
|
label={
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
|
<Avatar
|
||||||
|
alt=""
|
||||||
|
img={
|
||||||
|
userStore.isAuth ?
|
||||||
|
userStore.user.avatar
|
||||||
|
: "https://s.anixmirai.com/avatars/no_avatar.jpg"
|
||||||
|
}
|
||||||
|
rounded
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
<p className="text-sm">
|
||||||
|
{userStore.isAuth ? userStore.user.login : "Аноним"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userStore.isAuth && (
|
||||||
|
<>
|
||||||
|
<DropdownItem
|
||||||
|
onClick={() => router.push(`/profile/${userStore.user.id}`)}
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--user"></span>
|
||||||
|
<span className="ml-2">Профиль</span>
|
||||||
|
</DropdownItem>
|
||||||
|
{Object.entries(FifthButton).map(([key, item]) => {
|
||||||
|
if (item.auth && !userStore.isAuth) return;
|
||||||
|
if (preferenceStore.flags.showFifthButton === key) return;
|
||||||
|
return (
|
||||||
|
<DropdownItem
|
||||||
|
key={`navbar-mobile-${item.title}`}
|
||||||
|
onClick={() => router.push(item.href)}
|
||||||
|
>
|
||||||
|
<span className={`w-4 h-4 iconify ${item.icon}`}></span>
|
||||||
|
<span className="ml-2">{item.title}</span>
|
||||||
|
</DropdownItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<DropdownDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<DropdownItem
|
||||||
|
onClick={() => props.setIsSettingModalOpen(true)}
|
||||||
|
className="relative flex"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--settings"></span>
|
||||||
|
<span className="ml-2">Настройки</span>
|
||||||
|
</DropdownItem>
|
||||||
|
{userStore.isAuth ?
|
||||||
|
<DropdownItem onClick={() => userStore.logout()}>
|
||||||
|
<span className="w-4 h-4 iconify mdi--logout"></span>
|
||||||
|
<span className="ml-2">Выйти</span>
|
||||||
|
</DropdownItem>
|
||||||
|
: <DropdownItem
|
||||||
|
onClick={() => router.push(`/login?redirect=${pathname}`)}
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--login"></span>
|
||||||
|
<span className="ml-2">Войти</span>
|
||||||
|
</DropdownItem>
|
||||||
|
}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
139
app/components/Navbar/NavBarPc.tsx
Normal file
139
app/components/Navbar/NavBarPc.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Dropdown,
|
||||||
|
DropdownDivider,
|
||||||
|
DropdownItem,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const NavbarItems = [
|
||||||
|
{
|
||||||
|
title: "Домашняя",
|
||||||
|
icon: "mdi--home",
|
||||||
|
href: "/",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Обзор",
|
||||||
|
icon: "mdi--compass",
|
||||||
|
href: "/discovery",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Поиск",
|
||||||
|
icon: "mdi--search",
|
||||||
|
href: "/search",
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Закладки",
|
||||||
|
icon: "mdi--bookmark-multiple",
|
||||||
|
href: "/bookmarks",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Избранное",
|
||||||
|
icon: "mdi--favorite",
|
||||||
|
href: "/favorites",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Коллекции",
|
||||||
|
icon: "mdi--collections-bookmark",
|
||||||
|
href: "/collections",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "История",
|
||||||
|
icon: "mdi--history",
|
||||||
|
href: "/history",
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NavBarPc = (props: { setIsSettingModalOpen: any }) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="sticky top-0 left-0 right-0 z-50 hidden w-full h-16 font-medium text-white bg-black rounded-b-lg lg:block">
|
||||||
|
<div className="container flex items-center justify-between h-full px-2 mx-auto">
|
||||||
|
<div className="flex items-center h-full gap-3">
|
||||||
|
{NavbarItems.map((item) => {
|
||||||
|
if (item.auth && !userStore.isAuth) return;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
key={`navbar-pc-${item.title}`}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<span className={`iconify ${item.icon} w-6 h-6`}></span>
|
||||||
|
<span className="ml-1">{item.title}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
arrowIcon={false}
|
||||||
|
inline
|
||||||
|
label={
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar
|
||||||
|
alt=""
|
||||||
|
img={
|
||||||
|
userStore.isAuth ?
|
||||||
|
userStore.user.avatar
|
||||||
|
: "https://s.anixmirai.com/avatars/no_avatar.jpg"
|
||||||
|
}
|
||||||
|
rounded
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
<p className="ml-2">
|
||||||
|
{userStore.isAuth ? userStore.user.login : "Аноним"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{userStore.isAuth ?
|
||||||
|
<DropdownItem
|
||||||
|
onClick={() => router.push(`/profile/${userStore.user.id}`)}
|
||||||
|
className="relative flex"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--user"></span>
|
||||||
|
<span className="ml-2">Профиль</span>
|
||||||
|
</DropdownItem>
|
||||||
|
: ""}
|
||||||
|
<DropdownItem
|
||||||
|
onClick={() => props.setIsSettingModalOpen(true)}
|
||||||
|
className="relative flex"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--settings"></span>
|
||||||
|
<span className="ml-2">Настройки</span>
|
||||||
|
</DropdownItem>
|
||||||
|
{userStore.isAuth ?
|
||||||
|
<DropdownItem
|
||||||
|
onClick={() => userStore.logout()}
|
||||||
|
className="relative flex"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--logout"></span>
|
||||||
|
<span className="ml-2">Выйти</span>
|
||||||
|
</DropdownItem>
|
||||||
|
: <DropdownItem
|
||||||
|
onClick={() => router.push(`/login?redirect=${pathname}`)}
|
||||||
|
className="relative flex"
|
||||||
|
>
|
||||||
|
<span className="w-4 h-4 iconify mdi--login"></span>
|
||||||
|
<span className="ml-2">Войти</span>
|
||||||
|
</DropdownItem>
|
||||||
|
}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,215 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useUserStore } from "#/store/auth";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { SettingsModal } from "#/components/SettingsModal/SettingsModal";
|
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
|
||||||
|
|
||||||
export const Navbar = () => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
|
|
||||||
const preferenceStore = usePreferencesStore();
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Домашняя",
|
|
||||||
href: "/",
|
|
||||||
hrefInCategory: "/home",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--home-outline",
|
|
||||||
active: "material-symbols--home",
|
|
||||||
},
|
|
||||||
isAuthRequired: false,
|
|
||||||
isShownOnMobile: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Поиск",
|
|
||||||
href: "/search",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--search",
|
|
||||||
active: "material-symbols--search",
|
|
||||||
},
|
|
||||||
isAuthRequired: false,
|
|
||||||
isShownOnMobile: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Закладки",
|
|
||||||
href: "/bookmarks",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--bookmarks-outline",
|
|
||||||
active: "material-symbols--bookmarks",
|
|
||||||
},
|
|
||||||
isAuthRequired: true,
|
|
||||||
isShownOnMobile: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Избранное",
|
|
||||||
href: "/favorites",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--favorite-outline",
|
|
||||||
active: "material-symbols--favorite",
|
|
||||||
},
|
|
||||||
isAuthRequired: true,
|
|
||||||
isShownOnMobile: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Коллекции",
|
|
||||||
href: "/collections",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--collections-bookmark-outline",
|
|
||||||
active: "material-symbols--collections-bookmark",
|
|
||||||
},
|
|
||||||
isAuthRequired: true,
|
|
||||||
isShownOnMobile: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "История",
|
|
||||||
href: "/history",
|
|
||||||
icon: {
|
|
||||||
default: "material-symbols--history",
|
|
||||||
active: "material-symbols--history",
|
|
||||||
},
|
|
||||||
isAuthRequired: true,
|
|
||||||
isShownOnMobile: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<header className="fixed bottom-0 left-0 z-50 w-full text-white bg-black rounded-t-lg sm:sticky sm:top-0 sm:rounded-t-none sm:rounded-b-lg">
|
|
||||||
<div className={`container flex items-center min-h-[76px] justify-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-0" : "gap-4"} mx-auto sm:gap-0 sm:justify-between`}>
|
|
||||||
<div className={`flex items-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-4" : "gap-8"} px-2 py-4 sm:gap-4`}>
|
|
||||||
{menuItems.map((item) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={item.href}
|
|
||||||
key={`navbar__${item.id}`}
|
|
||||||
className={`flex-col items-center justify-center gap-1 lg:flex-row ${
|
|
||||||
item.isAuthRequired && !userStore.isAuth ? "hidden"
|
|
||||||
: item.isShownOnMobile ? "flex"
|
|
||||||
: "hidden sm:flex"
|
|
||||||
} ${[item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1]) ? "font-bold" : "font-medium"} transition-all`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`w-6 h-6 iconify ${[item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1]) ? item.icon.active : item.icon.default}`}
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && [item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1])) ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className={`flex items-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-4" : "gap-8"} px-2 py-4 sm:gap-4`}>
|
|
||||||
{!userStore.isAuth ?
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
pathname != "/login" ? `/login?redirect=${pathname}` : "#"
|
|
||||||
}
|
|
||||||
className={`flex items-center flex-col lg:flex-row gap-1 ${pathname == "/login" ? "font-bold" : "font-medium"} transition-all`}
|
|
||||||
>
|
|
||||||
<span className="w-6 h-6 iconify material-symbols--login"></span>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == "/login") ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
: <>
|
|
||||||
<Link
|
|
||||||
href={`/profile/${userStore.user.id}`}
|
|
||||||
className={`hidden lg:flex flex-col lg:flex-row items-center gap-1 ${pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"} transition-all`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={userStore.user.avatar}
|
|
||||||
alt=""
|
|
||||||
className="w-6 h-6 rounded-full"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == `/profile/${userStore.user.id}`) ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
{userStore.user.login}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
{preferenceStore.flags.showFifthButton ?
|
|
||||||
<Link
|
|
||||||
href={menuItems[preferenceStore.flags.showFifthButton].href}
|
|
||||||
className={`flex flex-col sm:hidden items-center gap-1 ${pathname == menuItems[preferenceStore.flags.showFifthButton].href ? "font-bold" : "font-medium"} transition-all`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`w-6 h-6 iconify ${pathname == menuItems[preferenceStore.flags.showFifthButton].href ? menuItems[preferenceStore.flags.showFifthButton].icon.active : menuItems[preferenceStore.flags.showFifthButton].icon.default}`}
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == menuItems[preferenceStore.flags.showFifthButton].href) ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
{menuItems[preferenceStore.flags.showFifthButton].title}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
: ""}
|
|
||||||
<Link
|
|
||||||
href={`/menu`}
|
|
||||||
className={`flex flex-col lg:hidden items-center gap-1 ${pathname == `/menu` || pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"} transition-all`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={userStore.user.avatar}
|
|
||||||
alt=""
|
|
||||||
className="w-6 h-6 rounded-full"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && (pathname == `/menu` || pathname == `/profile/${userStore.user.id}`)) ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
{userStore.user.login}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<button
|
|
||||||
className={`${userStore.isAuth ? "hidden lg:flex" : "flex"} flex-col items-center gap-1 lg:flex-row`}
|
|
||||||
onClick={() => setIsSettingModalOpen(true)}
|
|
||||||
>
|
|
||||||
<span className="w-6 h-6 iconify material-symbols--settings-outline-rounded"></span>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
Настройки
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{userStore.isAuth && (
|
|
||||||
<button
|
|
||||||
className="flex-col items-center hidden gap-1 lg:flex-row lg:flex"
|
|
||||||
onClick={() => userStore.logout()}
|
|
||||||
>
|
|
||||||
<span className="w-6 h-6 iconify material-symbols--logout"></span>
|
|
||||||
<span
|
|
||||||
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "lg:hidden xl:block" : "hidden"}`}
|
|
||||||
>
|
|
||||||
Выйти
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<SettingsModal
|
|
||||||
isOpen={isSettingModalOpen}
|
|
||||||
setIsOpen={setIsSettingModalOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -3,12 +3,21 @@ import Link from "next/link";
|
||||||
import ApexCharts from "apexcharts";
|
import ApexCharts from "apexcharts";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { minutesToTime } from "#/api/utils";
|
import { minutesToTime } from "#/api/utils";
|
||||||
|
import { ReleaseInfoSearchLink } from "../ReleaseInfo/ReleaseInfo.SearchLink";
|
||||||
|
|
||||||
|
type preferredItem = {
|
||||||
|
name: string;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const ProfileStats = (props: {
|
export const ProfileStats = (props: {
|
||||||
lists: Array<number>;
|
lists: Array<number>;
|
||||||
watched_count: number;
|
watched_count: number;
|
||||||
watched_time: number;
|
watched_time: number;
|
||||||
profile_id: number
|
profile_id: number;
|
||||||
|
preferred_genres: Array<preferredItem>;
|
||||||
|
preferred_audiences: Array<preferredItem>;
|
||||||
|
preferred_themes: Array<preferredItem>;
|
||||||
}) => {
|
}) => {
|
||||||
const getChartOptions = () => {
|
const getChartOptions = () => {
|
||||||
return {
|
return {
|
||||||
|
@ -81,41 +90,95 @@ export const ProfileStats = (props: {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="align-center whitespace-nowrap">
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-y-2 gap-x-4">
|
||||||
<span className="inline-block rounded w-4 h-4 bg-[#66bb6c]"></span>{" "}
|
<p className="align-center whitespace-nowrap">
|
||||||
Смотрю <span className="font-bold">{props.lists[0]}</span>
|
<span className="inline-block rounded w-4 h-4 bg-[#66bb6c]"></span>{" "}
|
||||||
</p>
|
Смотрю <span className="font-bold">{props.lists[0]}</span>
|
||||||
<p className="align-center whitespace-nowrap">
|
</p>
|
||||||
<span className="inline-block rounded w-4 h-4 bg-[#b566bb]"></span>{" "}
|
<p className="align-center whitespace-nowrap">
|
||||||
В планах <span className="font-bold">{props.lists[1]}</span>
|
<span className="inline-block rounded w-4 h-4 bg-[#b566bb]"></span>{" "}
|
||||||
</p>
|
В планах <span className="font-bold">{props.lists[1]}</span>
|
||||||
<p className="align-center whitespace-nowrap">
|
</p>
|
||||||
<span className="inline-block rounded w-4 h-4 bg-[#5c6cc0]"></span>{" "}
|
<p className="align-center whitespace-nowrap">
|
||||||
Просмотрено <span className="font-bold">{props.lists[2]}</span>
|
<span className="inline-block rounded w-4 h-4 bg-[#5c6cc0]"></span>{" "}
|
||||||
</p>
|
Просмотрено <span className="font-bold">{props.lists[2]}</span>
|
||||||
<p className="align-center whitespace-nowrap">
|
</p>
|
||||||
<span className="inline-block rounded w-4 h-4 bg-[#ffca28]"></span>{" "}
|
<p className="align-center whitespace-nowrap">
|
||||||
Отложено <span className="font-bold">{props.lists[3]}</span>
|
<span className="inline-block rounded w-4 h-4 bg-[#ffca28]"></span>{" "}
|
||||||
</p>
|
Отложено <span className="font-bold">{props.lists[3]}</span>
|
||||||
<p className="align-center whitespace-nowrap">
|
</p>
|
||||||
<span className="inline-block rounded w-4 h-4 bg-[#ef5450]"></span>{" "}
|
<p className="align-center whitespace-nowrap">
|
||||||
Брошено <span className="font-bold">{props.lists[4]}</span>
|
<span className="inline-block rounded w-4 h-4 bg-[#ef5450]"></span>{" "}
|
||||||
</p>
|
Брошено <span className="font-bold">{props.lists[4]}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<p>
|
||||||
|
Жанры:{" "}
|
||||||
|
<span>
|
||||||
|
{props.preferred_genres.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div key={`preferred-genres-${item.name}`} className="inline">
|
||||||
|
{index > 0 && ", "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={item.name}
|
||||||
|
searchBy={"tag"}
|
||||||
|
/>{" "}
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{item.percentage}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Аудитория:{" "}
|
||||||
|
<span>
|
||||||
|
{props.preferred_audiences.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div key={`preferred-audiences-${item.name}`} className="inline">
|
||||||
|
{index > 0 && ", "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={item.name}
|
||||||
|
searchBy={"tag"}
|
||||||
|
/>{" "}
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{item.percentage}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Тематика:{" "}
|
||||||
|
<span>
|
||||||
|
{props.preferred_themes.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div key={`preferred-themes-${item.name}`} className="inline">
|
||||||
|
{index > 0 && ", "}
|
||||||
|
<ReleaseInfoSearchLink
|
||||||
|
title={item.name}
|
||||||
|
searchBy={"tag"}
|
||||||
|
/>{" "}
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{item.percentage}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Просмотрено серий:{" "}
|
||||||
|
<span className="font-bold">{props.watched_count}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Время просмотра:{" "}
|
||||||
|
<span className="font-bold">
|
||||||
|
~{minutesToTime(props.watched_time)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="donut-chart"></div>
|
<div id="donut-chart"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
Просмотрено серий:{" "}
|
|
||||||
<span className="font-bold">{props.watched_count}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Время просмотра:{" "}
|
|
||||||
<span className="font-bold">
|
|
||||||
~{minutesToTime(props.watched_time, "daysHours")}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const RelatedSection = (props: any) => {
|
||||||
<div className="flex items-center justify-center p-4">
|
<div className="flex items-center justify-center p-4">
|
||||||
{props.images.map((item, index) => {
|
{props.images.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={`related-img-${index}`} className="w-[100px] lg:w-[300px] aspect-[9/12] even:scale-110 shadow-md even:[box-shadow:_0px_0px_16px_black;] even:z-30 origin-center first:[transform:translateX(25%)] last:[transform:translateX(-25%)] rounded-lg overflow-hidden">
|
<div key={`related-img-${index}`} className="w-[100px] lg:w-[300px] aspect-[9/12] even:scale-110 shadow-md even:[box-shadow:_0px_0px_16px_black;] even:z-20 origin-center first:[transform:translateX(25%)] last:[transform:translateX(-25%)] rounded-lg overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
fill={true}
|
fill={true}
|
||||||
src={item}
|
src={item}
|
||||||
|
|
|
@ -40,12 +40,6 @@ export const ReleaseCourusel = (props: {
|
||||||
prevEl: ".swiper-button-prev"
|
prevEl: ".swiper-button-prev"
|
||||||
}}
|
}}
|
||||||
allowTouchMove={true}
|
allowTouchMove={true}
|
||||||
breakpoints={{
|
|
||||||
1800: {
|
|
||||||
initialSlide: 2,
|
|
||||||
centeredSlides: true
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={Styles.swiper}
|
className={Styles.swiper}
|
||||||
>
|
>
|
||||||
{props.content.map((release) => {
|
{props.content.map((release) => {
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const ReleaseInfoInfo = (props: {
|
||||||
{"/"}
|
{"/"}
|
||||||
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
|
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
|
||||||
{props.duration != 0 &&
|
{props.duration != 0 &&
|
||||||
`по ${minutesToTime(props.duration, "daysHours")}`}
|
`по ${minutesToTime(props.duration)}`}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
// const searchBy = {
|
|
||||||
// title: 0,
|
|
||||||
// studio: 1,
|
|
||||||
// director: 2,
|
|
||||||
// author: 3,
|
|
||||||
// genre: 4
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: сделать какую-нибудь анимацию на ссылке при наведении и фокусе
|
// TODO: сделать какую-нибудь анимацию на ссылке при наведении и фокусе
|
||||||
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string | number | null }) => {
|
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string }) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="underline"
|
className="text-gray-700 transition-colors duration-300 hover:text-black dark:text-gray-300 hover:dark:text-white"
|
||||||
href={`/search?q=${props.title}&searchBy=${props.searchBy}`}
|
href={`/search?query=${props.title}¶ms={"where"%3A"releases"%2C"searchBy"%3A"${props.searchBy}"}`}
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -83,7 +83,7 @@ export const PosterWithStuff = (props: {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={`release_${props.id}_genre_${genre}_${index}`}
|
key={`release_${props.id}_genre_${genre}_${index}`}
|
||||||
className="font-light leading-none text-white md:text-sm lg:text-base xl:text-lg"
|
className="hidden font-light leading-none text-white sm:inline md:text-sm lg:text-base xl:text-lg"
|
||||||
>
|
>
|
||||||
{index > 0 && ", "}
|
{index > 0 && ", "}
|
||||||
{genre}
|
{genre}
|
||||||
|
@ -91,18 +91,18 @@ export const PosterWithStuff = (props: {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{props.title_ru && (
|
{props.title_ru && (
|
||||||
<p className="py-1 text-xl font-bold leading-none text-white md:text-2xl md:py-0">
|
<p className="text-xl font-bold leading-none text-white md:text-2xl md:py-0 line-clamp-2 lg:line-clamp-3">
|
||||||
{props.title_ru}
|
{props.title_ru}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{props.title_original && (
|
{props.title_original && (
|
||||||
<p className="text-sm leading-none text-gray-300 md:text-base">
|
<p className="hidden mt-2 text-sm leading-none text-gray-300 sm:[display:-webkit-box] md:text-base line-clamp-2">
|
||||||
{props.title_original}
|
{props.title_original}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{settings.showDescription && props.description && (
|
{settings.showDescription && props.description && (
|
||||||
<p className="mt-2 text-sm font-light leading-none text-white lg:text-base xl:text-lg line-clamp-4">
|
<p className="hidden mt-2 text-sm font-light leading-none text-white sm:block lg:text-base xl:text-lg line-clamp-4">
|
||||||
{props.description}
|
{props.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const ReleaseSection = (props: {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-2 gap-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||||
{props.content.map((release) => {
|
{props.content.map((release) => {
|
||||||
return (
|
return (
|
||||||
<div key={release.id} className="w-full h-full">
|
<div key={release.id} className="w-full h-full">
|
||||||
|
@ -23,8 +23,8 @@ export const ReleaseSection = (props: {
|
||||||
chipsSettings={{
|
chipsSettings={{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lastWatchedHidden:
|
lastWatchedHidden:
|
||||||
(props.sectionTitle &&
|
props.sectionTitle &&
|
||||||
props.sectionTitle.toLowerCase() != "история")
|
props.sectionTitle.toLowerCase() != "история",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,17 +35,11 @@ const BookmarksCategory = {
|
||||||
abandoned: "Заброшено",
|
abandoned: "Заброшено",
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavbarTitles = {
|
|
||||||
always: "Всегда",
|
|
||||||
links: "Только ссылки",
|
|
||||||
selected: "Только выбранные",
|
|
||||||
never: "Никогда",
|
|
||||||
};
|
|
||||||
|
|
||||||
const FifthButton = {
|
const FifthButton = {
|
||||||
3: "Избранное",
|
favorites: "Избранное",
|
||||||
4: "Коллекции",
|
collections: "Коллекции",
|
||||||
5: "История",
|
history: "История",
|
||||||
|
discovery: "Обзор",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
|
@ -56,7 +50,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
const [isPlayerConfigured, setIsPlayerConfigured] = useState(false);
|
const [isPlayerConfigured, setIsPlayerConfigured] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const NEXT_PUBLIC_PLAYER_PARSER_URL = env("NEXT_PUBLIC_PLAYER_PARSER_URL") || null;
|
const NEXT_PUBLIC_PLAYER_PARSER_URL =
|
||||||
|
env("NEXT_PUBLIC_PLAYER_PARSER_URL") || null;
|
||||||
if (NEXT_PUBLIC_PLAYER_PARSER_URL) {
|
if (NEXT_PUBLIC_PLAYER_PARSER_URL) {
|
||||||
setIsPlayerConfigured(true);
|
setIsPlayerConfigured(true);
|
||||||
}
|
}
|
||||||
|
@ -176,35 +171,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
: ""}
|
: ""}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className=" dark:text-white max-w-96">
|
|
||||||
Показывать название пункта в навигации
|
|
||||||
</p>
|
|
||||||
<Dropdown
|
|
||||||
color="blue"
|
|
||||||
label={NavbarTitles[preferenceStore.flags.showNavbarTitles]}
|
|
||||||
>
|
|
||||||
{Object.keys(NavbarTitles).map(
|
|
||||||
(key: "always" | "links" | "selected" | "never") => {
|
|
||||||
return (
|
|
||||||
<DropdownItem
|
|
||||||
className={`${key == "links" ? "hidden lg:flex" : ""}`}
|
|
||||||
key={`navbar-titles-${key}`}
|
|
||||||
onClick={() =>
|
|
||||||
preferenceStore.setFlags({
|
|
||||||
showNavbarTitles: key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{NavbarTitles[key]}
|
|
||||||
</DropdownItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
{userStore.isAuth ?
|
{userStore.isAuth ?
|
||||||
<div className="flex items-center justify-between sm:hidden">
|
<div className="flex items-center justify-between lg:hidden">
|
||||||
<p className=" dark:text-white max-w-96">
|
<p className=" dark:text-white max-w-96">
|
||||||
Пятый пункт в навигации
|
Пятый пункт в навигации
|
||||||
</p>
|
</p>
|
||||||
|
@ -230,9 +198,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={`navbar-fifthbutton-${key}`}
|
key={`navbar-fifthbutton-${key}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
preferenceStore.setFlags({
|
preferenceStore.setFlags({ showFifthButton: key })
|
||||||
showFifthButton: Number(key) as 3 | 4 | 5,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{FifthButton[key]}
|
{FifthButton[key]}
|
||||||
|
@ -262,7 +228,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className=" dark:text-white">Сохранять историю просмотра</p>
|
<p className=" dark:text-white">Сохранять историю просмотра</p>
|
||||||
<p className="max-w-sm text-gray-500 dark:text-gray-300">
|
<p className="max-w-sm text-sm text-gray-500 dark:text-gray-300">
|
||||||
При отключении, история не будет сохранятся как локально, так и
|
При отключении, история не будет сохранятся как локально, так и
|
||||||
на аккаунте
|
на аккаунте
|
||||||
</p>
|
</p>
|
||||||
|
@ -285,7 +251,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className=" dark:text-white">Новый плеер</p>
|
<p className=" dark:text-white">Новый плеер</p>
|
||||||
<p className="text-gray-500 dark:text-gray-300">
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
Поддерживаемые источники: Kodik, Sibnet, Libria
|
Поддерживаемые источники: Kodik, Sibnet, Libria
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,20 +12,19 @@ export const UserSection = (props: { sectionTitle?: string; content: any }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="m-4">
|
<div className="m-4">
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{props.content.map((user) => {
|
{props.content.map((user) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`/profile/${user.id}`} key={user.id} className="w-full max-w-[234px] h-full max-h-[234px] aspect-square flex-shrink-0">
|
<Link href={`/profile/${user.id}`} key={user.id}>
|
||||||
<Card className="items-center justify-center w-full h-full">
|
<Card>
|
||||||
<Avatar img={user.avatar} alt={user.login || ""} size="lg" rounded={true} />
|
<div className="flex items-center gap-4">
|
||||||
<h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">
|
<Avatar img={user.avatar} alt="" size="lg" rounded={true} className="flex-shrink-0"/>
|
||||||
{user.login}
|
<p className="text-xl font-medium text-gray-900 dark:text-white line-clamp-1">{user.login}</p>
|
||||||
</h5>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{props.content.length == 1 && <div></div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
12
app/discovery/collections/page.tsx
Normal file
12
app/discovery/collections/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DiscoverCollectionsPage } from "#/pages/DiscoverCollections";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Обзор - Коллекции",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export default function Discover() {
|
||||||
|
return <DiscoverCollectionsPage />;
|
||||||
|
}
|
12
app/discovery/filter/page.tsx
Normal file
12
app/discovery/filter/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DiscoverFilterPage } from "#/pages/DiscoverFilter";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Фильтр",
|
||||||
|
description: "Поиск по фильтру",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export default function Discover() {
|
||||||
|
return <DiscoverFilterPage />;
|
||||||
|
}
|
12
app/discovery/page.tsx
Normal file
12
app/discovery/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DiscoverPage } from "#/pages/Discover";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Обзор",
|
||||||
|
description: "Рекомендации и популярное",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export default function Discover() {
|
||||||
|
return <DiscoverPage />;
|
||||||
|
}
|
12
app/discovery/recommendations/page.tsx
Normal file
12
app/discovery/recommendations/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DiscoverRecommendationsPage } from "#/pages/DiscoverRecommendations";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Обзор - Рекомендации",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export default function Discover() {
|
||||||
|
return <DiscoverRecommendationsPage />;
|
||||||
|
}
|
12
app/discovery/watching/page.tsx
Normal file
12
app/discovery/watching/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DiscoverWatchingPage } from "#/pages/DiscoverWatching";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Обзор - Смотрят сейчас",
|
||||||
|
description: "Релизы которые сейчас смотрят",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
|
export default function Discover() {
|
||||||
|
return <DiscoverWatchingPage />;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import "./globals.css";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { ThemeModeScript } from "flowbite-react";
|
import { ThemeModeScript } from "flowbite-react";
|
||||||
import { PublicEnvScript } from 'next-runtime-env';
|
import { PublicEnvScript } from 'next-runtime-env';
|
||||||
|
import { ThemeInit } from "../.flowbite-react/init";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
metadataBase: new URL("https://anix.wah.su"),
|
metadataBase: new URL("https://anix.wah.su"),
|
||||||
|
@ -35,6 +36,7 @@ export default function RootLayout({ children }) {
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<PublicEnvScript />
|
<PublicEnvScript />
|
||||||
|
<ThemeInit />
|
||||||
<ThemeModeScript />
|
<ThemeModeScript />
|
||||||
</head>
|
</head>
|
||||||
<App>{children}</App>
|
<App>{children}</App>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const metadata = {
|
|
||||||
title: "Меню",
|
|
||||||
};
|
|
||||||
|
|
||||||
import { MenuPage } from "#/pages/MobileMenuPage";
|
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return <MenuPage />;
|
|
||||||
}
|
|
|
@ -82,7 +82,7 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
@ -105,7 +105,9 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
className="flex-1 max-w-full mx-4"
|
className="flex-1 max-w-full mx-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(`/search?q=${searchVal}&where=list&list=${props.slug}`);
|
router.push(
|
||||||
|
`/search?query=${searchVal}¶ms={"where"%3A"list"%2C"searchBy"%3A"${props.slug}"}`
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
|
|
@ -54,7 +54,9 @@ export function CollectionsPage() {
|
||||||
className="flex-1 max-w-full mx-4 mb-4"
|
className="flex-1 max-w-full mx-4 mb-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(`/search?q=${searchVal}&where=collections`);
|
router.push(
|
||||||
|
`/search?query=${searchVal}¶ms={"where"%3A"collections_fav"%2C"searchBy"%3A"none"}`
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
|
70
app/pages/Discover.tsx
Normal file
70
app/pages/Discover.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"use client";
|
||||||
|
import { CollectionsOfTheWeek } from "#/components/Discovery/CollectionsOfTheWeek";
|
||||||
|
import { DiscussingToday } from "#/components/Discovery/DiscussingToday";
|
||||||
|
import { InterestingCarousel } from "#/components/Discovery/InterestingCarousel";
|
||||||
|
import { FiltersModal } from "#/components/Discovery/Modal/FiltersModal";
|
||||||
|
import { PopularModal } from "#/components/Discovery/Modal/PopularModal";
|
||||||
|
import { ScheduleModal } from "#/components/Discovery/Modal/ScheduleModal";
|
||||||
|
import { RecommendedCarousel } from "#/components/Discovery/RecommendedCarousel";
|
||||||
|
import { WatchingNowCarousel } from "#/components/Discovery/WatchingNowCarousel";
|
||||||
|
import { Button } from "flowbite-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const DiscoverPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const [PopularModalOpen, setPopularModalOpen] = useState(false);
|
||||||
|
const [ScheduleModalOpen, setScheduleModalOpen] = useState(false);
|
||||||
|
const [FiltersModalOpen, setFiltersModalOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InterestingCarousel />
|
||||||
|
<div className="grid grid-cols-2 gap-4 my-4 lg:grid-cols-4">
|
||||||
|
<Button
|
||||||
|
size="xl"
|
||||||
|
color="yellow"
|
||||||
|
onClick={() => setPopularModalOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 inline-block w-8 h-8 mr-2 iconify mdi--fire"></span>
|
||||||
|
<span>Популярное</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xl"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => setScheduleModalOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 inline-block w-8 h-8 mr-2 iconify mdi--calendar-month"></span>
|
||||||
|
<span>Расписание</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xl"
|
||||||
|
color="purple"
|
||||||
|
onClick={() => router.push("/discovery/collections?sort=recent")}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 inline-block w-8 h-8 mr-2 iconify mdi--collections-bookmark"></span>
|
||||||
|
<span>Коллекции</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xl"
|
||||||
|
color="green"
|
||||||
|
onClick={() => setFiltersModalOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 inline-block w-8 h-8 mr-2 iconify mdi--mixer-settings"></span>
|
||||||
|
<span>Фильтр</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RecommendedCarousel />
|
||||||
|
<DiscussingToday />
|
||||||
|
<WatchingNowCarousel />
|
||||||
|
<CollectionsOfTheWeek />
|
||||||
|
|
||||||
|
<PopularModal isOpen={PopularModalOpen} setIsOpen={setPopularModalOpen} />
|
||||||
|
<ScheduleModal
|
||||||
|
isOpen={ScheduleModalOpen}
|
||||||
|
setIsOpen={setScheduleModalOpen}
|
||||||
|
/>
|
||||||
|
<FiltersModal isOpen={FiltersModalOpen} setIsOpen={setFiltersModalOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
154
app/pages/DiscoverCollections.tsx
Normal file
154
app/pages/DiscoverCollections.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { Button, ButtonGroup } from "flowbite-react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
|
export const DiscoverCollectionsSort = [
|
||||||
|
{
|
||||||
|
id: "all_time_popular",
|
||||||
|
name: "Популярные за всё время",
|
||||||
|
where: 1,
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "year_popular",
|
||||||
|
name: "Популярные за год",
|
||||||
|
where: 1,
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "season_popular",
|
||||||
|
name: "Популярные за сезон",
|
||||||
|
where: 1,
|
||||||
|
sort: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "week_popular",
|
||||||
|
name: "Популярные за неделю",
|
||||||
|
where: 1,
|
||||||
|
sort: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "recent",
|
||||||
|
name: "Недавно добавленные",
|
||||||
|
where: 1,
|
||||||
|
sort: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "random",
|
||||||
|
name: "Случайные",
|
||||||
|
where: 1,
|
||||||
|
sort: 6,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DiscoverCollectionsPage = () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [previousPage, setPreviousPage] = useState(0);
|
||||||
|
const [selectedSort, setSelectedSort] = useState(
|
||||||
|
searchParams.get("sort") || "recent"
|
||||||
|
);
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
|
||||||
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
|
|
||||||
|
let where = null;
|
||||||
|
let sort = null;
|
||||||
|
let obj = null;
|
||||||
|
obj = DiscoverCollectionsSort.find((item) => item.id == selectedSort);
|
||||||
|
|
||||||
|
if (obj) {
|
||||||
|
where = obj.where;
|
||||||
|
sort = obj.sort;
|
||||||
|
} else {
|
||||||
|
where = DiscoverCollectionsSort[5].where;
|
||||||
|
sort = DiscoverCollectionsSort[5].sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
url = `${ENDPOINTS.discover.collections}/${pageIndex}?where=${where}&sort=${sort}&previous_page=${previousPage}`;
|
||||||
|
|
||||||
|
if (userStore.token) {
|
||||||
|
url += `&token=${userStore.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||||
|
getKey,
|
||||||
|
useSWRfetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.replace("/discovery/collections?sort=" + selectedSort);
|
||||||
|
setContent(null);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedSort]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
let allContent = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
allContent.push(...data[i].content);
|
||||||
|
}
|
||||||
|
setContent(allContent);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition();
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollPosition >= 98 && scrollPosition <= 99) {
|
||||||
|
setPreviousPage(size);
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollPosition]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 overflow-x-auto">
|
||||||
|
<ButtonGroup>
|
||||||
|
{Object.entries(DiscoverCollectionsSort).map(([key, item]) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={`discover-collections-sort-${item.id}`}
|
||||||
|
onClick={() => setSelectedSort(item.id)}
|
||||||
|
color={selectedSort == item.id ? "blue" : "light"}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
{error ?
|
||||||
|
<main className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-2xl font-bold">Ошибка</h1>
|
||||||
|
<p className="text-lg">
|
||||||
|
Произошла ошибка при загрузке коллекций. Попробуйте обновить
|
||||||
|
страницу или зайдите позже.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
: isLoading || !content ?
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: <CollectionsSection content={content} sectionTitle={"Коллекции"} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
145
app/pages/DiscoverFilter.tsx
Normal file
145
app/pages/DiscoverFilter.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { FilterDefault, tryCatchAPI } from "#/api/utils";
|
||||||
|
import { FiltersModal } from "#/components/Discovery/Modal/FiltersModal";
|
||||||
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { Button } from "flowbite-react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
|
const postFetcher = async (url: string, payload: string) => {
|
||||||
|
const { data, error } = await tryCatchAPI(
|
||||||
|
fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: payload,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DiscoverFilterPage = () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const [filter, setFilter] = useState(null);
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
const [FooterH, setFooterH] = useState(null);
|
||||||
|
const [FiltersModalOpen, setFiltersModalOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const queryParams = searchParams.get("filter");
|
||||||
|
if (queryParams) {
|
||||||
|
try {
|
||||||
|
const _filter = JSON.parse(queryParams);
|
||||||
|
if (Object.keys(_filter).length != Object.keys(FilterDefault).length) {
|
||||||
|
setFilter(FilterDefault);
|
||||||
|
} else {
|
||||||
|
setFilter(_filter);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setFilter(FilterDefault);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFilter(FilterDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window) {
|
||||||
|
setFooterH(document.querySelector("footer").clientHeight + 16);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContent(null);
|
||||||
|
const url = new URL(`/discovery/filter`, window.location.origin);
|
||||||
|
url.searchParams.set("filter", JSON.stringify(filter));
|
||||||
|
router.replace(url.toString());
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
|
if (!filter) return null;
|
||||||
|
|
||||||
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
|
|
||||||
|
let url = `${ENDPOINTS.filter}/${pageIndex}`;
|
||||||
|
if (userStore.token) {
|
||||||
|
url += `?token=${userStore.token}`;
|
||||||
|
}
|
||||||
|
return [url, JSON.stringify(filter)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||||
|
getKey,
|
||||||
|
([url, payload]) => postFetcher(url, payload),
|
||||||
|
{ initialSize: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
let _content = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
_content.push(...data[i].content);
|
||||||
|
}
|
||||||
|
setContent(_content);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition();
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollPosition >= 98 && scrollPosition <= 99) {
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollPosition]);
|
||||||
|
|
||||||
|
if (!filter) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error ?
|
||||||
|
<div className="flex flex-col justify-between w-full p-4 border border-red-200 rounded-md md:flex-row bg-red-50 dark:bg-red-700 dark:border-red-600">
|
||||||
|
<div className="mb-4 md:mb-0 md:me-4">
|
||||||
|
<p>Произошла ошибка фильтра</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <></>}
|
||||||
|
{content ?
|
||||||
|
<ReleaseSection content={content} sectionTitle={"Фильтр"} />
|
||||||
|
: <></>}
|
||||||
|
{isLoading ?
|
||||||
|
<div className="flex items-center justify-center w-full h-16">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: ""}
|
||||||
|
<Button
|
||||||
|
color="green"
|
||||||
|
pill
|
||||||
|
className="fixed bottom-[var(--header-height)] right-4"
|
||||||
|
style={{ "--header-height": `${FooterH}px` } as React.CSSProperties}
|
||||||
|
onClick={() => setFiltersModalOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0 inline-block w-8 h-8 iconify mdi--mixer-settings"></span>
|
||||||
|
</Button>
|
||||||
|
<FiltersModal
|
||||||
|
isOpen={FiltersModalOpen}
|
||||||
|
setIsOpen={setFiltersModalOpen}
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
84
app/pages/DiscoverRecommendations.tsx
Normal file
84
app/pages/DiscoverRecommendations.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
|
export const DiscoverRecommendationsPage = () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [previousPage, setPreviousPage] = useState(0);
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
|
||||||
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
url = `${ENDPOINTS.discover.recommendations}/${pageIndex}?previous_page=${previousPage}&token=${userStore.token}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||||
|
getKey,
|
||||||
|
useSWRfetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
let allContent = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
allContent.push(...data[i].content);
|
||||||
|
}
|
||||||
|
setContent(allContent);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition();
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollPosition >= 98 && scrollPosition <= 99) {
|
||||||
|
setPreviousPage(size);
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollPosition]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userStore.state === "finished" && !userStore.token) {
|
||||||
|
router.push(`/login?redirect=/discovery/recommendations`);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [userStore.state, userStore.token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error ?
|
||||||
|
<main className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-2xl font-bold">Ошибка</h1>
|
||||||
|
<p className="text-lg">
|
||||||
|
Произошла ошибка при загрузке рекомендаций. Попробуйте обновить
|
||||||
|
страницу или зайдите позже.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
: !content ?
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: <ReleaseSection content={content} sectionTitle={"Рекомендации"} />}
|
||||||
|
{content && isLoading ?
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-full min-h-16">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: ""}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
79
app/pages/DiscoverWatching.tsx
Normal file
79
app/pages/DiscoverWatching.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
|
||||||
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
|
import { useUserStore } from "#/store/auth";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
|
export const DiscoverWatchingPage = () => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [content, setContent] = useState(null);
|
||||||
|
|
||||||
|
const getKey = (pageIndex: number, previousPageData: any) => {
|
||||||
|
if (previousPageData && !previousPageData.content.length) return null;
|
||||||
|
|
||||||
|
let url: string;
|
||||||
|
url = `${ENDPOINTS.discover.watching}/${pageIndex}`;
|
||||||
|
|
||||||
|
if (userStore.token) {
|
||||||
|
url += `?token=${userStore.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||||
|
getKey,
|
||||||
|
useSWRfetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
let allContent = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
allContent.push(...data[i].content);
|
||||||
|
}
|
||||||
|
setContent(allContent);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const scrollPosition = useScrollPosition();
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollPosition >= 98 && scrollPosition <= 99) {
|
||||||
|
setSize(size + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollPosition]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error ?
|
||||||
|
<main className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-2xl font-bold">Ошибка</h1>
|
||||||
|
<p className="text-lg">
|
||||||
|
Произошла ошибка при загрузке. Попробуйте обновить страницу или
|
||||||
|
зайдите позже.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
: !content ?
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: <ReleaseSection content={content} sectionTitle={"Смотрят сейчас"} />}
|
||||||
|
{content && isLoading ?
|
||||||
|
<div className="flex flex-col items-center justify-center min-w-full min-h-16">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
: ""}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -69,7 +69,9 @@ export function FavoritesPage() {
|
||||||
className="flex-1 max-w-full mx-4 mb-4"
|
className="flex-1 max-w-full mx-4 mb-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(`/search?q=${searchVal}&where=favorites`);
|
router.push(
|
||||||
|
`/search?query=${searchVal}¶ms={"where"%3A"favorites"%2C"searchBy"%3A"none"}`
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
@ -129,9 +131,9 @@ export function FavoritesPage() {
|
||||||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
||||||
<span
|
<span
|
||||||
className={`w-6 h-6 iconify ${
|
className={`w-6 h-6 iconify ${
|
||||||
sort.values[index].value.split("_")[1] == "descending"
|
sort.values[index].value.split("_")[1] == "descending" ?
|
||||||
? sort.descendingIcon
|
sort.descendingIcon
|
||||||
: sort.ascendingIcon
|
: sort.ascendingIcon
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
@ -139,18 +141,17 @@ export function FavoritesPage() {
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
{content && content.length > 0 ? (
|
{content && content.length > 0 ?
|
||||||
<ReleaseSection content={content} />
|
<ReleaseSection content={content} />
|
||||||
) : isLoading ? (
|
: isLoading ?
|
||||||
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||||
<div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
|
||||||
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
||||||
<p>В избранном пока ничего нет...</p>
|
<p>В избранном пока ничего нет...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
{data &&
|
{data &&
|
||||||
data[data.length - 1].current_page <
|
data[data.length - 1].current_page <
|
||||||
data[data.length - 1].total_page_count && (
|
data[data.length - 1].total_page_count && (
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Button } from "flowbite-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSWRfetcher } from "#/api/utils";
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
|
||||||
|
|
||||||
export function HistoryPage() {
|
export function HistoryPage() {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
const authState = useUserStore((state) => state.state);
|
const authState = useUserStore((state) => state.state);
|
||||||
|
@ -62,7 +61,9 @@ export function HistoryPage() {
|
||||||
className="flex-1 max-w-full mx-4 mb-4"
|
className="flex-1 max-w-full mx-4 mb-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(`/search?q=${searchVal}&where=history`);
|
router.push(
|
||||||
|
`/search?query=${searchVal}¶ms={"where"%3A"history"%2C"searchBy"%3A"none"}`
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
@ -106,7 +107,7 @@ export function HistoryPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{content && content.length > 0 ? (
|
{content && content.length > 0 ?
|
||||||
<>
|
<>
|
||||||
<ReleaseSection sectionTitle="История" content={content} />
|
<ReleaseSection sectionTitle="История" content={content} />
|
||||||
{data && data[0].total_count != content.length && (
|
{data && data[0].total_count != content.length && (
|
||||||
|
@ -122,16 +123,15 @@ export function HistoryPage() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : isLoading ? (
|
: isLoading ?
|
||||||
<div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]">
|
<div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||||
<div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
|
||||||
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
||||||
<p>В истории пока ничего нет...</p>
|
<p>В истории пока ничего нет...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,22 @@ import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
||||||
import { Spinner } from "#/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useUserStore } from "#/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { _FetchHomePageReleases } from "#/api/utils";
|
import { FetchFilter } from "#/api/utils";
|
||||||
|
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
import { usePreferencesStore } from "#/store/preferences";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
ListAnnounce,
|
||||||
|
ListFilms,
|
||||||
|
ListFinished,
|
||||||
|
ListLast,
|
||||||
|
ListOngoing,
|
||||||
|
} from "./IndexFilters";
|
||||||
|
|
||||||
export function IndexPage() {
|
export function IndexPage() {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
const preferenceStore = usePreferencesStore();
|
const preferenceStore = usePreferencesStore();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [lastReleasesData, setLastReleasesData] = useState(null);
|
const [lastReleasesData, setLastReleasesData] = useState(null);
|
||||||
const [ongoingReleasesData, setOngoingReleasesData] = useState(null);
|
const [ongoingReleasesData, setOngoingReleasesData] = useState(null);
|
||||||
|
@ -21,7 +28,9 @@ export function IndexPage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preferenceStore.params.skipToCategory.enabled) {
|
if (preferenceStore.params.skipToCategory.enabled) {
|
||||||
router.push(`/home/${preferenceStore.params.skipToCategory.homeCategory}`);
|
router.push(
|
||||||
|
`/home/${preferenceStore.params.skipToCategory.homeCategory}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -35,11 +44,19 @@ export function IndexPage() {
|
||||||
setAnnounceReleasesData(null);
|
setAnnounceReleasesData(null);
|
||||||
setFilmsReleasesData(null);
|
setFilmsReleasesData(null);
|
||||||
|
|
||||||
const lastReleases = await _FetchHomePageReleases("last", token);
|
const [lastReleases] = await FetchFilter(ListLast.filter, 0, token);
|
||||||
const ongoingReleases = await _FetchHomePageReleases("ongoing", token);
|
const [ongoingReleases] = await FetchFilter(ListOngoing.filter, 0, token);
|
||||||
const finishedReleases = await _FetchHomePageReleases("finished", token);
|
const [announceReleases] = await FetchFilter(
|
||||||
const announceReleases = await _FetchHomePageReleases("announce", token);
|
ListAnnounce.filter,
|
||||||
const filmsReleases = await _FetchHomePageReleases("films", token);
|
0,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
const [finishedReleases] = await FetchFilter(
|
||||||
|
ListFinished.filter,
|
||||||
|
0,
|
||||||
|
token
|
||||||
|
);
|
||||||
|
const [filmsReleases] = await FetchFilter(ListFilms.filter, 0, token);
|
||||||
|
|
||||||
setLastReleasesData(lastReleases);
|
setLastReleasesData(lastReleases);
|
||||||
setOngoingReleasesData(ongoingReleases);
|
setOngoingReleasesData(ongoingReleases);
|
||||||
|
@ -56,16 +73,12 @@ export function IndexPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{lastReleasesData ? (
|
{lastReleasesData && (
|
||||||
<ReleaseCourusel
|
<ReleaseCourusel
|
||||||
sectionTitle="Последние релизы"
|
sectionTitle="Последние релизы"
|
||||||
showAllLink="/home/last"
|
showAllLink="/home/last"
|
||||||
content={lastReleasesData.content}
|
content={lastReleasesData.content}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{finishedReleasesData && (
|
{finishedReleasesData && (
|
||||||
<ReleaseCourusel
|
<ReleaseCourusel
|
||||||
|
@ -95,15 +108,11 @@ export function IndexPage() {
|
||||||
content={filmsReleasesData.content}
|
content={filmsReleasesData.content}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading &&
|
{isLoading && (
|
||||||
!lastReleasesData &&
|
<div className="flex items-center justify-center h-32 min-w-full">
|
||||||
!finishedReleasesData &&
|
<Spinner />
|
||||||
!ongoingReleasesData &&
|
</div>
|
||||||
!announceReleasesData && (
|
)}
|
||||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
|
||||||
<h1 className="text-2xl">Ошибка загрузки контента...</h1>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { _FetchHomePageReleases } from "#/api/utils";
|
import { FetchFilter } from "#/api/utils";
|
||||||
import { Button, ButtonGroup } from "flowbite-react";
|
import { Button, ButtonGroup } from "flowbite-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { slugToFilter } from "./IndexFilters";
|
||||||
|
|
||||||
export function IndexCategoryPage(props) {
|
export function IndexCategoryPage(props) {
|
||||||
const token = useUserStore((state) => state.token);
|
const token = useUserStore((state) => state.token);
|
||||||
|
@ -20,7 +21,7 @@ export function IndexCategoryPage(props) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setContent(null);
|
setContent(null);
|
||||||
|
|
||||||
const data: any = await _FetchHomePageReleases(props.slug, token, page);
|
const [ data ] = await FetchFilter(slugToFilter[props.slug].filter, page, token);
|
||||||
|
|
||||||
setContent(data.content);
|
setContent(data.content);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -32,7 +33,7 @@ export function IndexCategoryPage(props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function _loadNextReleasesPage() {
|
async function _loadNextReleasesPage() {
|
||||||
const data: any = await _FetchHomePageReleases(props.slug, token, page);
|
const [ data ] = await FetchFilter(slugToFilter[props.slug].filter, page, token);
|
||||||
const newContent = [...content, ...data.content];
|
const newContent = [...content, ...data.content];
|
||||||
setContent(newContent);
|
setContent(newContent);
|
||||||
}
|
}
|
||||||
|
|
34
app/pages/IndexFilters.ts
Normal file
34
app/pages/IndexFilters.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { FilterDefault } from "#/api/utils";
|
||||||
|
|
||||||
|
export const ListLast = {
|
||||||
|
name: "Последнее",
|
||||||
|
filter: FilterDefault,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListOngoing = {
|
||||||
|
name: "Онгоинги",
|
||||||
|
filter: { ...FilterDefault, status_id: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListAnnounce = {
|
||||||
|
name: "Анонсы",
|
||||||
|
filter: { ...FilterDefault, status_id: 3 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListFinished = {
|
||||||
|
name: "Завершённые",
|
||||||
|
filter: { ...FilterDefault, status_id: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListFilms = {
|
||||||
|
name: "Фильмы",
|
||||||
|
filter: { ...FilterDefault, category_id: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const slugToFilter = {
|
||||||
|
last: ListLast,
|
||||||
|
ongoing: ListOngoing,
|
||||||
|
announce: ListAnnounce,
|
||||||
|
finished: ListFinished,
|
||||||
|
films: ListFilms,
|
||||||
|
}
|
|
@ -1,130 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Card } from "flowbite-react";
|
|
||||||
import { useUserStore } from "#/store/auth";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { SettingsModal } from "#/components/SettingsModal/SettingsModal";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
|
||||||
|
|
||||||
export const MenuPage = () => {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const preferenceStore = usePreferencesStore();
|
|
||||||
const router = useRouter();
|
|
||||||
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!userStore.user) {
|
|
||||||
router.push("/");
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [userStore.user]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{userStore.user && (
|
|
||||||
<div className="fixed flex flex-col justify-end gap-2 left-4 right-4 bottom-24 sm:static">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Link
|
|
||||||
href={`/profile/${userStore.user.id}`}
|
|
||||||
className="flex-1 w-full min-w-full sm:w-auto sm:min-w-0"
|
|
||||||
>
|
|
||||||
<Card className="flex-1 w-full min-w-full sm:w-auto sm:min-w-0">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Image
|
|
||||||
src={userStore.user.avatar}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
alt=""
|
|
||||||
className="w-16 h-16 rounded-full sm:w-28 sm:h-28"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-xl sm:text-2xl">
|
|
||||||
{userStore.user.login}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400 whitespace-pre-wrap sm:text-base dark:text-gray-300">
|
|
||||||
{userStore.user.status}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
<div className="flex flex-1 h-full gap-2 sm:flex-col">
|
|
||||||
<button
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => {
|
|
||||||
userStore.logout();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
|
||||||
<span
|
|
||||||
className={`iconify material-symbols--logout-rounded w-6 h-6 text-red-500`}
|
|
||||||
></span>
|
|
||||||
<p className="text-red-500">Выйти</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => {
|
|
||||||
setIsSettingModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card className="flex-1">
|
|
||||||
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
|
||||||
<span
|
|
||||||
className={`iconify material-symbols--settings-outline-rounded w-6 h-6`}
|
|
||||||
></span>
|
|
||||||
<p>Настройки</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{preferenceStore.flags.showFifthButton != 3 ?
|
|
||||||
<Link href="/favorites" className="flex-1 sm:hidden">
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={`iconify material-symbols--favorite-outline w-6 h-6`}
|
|
||||||
></span>
|
|
||||||
<p>Избранное</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
: ""}
|
|
||||||
{preferenceStore.flags.showFifthButton != 4 ?
|
|
||||||
<Link href="/collections" className="flex-1 sm:hidden">
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={`iconify material-symbols--collections-bookmark-outline w-6 h-6`}
|
|
||||||
></span>
|
|
||||||
<p>Коллекции</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
: ""}
|
|
||||||
{preferenceStore.flags.showFifthButton != 5 ?
|
|
||||||
<Link href="/history" className="flex-1 sm:hidden">
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={`iconify material-symbols--history w-6 h-6`}
|
|
||||||
></span>
|
|
||||||
<p>История</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
: ""}
|
|
||||||
<SettingsModal
|
|
||||||
isOpen={isSettingModalOpen}
|
|
||||||
setIsOpen={setIsSettingModalOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -164,6 +164,9 @@ export const ProfilePage = (props: any) => {
|
||||||
watched_count={user.watched_episode_count}
|
watched_count={user.watched_episode_count}
|
||||||
watched_time={user.watched_time}
|
watched_time={user.watched_time}
|
||||||
profile_id={user.id}
|
profile_id={user.id}
|
||||||
|
preferred_genres={user.preferred_genres || []}
|
||||||
|
preferred_audiences={user.preferred_audiences || []}
|
||||||
|
preferred_themes={user.preferred_themes || []}
|
||||||
/>
|
/>
|
||||||
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
||||||
<div className="flex flex-col gap-2 lg:hidden">
|
<div className="flex flex-col gap-2 lg:hidden">
|
||||||
|
|
|
@ -243,7 +243,7 @@ export function SearchPage() {
|
||||||
return [url, JSON.stringify({ query, searchBy })];
|
return [url, JSON.stringify({ query, searchBy })];
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize, mutate } = useSWRInfinite(
|
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||||
getKey,
|
getKey,
|
||||||
([url, payload]) => postFetcher(url, payload),
|
([url, payload]) => postFetcher(url, payload),
|
||||||
{ initialSize: 2 }
|
{ initialSize: 2 }
|
||||||
|
@ -279,7 +279,7 @@ export function SearchPage() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="sticky top-0 sm:top-[var(--header-height)] z-50 flex flex-wrap w-full gap-2 bg-black bg-opacity-25 py-2 px-2 rounded-lg backdrop-blur-sm"
|
className="sticky top-0 sm:top-[var(--header-height)] z-30 flex flex-wrap w-full gap-2 bg-black bg-opacity-25 py-2 px-2 rounded-lg backdrop-blur-sm"
|
||||||
style={{ "--header-height": `${HeaderH}px` } as React.CSSProperties}
|
style={{ "--header-height": `${HeaderH}px` } as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col flex-1 w-full lg:flex-row">
|
<div className="flex flex-col flex-1 w-full lg:flex-row">
|
||||||
|
|
|
@ -9,8 +9,7 @@ interface preferencesState {
|
||||||
// saveSearchHistory: boolean;
|
// saveSearchHistory: boolean;
|
||||||
saveWatchHistory?: boolean;
|
saveWatchHistory?: boolean;
|
||||||
showChangelog?: boolean;
|
showChangelog?: boolean;
|
||||||
showNavbarTitles?: "always" | "links" | "selected" | "never";
|
showFifthButton?: null | string;
|
||||||
showFifthButton?: null | 3 | 4 | 5;
|
|
||||||
};
|
};
|
||||||
params: {
|
params: {
|
||||||
isFirstLaunch?: boolean;
|
isFirstLaunch?: boolean;
|
||||||
|
@ -39,11 +38,9 @@ export const usePreferencesStore = create<preferencesState>()(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
_hasHydrated: false,
|
_hasHydrated: false,
|
||||||
flags: {
|
flags: {
|
||||||
// saveSearchHistory: true,
|
|
||||||
saveWatchHistory: true,
|
saveWatchHistory: true,
|
||||||
showChangelog: true,
|
showChangelog: true,
|
||||||
showNavbarTitles: "always",
|
showFifthButton: "discovery",
|
||||||
showFifthButton: null,
|
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
isFirstLaunch: true,
|
isFirstLaunch: true,
|
||||||
|
@ -80,6 +77,7 @@ export const usePreferencesStore = create<preferencesState>()(
|
||||||
persistedState as preferencesState
|
persistedState as preferencesState
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
version: 2,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const withFlowbiteReact = require("flowbite-react/plugin/nextjs");
|
import withFlowbiteReact from "flowbite-react/plugin/nextjs";
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const NextConfig = {
|
const NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
@ -80,6 +81,4 @@ const NextConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = withFlowbiteReact(NextConfig);
|
export default withFlowbiteReact(NextConfig);
|
||||||
|
|
||||||
module.exports = config;
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -7,12 +7,11 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "new",
|
"name": "new",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apexcharts": "^3.52.0",
|
"apexcharts": "^3.52.0",
|
||||||
"deepmerge-ts": "^7.1.0",
|
"deepmerge-ts": "^7.1.0",
|
||||||
"flowbite": "^2.4.1",
|
"flowbite": "^2.4.1",
|
||||||
"flowbite-react": "^0.11.7",
|
"flowbite-react": "^0.12.7",
|
||||||
"hls-video-element": "^1.5.0",
|
"hls-video-element": "^1.5.0",
|
||||||
"markdown-to-jsx": "^7.4.7",
|
"markdown-to-jsx": "^7.4.7",
|
||||||
"media-chrome": "^4.9.0",
|
"media-chrome": "^4.9.0",
|
||||||
|
@ -3348,9 +3347,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flowbite-react": {
|
"node_modules/flowbite-react": {
|
||||||
"version": "0.11.7",
|
"version": "0.12.7",
|
||||||
"resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.12.7.tgz",
|
||||||
"integrity": "sha512-Z8m+ycHEsXPacSAi8P4yYDeff7LvcHNwbGAnL/+Fpiv+6ZWDEAGY/YPKhUofZsZa837JTYrbcbmgjqQ1bpt51g==",
|
"integrity": "sha512-d8GR7mnCfdIl4n5RXxz4dKin6DIEA7Ax9mXDpJhz9gwxaPKUklKJZKtQ+KkdmFNrB65Zy76Pam01yr3LcxlseA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "1.6.9",
|
"@floating-ui/core": "1.6.9",
|
||||||
|
|
|
@ -7,14 +7,13 @@
|
||||||
"dev-with-services": "node ./run-all.dev.js",
|
"dev-with-services": "node ./run-all.dev.js",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"postinstall": "flowbite-react patch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apexcharts": "^3.52.0",
|
"apexcharts": "^3.52.0",
|
||||||
"deepmerge-ts": "^7.1.0",
|
"deepmerge-ts": "^7.1.0",
|
||||||
"flowbite": "^2.4.1",
|
"flowbite": "^2.4.1",
|
||||||
"flowbite-react": "^0.11.7",
|
"flowbite-react": "^0.12.7",
|
||||||
"hls-video-element": "^1.5.0",
|
"hls-video-element": "^1.5.0",
|
||||||
"markdown-to-jsx": "^7.4.7",
|
"markdown-to-jsx": "^7.4.7",
|
||||||
"media-chrome": "^4.9.0",
|
"media-chrome": "^4.9.0",
|
||||||
|
|
21
public/changelog/3.9.0.md
Normal file
21
public/changelog/3.9.0.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# 3.9.0
|
||||||
|
|
||||||
|
## Добавлено
|
||||||
|
|
||||||
|
- Статистика тематика, жанры и аудитория в статистике профиля
|
||||||
|
- Страница "Обзор"
|
||||||
|
|
||||||
|
## Изменено
|
||||||
|
|
||||||
|
- Секции карточек релизов теперь в 2 колонки на телефонах
|
||||||
|
- Вид карточек в поиске пользователей
|
||||||
|
- По стандарту пятой кнопкой в мобильном навбаре стоит пункт "обзор"
|
||||||
|
|
||||||
|
## Исправлено
|
||||||
|
|
||||||
|
- Неправильное время просмотра в статистике профиле в некоторых случаях
|
||||||
|
- Ссылки на переход в поиск с страницы релиза, закладок, избранных, истории, коллекциях теперь работают для нового поиска
|
||||||
|
|
||||||
|
## Удалено
|
||||||
|
|
||||||
|
- Настройки показа названий страниц в навигации
|
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
".flowbite-react\\class-list.json"
|
".flowbite-react/class-list.json"
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands", "solar"]),
|
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands", "solar"]),
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"next.config.js"
|
"next.config.mjs"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", "player-parser", "api-prox"]
|
"exclude": ["node_modules", "player-parser", "api-prox"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue