mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-03 21:15:35 +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",
|
||||
"components": [],
|
||||
"dark": true,
|
||||
"prefix": "",
|
||||
"path": "src/components",
|
||||
"prefix": "",
|
||||
"rsc": 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 (
|
||||
!apiResponse ||
|
||||
!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(
|
||||
`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/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
"x-unknown/unknown"
|
||||
"x-unknown/unknown",
|
||||
];
|
||||
|
||||
const isSupported = supportedContentTypes.includes(
|
||||
|
@ -335,7 +337,9 @@ app.post("/*path", async (req, res) => {
|
|||
if (
|
||||
!apiResponse ||
|
||||
!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(
|
||||
`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";
|
||||
import { useUserStore } from "./store/auth";
|
||||
import { usePreferencesStore } from "./store/preferences";
|
||||
import { Navbar } from "./components/Navbar/NavbarUpdate";
|
||||
import { Inter } from "next/font/google";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
|
@ -14,6 +13,9 @@ import {
|
|||
import { Spinner } from "./components/Spinner/Spinner";
|
||||
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
||||
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"] });
|
||||
|
||||
|
@ -23,6 +25,7 @@ export const App = (props) => {
|
|||
const [showChangelog, setShowChangelog] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState("");
|
||||
const [previousVersions, setPreviousVersions] = useState([]);
|
||||
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function _checkVersion() {
|
||||
|
@ -68,8 +71,8 @@ export const App = (props) => {
|
|||
<body
|
||||
className={`${inter.className} overflow-x-hidden dark:bg-[#0d1117] dark:text-white`}
|
||||
>
|
||||
<Navbar />
|
||||
<main className="container px-2 pt-4 pb-24 mx-auto sm:pb-0">
|
||||
<NavBarPc setIsSettingModalOpen={setIsSettingModalOpen} />
|
||||
<main className="container px-2 pt-4 pb-24 mx-auto lg:pb-0">
|
||||
{props.children}
|
||||
</main>
|
||||
<ChangelogModal
|
||||
|
@ -123,6 +126,11 @@ export const App = (props) => {
|
|||
theme="colored"
|
||||
transition={Bounce}
|
||||
/>
|
||||
<NavBarMobile setIsSettingModalOpen={setIsSettingModalOpen} />
|
||||
<SettingsModal
|
||||
isOpen={isSettingModalOpen}
|
||||
setIsOpen={setIsSettingModalOpen}
|
||||
/>
|
||||
</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";
|
||||
|
||||
const NEXT_PUBLIC_API_URL = env("NEXT_PUBLIC_API_URL") || null;
|
||||
|
@ -51,6 +51,7 @@ export const ENDPOINTS = {
|
|||
}
|
||||
},
|
||||
filter: `${API_PREFIX}/filter`,
|
||||
filterTypes: `${API_PREFIX}/type/all`,
|
||||
search: {
|
||||
profileList: `${API_PREFIX}/search/profile/list`,
|
||||
profileHistory: `${API_PREFIX}/search/history`,
|
||||
|
@ -74,5 +75,13 @@ export const ENDPOINTS = {
|
|||
releaseInCollections: `${API_PREFIX}/collection/all/release`,
|
||||
userCollections: `${API_PREFIX}/collection/all/profile`,
|
||||
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(
|
||||
min: number,
|
||||
type?: "full" | "daysOnly" | "daysHours"
|
||||
) {
|
||||
const d = Math.floor(min / 1440); // 60*24
|
||||
const h = Math.floor((min - d * 1440) / 60);
|
||||
const m = Math.round(min % 60);
|
||||
export function minutesToTime(min: number) {
|
||||
const seconds = min * 60;
|
||||
const epoch = new Date(0);
|
||||
const date = new Date(seconds * 1000);
|
||||
|
||||
var dDisplay =
|
||||
d > 0 ? `${d} ${numberDeclension(d, "день", "дня", "дней")}` : "";
|
||||
var hDisplay =
|
||||
h > 0 ? `${h} ${numberDeclension(h, "час", "часа", "часов")}` : "";
|
||||
var mDisplay =
|
||||
m > 0 ? `${m} ${numberDeclension(m, "минута", "минуты", "минут")}` : "";
|
||||
const diffInMinutes =
|
||||
new Date(date.getTime() - epoch.getTime()).getTime() / 1000 / 60;
|
||||
|
||||
if (type == "daysOnly") {
|
||||
if (d > 0) return dDisplay;
|
||||
return "? дней";
|
||||
} else if (type == "daysHours") {
|
||||
if (d > 0 && h > 0) return dDisplay + ", " + hDisplay;
|
||||
if (h > 0) return hDisplay;
|
||||
if (m > 0) return mDisplay;
|
||||
} else {
|
||||
return `${d > 0 ? dDisplay : ""}${h > 0 ? ", " + hDisplay : ""}${m > 0 ? ", " + mDisplay : ""}`;
|
||||
}
|
||||
let days = Math.floor(diffInMinutes / 1440);
|
||||
if (days < 0) days = 0;
|
||||
const daysToMinutes = days * 1440;
|
||||
|
||||
let hours = Math.floor((diffInMinutes - daysToMinutes) / 60);
|
||||
if (hours < 0) hours = 0;
|
||||
const hoursToMinutes = hours * 60;
|
||||
|
||||
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> = {
|
||||
last: null,
|
||||
finished: 1,
|
||||
ongoing: 2,
|
||||
announce: 3,
|
||||
export const FilterCountry = ["Япония", "Китай", "Южная Корея"];
|
||||
export const FilterCategoryIdToString: Record<number, string> = {
|
||||
1: "Сериал",
|
||||
2: "Полнометражный фильм",
|
||||
3: "OVA",
|
||||
4: "Дорама",
|
||||
};
|
||||
|
||||
export async function _FetchHomePageReleases(
|
||||
status: string,
|
||||
token: string | null,
|
||||
page: string | number = 0
|
||||
) {
|
||||
let statusId: null | number = null;
|
||||
let categoryId: null | number = null;
|
||||
if (status == "films") {
|
||||
categoryId = 2;
|
||||
} else {
|
||||
statusId = StatusList[status];
|
||||
}
|
||||
|
||||
const body = {
|
||||
country: null,
|
||||
season: null,
|
||||
sort: 0,
|
||||
studio: null,
|
||||
age_ratings: [],
|
||||
category_id: categoryId,
|
||||
end_year: null,
|
||||
episode_duration_from: null,
|
||||
episode_duration_to: null,
|
||||
export const FilterGenre = {
|
||||
uncategorized: {
|
||||
name: "Нет категории",
|
||||
genres: [
|
||||
"авангард",
|
||||
"гурман",
|
||||
"драма",
|
||||
"комедия",
|
||||
"повседневность",
|
||||
"приключения",
|
||||
"романтика",
|
||||
"сверхъестественное",
|
||||
"спорт",
|
||||
"тайна",
|
||||
"триллер",
|
||||
"ужасы",
|
||||
"фантастика",
|
||||
"фэнтези",
|
||||
"экшен",
|
||||
"эротика",
|
||||
"этти",
|
||||
],
|
||||
},
|
||||
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_to: null,
|
||||
genres: [],
|
||||
profile_list_exclusions: [],
|
||||
start_year: null,
|
||||
status_id: statusId,
|
||||
types: [],
|
||||
is_genres_exclude_mode_enabled: false,
|
||||
};
|
||||
},
|
||||
{
|
||||
name: "От 1 до 12",
|
||||
episodes_from: 1,
|
||||
episodes_to: 12,
|
||||
},
|
||||
{
|
||||
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;
|
||||
url = `${ENDPOINTS.filter}/${page}`;
|
||||
if (token) {
|
||||
url += `?token=${token}`;
|
||||
}
|
||||
|
||||
const data: Object = fetch(url, {
|
||||
method: "POST",
|
||||
headers: HEADERS,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Error fetching data");
|
||||
}
|
||||
const { data, error } = await fetchDataViaPost(
|
||||
url,
|
||||
JSON.stringify({
|
||||
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,
|
||||
})
|
||||
.then((data: Object) => {
|
||||
return data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
return data;
|
||||
);
|
||||
|
||||
return [data, error];
|
||||
}
|
||||
|
||||
export const BookmarksList = {
|
||||
|
|
|
@ -6,6 +6,12 @@
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (min-width: 1024px) {
|
||||
.swiper {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.section:hover .swiper-button {
|
||||
display: flex !important;
|
||||
|
|
|
@ -55,7 +55,7 @@ export const CollectionCourusel = (props: {
|
|||
)}
|
||||
</div>
|
||||
<div className="m-4">
|
||||
<div className="swiper">
|
||||
<div className={`swiper ${Styles.swiper}`}>
|
||||
<div className="swiper-wrapper">
|
||||
{props.isMyCollections && (
|
||||
<div className="swiper-slide" style={{ width: "fit-content" }}>
|
||||
|
|
|
@ -5,60 +5,43 @@ import Image from "next/image";
|
|||
export const CollectionLink = (props: any) => {
|
||||
return (
|
||||
<Link href={`/collection/${props.id}`}>
|
||||
<div className="w-full aspect-video group">
|
||||
<div
|
||||
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%] "
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%)`,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={props.image}
|
||||
fill={true}
|
||||
alt={props.title || ""}
|
||||
className="-z-[1] object-cover"
|
||||
sizes="
|
||||
(max-width: 768px) 300px,
|
||||
(max-width: 1024px) 600px,
|
||||
900px
|
||||
"
|
||||
<div className="relative w-full overflow-hidden rounded-lg group aspect-video">
|
||||
<Image
|
||||
src={props.image}
|
||||
fill={true}
|
||||
alt={""}
|
||||
className="-z-[1] object-cover inset-0 absolute w-full h-full group-hover:scale-110 transition-all duration-300 ease-in-out"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black to-transparent"></div>
|
||||
<div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-2 top-2">
|
||||
<Chip
|
||||
icon_name="material-symbols--favorite"
|
||||
name_2={props.favorites_count}
|
||||
/>
|
||||
<div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-2 top-2">
|
||||
{props.comment_count && (
|
||||
<Chip
|
||||
icon_name="material-symbols--favorite"
|
||||
name_2={props.favorites_count}
|
||||
icon_name="material-symbols--comment"
|
||||
name_2={props.comment_count}
|
||||
/>
|
||||
{props.comment_count && (
|
||||
<Chip
|
||||
icon_name="material-symbols--comment"
|
||||
name_2={props.comment_count}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
{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.description && (
|
||||
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
|
||||
{`${props.description.slice(0, 125)}${
|
||||
props.description.length > 125 ? "..." : ""
|
||||
}`}
|
||||
</p>
|
||||
)}
|
||||
</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 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>
|
||||
</Link>
|
||||
|
|
|
@ -16,7 +16,7 @@ export const CollectionsSection = (props: {
|
|||
</div>
|
||||
)}
|
||||
<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.content.map((collection) => {
|
||||
return (
|
||||
|
@ -25,7 +25,6 @@ export const CollectionsSection = (props: {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{props.content.length == 1 && !props.isMyCollections && <div></div>}
|
||||
</div>
|
||||
</div>
|
||||
</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 { useEffect } from "react";
|
||||
import { minutesToTime } from "#/api/utils";
|
||||
import { ReleaseInfoSearchLink } from "../ReleaseInfo/ReleaseInfo.SearchLink";
|
||||
|
||||
type preferredItem = {
|
||||
name: string;
|
||||
percentage: number;
|
||||
};
|
||||
|
||||
export const ProfileStats = (props: {
|
||||
lists: Array<number>;
|
||||
watched_count: number;
|
||||
watched_time: number;
|
||||
profile_id: number
|
||||
profile_id: number;
|
||||
preferred_genres: Array<preferredItem>;
|
||||
preferred_audiences: Array<preferredItem>;
|
||||
preferred_themes: Array<preferredItem>;
|
||||
}) => {
|
||||
const getChartOptions = () => {
|
||||
return {
|
||||
|
@ -81,41 +90,95 @@ export const ProfileStats = (props: {
|
|||
</div>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#66bb6c]"></span>{" "}
|
||||
Смотрю <span className="font-bold">{props.lists[0]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#b566bb]"></span>{" "}
|
||||
В планах <span className="font-bold">{props.lists[1]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#5c6cc0]"></span>{" "}
|
||||
Просмотрено <span className="font-bold">{props.lists[2]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ffca28]"></span>{" "}
|
||||
Отложено <span className="font-bold">{props.lists[3]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ef5450]"></span>{" "}
|
||||
Брошено <span className="font-bold">{props.lists[4]}</span>
|
||||
</p>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-y-2 gap-x-4">
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#66bb6c]"></span>{" "}
|
||||
Смотрю <span className="font-bold">{props.lists[0]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#b566bb]"></span>{" "}
|
||||
В планах <span className="font-bold">{props.lists[1]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#5c6cc0]"></span>{" "}
|
||||
Просмотрено <span className="font-bold">{props.lists[2]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ffca28]"></span>{" "}
|
||||
Отложено <span className="font-bold">{props.lists[3]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ef5450]"></span>{" "}
|
||||
Брошено <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 id="donut-chart"></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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ export const RelatedSection = (props: any) => {
|
|||
<div className="flex items-center justify-center p-4">
|
||||
{props.images.map((item, index) => {
|
||||
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
|
||||
fill={true}
|
||||
src={item}
|
||||
|
|
|
@ -40,12 +40,6 @@ export const ReleaseCourusel = (props: {
|
|||
prevEl: ".swiper-button-prev"
|
||||
}}
|
||||
allowTouchMove={true}
|
||||
breakpoints={{
|
||||
1800: {
|
||||
initialSlide: 2,
|
||||
centeredSlides: true
|
||||
}
|
||||
}}
|
||||
className={Styles.swiper}
|
||||
>
|
||||
{props.content.map((release) => {
|
||||
|
|
|
@ -59,7 +59,7 @@ export const ReleaseInfoInfo = (props: {
|
|||
{"/"}
|
||||
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
|
||||
{props.duration != 0 &&
|
||||
`по ${minutesToTime(props.duration, "daysHours")}`}
|
||||
`по ${minutesToTime(props.duration)}`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
import Link from "next/link";
|
||||
|
||||
// const searchBy = {
|
||||
// title: 0,
|
||||
// studio: 1,
|
||||
// director: 2,
|
||||
// author: 3,
|
||||
// genre: 4
|
||||
// }
|
||||
|
||||
// TODO: сделать какую-нибудь анимацию на ссылке при наведении и фокусе
|
||||
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string | number | null }) => {
|
||||
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string }) => {
|
||||
return (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/search?q=${props.title}&searchBy=${props.searchBy}`}
|
||||
className="text-gray-700 transition-colors duration-300 hover:text-black dark:text-gray-300 hover:dark:text-white"
|
||||
href={`/search?query=${props.title}¶ms={"where"%3A"releases"%2C"searchBy"%3A"${props.searchBy}"}`}
|
||||
>
|
||||
{props.title}
|
||||
</Link>
|
||||
|
|
|
@ -83,7 +83,7 @@ export const PosterWithStuff = (props: {
|
|||
return (
|
||||
<span
|
||||
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 && ", "}
|
||||
{genre}
|
||||
|
@ -91,18 +91,18 @@ export const PosterWithStuff = (props: {
|
|||
);
|
||||
})}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
@ -14,7 +14,7 @@ export const ReleaseSection = (props: {
|
|||
</div>
|
||||
)}
|
||||
<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) => {
|
||||
return (
|
||||
<div key={release.id} className="w-full h-full">
|
||||
|
@ -23,8 +23,8 @@ export const ReleaseSection = (props: {
|
|||
chipsSettings={{
|
||||
enabled: true,
|
||||
lastWatchedHidden:
|
||||
(props.sectionTitle &&
|
||||
props.sectionTitle.toLowerCase() != "история")
|
||||
props.sectionTitle &&
|
||||
props.sectionTitle.toLowerCase() != "история",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -35,17 +35,11 @@ const BookmarksCategory = {
|
|||
abandoned: "Заброшено",
|
||||
};
|
||||
|
||||
const NavbarTitles = {
|
||||
always: "Всегда",
|
||||
links: "Только ссылки",
|
||||
selected: "Только выбранные",
|
||||
never: "Никогда",
|
||||
};
|
||||
|
||||
const FifthButton = {
|
||||
3: "Избранное",
|
||||
4: "Коллекции",
|
||||
5: "История",
|
||||
favorites: "Избранное",
|
||||
collections: "Коллекции",
|
||||
history: "История",
|
||||
discovery: "Обзор",
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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) {
|
||||
setIsPlayerConfigured(true);
|
||||
}
|
||||
|
@ -176,35 +171,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
</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 ?
|
||||
<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>
|
||||
|
@ -230,9 +198,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
<DropdownItem
|
||||
key={`navbar-fifthbutton-${key}`}
|
||||
onClick={() =>
|
||||
preferenceStore.setFlags({
|
||||
showFifthButton: Number(key) as 3 | 4 | 5,
|
||||
})
|
||||
preferenceStore.setFlags({ showFifthButton: key })
|
||||
}
|
||||
>
|
||||
{FifthButton[key]}
|
||||
|
@ -262,7 +228,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
|
@ -285,7 +251,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -12,20 +12,19 @@ export const UserSection = (props: { sectionTitle?: string; content: any }) => {
|
|||
</div>
|
||||
)}
|
||||
<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) => {
|
||||
return (
|
||||
<Link href={`/profile/${user.id}`} key={user.id} className="w-full max-w-[234px] h-full max-h-[234px] aspect-square flex-shrink-0">
|
||||
<Card className="items-center justify-center w-full h-full">
|
||||
<Avatar img={user.avatar} alt={user.login || ""} size="lg" rounded={true} />
|
||||
<h5 className="mb-1 text-xl font-medium text-gray-900 dark:text-white">
|
||||
{user.login}
|
||||
</h5>
|
||||
<Link href={`/profile/${user.id}`} key={user.id}>
|
||||
<Card>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar img={user.avatar} alt="" size="lg" rounded={true} className="flex-shrink-0"/>
|
||||
<p className="text-xl font-medium text-gray-900 dark:text-white line-clamp-1">{user.login}</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{props.content.length == 1 && <div></div>}
|
||||
</div>
|
||||
</div>
|
||||
</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 { ThemeModeScript } from "flowbite-react";
|
||||
import { PublicEnvScript } from 'next-runtime-env';
|
||||
import { ThemeInit } from "../.flowbite-react/init";
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL("https://anix.wah.su"),
|
||||
|
@ -35,6 +36,7 @@ export default function RootLayout({ children }) {
|
|||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<PublicEnvScript />
|
||||
<ThemeInit />
|
||||
<ThemeModeScript />
|
||||
</head>
|
||||
<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 />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
@ -105,7 +105,9 @@ export function BookmarksCategoryPage(props: any) {
|
|||
className="flex-1 max-w-full mx-4"
|
||||
onSubmit={(e) => {
|
||||
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
|
||||
|
|
|
@ -54,7 +54,9 @@ export function CollectionsPage() {
|
|||
className="flex-1 max-w-full mx-4 mb-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/search?q=${searchVal}&where=collections`);
|
||||
router.push(
|
||||
`/search?query=${searchVal}¶ms={"where"%3A"collections_fav"%2C"searchBy"%3A"none"}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/search?q=${searchVal}&where=favorites`);
|
||||
router.push(
|
||||
`/search?query=${searchVal}¶ms={"where"%3A"favorites"%2C"searchBy"%3A"none"}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<label
|
||||
|
@ -129,9 +131,9 @@ export function FavoritesPage() {
|
|||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
||||
<span
|
||||
className={`w-6 h-6 iconify ${
|
||||
sort.values[index].value.split("_")[1] == "descending"
|
||||
? sort.descendingIcon
|
||||
: sort.ascendingIcon
|
||||
sort.values[index].value.split("_")[1] == "descending" ?
|
||||
sort.descendingIcon
|
||||
: sort.ascendingIcon
|
||||
}`}
|
||||
></span>
|
||||
{item.name}
|
||||
|
@ -139,18 +141,17 @@ export function FavoritesPage() {
|
|||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
{content && content.length > 0 ? (
|
||||
{content && content.length > 0 ?
|
||||
<ReleaseSection content={content} />
|
||||
) : isLoading ? (
|
||||
: isLoading ?
|
||||
<div className="flex flex-col items-center justify-center min-w-full min-h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
||||
<p>В избранном пока ничего нет...</p>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
{data &&
|
||||
data[data.length - 1].current_page <
|
||||
data[data.length - 1].total_page_count && (
|
||||
|
|
|
@ -10,7 +10,6 @@ import { Button } from "flowbite-react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { useSWRfetcher } from "#/api/utils";
|
||||
|
||||
|
||||
export function HistoryPage() {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const authState = useUserStore((state) => state.state);
|
||||
|
@ -62,7 +61,9 @@ export function HistoryPage() {
|
|||
className="flex-1 max-w-full mx-4 mb-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/search?q=${searchVal}&where=history`);
|
||||
router.push(
|
||||
`/search?query=${searchVal}¶ms={"where"%3A"history"%2C"searchBy"%3A"none"}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<label
|
||||
|
@ -106,7 +107,7 @@ export function HistoryPage() {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{content && content.length > 0 ? (
|
||||
{content && content.length > 0 ?
|
||||
<>
|
||||
<ReleaseSection sectionTitle="История" content={content} />
|
||||
{data && data[0].total_count != content.length && (
|
||||
|
@ -122,16 +123,15 @@ export function HistoryPage() {
|
|||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : isLoading ? (
|
||||
: isLoading ?
|
||||
<div className="flex flex-col items-center justify-center min-w-full min-h-[100dvh]">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
: <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
|
||||
<span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
|
||||
<p>В истории пока ничего нет...</p>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,15 +3,22 @@ import { ReleaseCourusel } from "#/components/ReleaseCourusel/ReleaseCourusel";
|
|||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { useState, useEffect } from "react";
|
||||
import { _FetchHomePageReleases } from "#/api/utils";
|
||||
import { FetchFilter } from "#/api/utils";
|
||||
|
||||
import { usePreferencesStore } from "#/store/preferences";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ListAnnounce,
|
||||
ListFilms,
|
||||
ListFinished,
|
||||
ListLast,
|
||||
ListOngoing,
|
||||
} from "./IndexFilters";
|
||||
|
||||
export function IndexPage() {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const preferenceStore = usePreferencesStore();
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [lastReleasesData, setLastReleasesData] = useState(null);
|
||||
const [ongoingReleasesData, setOngoingReleasesData] = useState(null);
|
||||
|
@ -21,7 +28,9 @@ export function IndexPage() {
|
|||
|
||||
useEffect(() => {
|
||||
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
|
||||
}, []);
|
||||
|
@ -35,11 +44,19 @@ export function IndexPage() {
|
|||
setAnnounceReleasesData(null);
|
||||
setFilmsReleasesData(null);
|
||||
|
||||
const lastReleases = await _FetchHomePageReleases("last", token);
|
||||
const ongoingReleases = await _FetchHomePageReleases("ongoing", token);
|
||||
const finishedReleases = await _FetchHomePageReleases("finished", token);
|
||||
const announceReleases = await _FetchHomePageReleases("announce", token);
|
||||
const filmsReleases = await _FetchHomePageReleases("films", token);
|
||||
const [lastReleases] = await FetchFilter(ListLast.filter, 0, token);
|
||||
const [ongoingReleases] = await FetchFilter(ListOngoing.filter, 0, token);
|
||||
const [announceReleases] = await FetchFilter(
|
||||
ListAnnounce.filter,
|
||||
0,
|
||||
token
|
||||
);
|
||||
const [finishedReleases] = await FetchFilter(
|
||||
ListFinished.filter,
|
||||
0,
|
||||
token
|
||||
);
|
||||
const [filmsReleases] = await FetchFilter(ListFilms.filter, 0, token);
|
||||
|
||||
setLastReleasesData(lastReleases);
|
||||
setOngoingReleasesData(ongoingReleases);
|
||||
|
@ -56,16 +73,12 @@ export function IndexPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{lastReleasesData ? (
|
||||
{lastReleasesData && (
|
||||
<ReleaseCourusel
|
||||
sectionTitle="Последние релизы"
|
||||
showAllLink="/home/last"
|
||||
content={lastReleasesData.content}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{finishedReleasesData && (
|
||||
<ReleaseCourusel
|
||||
|
@ -95,15 +108,11 @@ export function IndexPage() {
|
|||
content={filmsReleasesData.content}
|
||||
/>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!lastReleasesData &&
|
||||
!finishedReleasesData &&
|
||||
!ongoingReleasesData &&
|
||||
!announceReleasesData && (
|
||||
<div className="flex items-center justify-center min-w-full min-h-screen">
|
||||
<h1 className="text-2xl">Ошибка загрузки контента...</h1>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center h-32 min-w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ import { Spinner } from "#/components/Spinner/Spinner";
|
|||
import { useState, useEffect } from "react";
|
||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||
import { useUserStore } from "../store/auth";
|
||||
import { _FetchHomePageReleases } from "#/api/utils";
|
||||
import { FetchFilter } from "#/api/utils";
|
||||
import { Button, ButtonGroup } from "flowbite-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { slugToFilter } from "./IndexFilters";
|
||||
|
||||
export function IndexCategoryPage(props) {
|
||||
const token = useUserStore((state) => state.token);
|
||||
|
@ -20,7 +21,7 @@ export function IndexCategoryPage(props) {
|
|||
setIsLoading(true);
|
||||
setContent(null);
|
||||
|
||||
const data: any = await _FetchHomePageReleases(props.slug, token, page);
|
||||
const [ data ] = await FetchFilter(slugToFilter[props.slug].filter, page, token);
|
||||
|
||||
setContent(data.content);
|
||||
setIsLoading(false);
|
||||
|
@ -32,7 +33,7 @@ export function IndexCategoryPage(props) {
|
|||
|
||||
useEffect(() => {
|
||||
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];
|
||||
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_time={user.watched_time}
|
||||
profile_id={user.id}
|
||||
preferred_genres={user.preferred_genres || []}
|
||||
preferred_audiences={user.preferred_audiences || []}
|
||||
preferred_themes={user.preferred_themes || []}
|
||||
/>
|
||||
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
||||
<div className="flex flex-col gap-2 lg:hidden">
|
||||
|
|
|
@ -243,7 +243,7 @@ export function SearchPage() {
|
|||
return [url, JSON.stringify({ query, searchBy })];
|
||||
};
|
||||
|
||||
const { data, error, isLoading, size, setSize, mutate } = useSWRInfinite(
|
||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
||||
getKey,
|
||||
([url, payload]) => postFetcher(url, payload),
|
||||
{ initialSize: 2 }
|
||||
|
@ -279,7 +279,7 @@ export function SearchPage() {
|
|||
return (
|
||||
<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}
|
||||
>
|
||||
<div className="flex flex-col flex-1 w-full lg:flex-row">
|
||||
|
|
|
@ -9,8 +9,7 @@ interface preferencesState {
|
|||
// saveSearchHistory: boolean;
|
||||
saveWatchHistory?: boolean;
|
||||
showChangelog?: boolean;
|
||||
showNavbarTitles?: "always" | "links" | "selected" | "never";
|
||||
showFifthButton?: null | 3 | 4 | 5;
|
||||
showFifthButton?: null | string;
|
||||
};
|
||||
params: {
|
||||
isFirstLaunch?: boolean;
|
||||
|
@ -39,11 +38,9 @@ export const usePreferencesStore = create<preferencesState>()(
|
|||
(set, get) => ({
|
||||
_hasHydrated: false,
|
||||
flags: {
|
||||
// saveSearchHistory: true,
|
||||
saveWatchHistory: true,
|
||||
showChangelog: true,
|
||||
showNavbarTitles: "always",
|
||||
showFifthButton: null,
|
||||
showFifthButton: "discovery",
|
||||
},
|
||||
params: {
|
||||
isFirstLaunch: true,
|
||||
|
@ -80,6 +77,7 @@ export const usePreferencesStore = create<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} */
|
||||
const NextConfig = {
|
||||
output: "standalone",
|
||||
|
@ -80,6 +81,4 @@ const NextConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const config = withFlowbiteReact(NextConfig);
|
||||
|
||||
module.exports = config;
|
||||
export default withFlowbiteReact(NextConfig);
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -7,12 +7,11 @@
|
|||
"": {
|
||||
"name": "new",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.52.0",
|
||||
"deepmerge-ts": "^7.1.0",
|
||||
"flowbite": "^2.4.1",
|
||||
"flowbite-react": "^0.11.7",
|
||||
"flowbite-react": "^0.12.7",
|
||||
"hls-video-element": "^1.5.0",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"media-chrome": "^4.9.0",
|
||||
|
@ -3348,9 +3347,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/flowbite-react": {
|
||||
"version": "0.11.7",
|
||||
"resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.11.7.tgz",
|
||||
"integrity": "sha512-Z8m+ycHEsXPacSAi8P4yYDeff7LvcHNwbGAnL/+Fpiv+6ZWDEAGY/YPKhUofZsZa837JTYrbcbmgjqQ1bpt51g==",
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/flowbite-react/-/flowbite-react-0.12.7.tgz",
|
||||
"integrity": "sha512-d8GR7mnCfdIl4n5RXxz4dKin6DIEA7Ax9mXDpJhz9gwxaPKUklKJZKtQ+KkdmFNrB65Zy76Pam01yr3LcxlseA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "1.6.9",
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
"dev-with-services": "node ./run-all.dev.js",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "flowbite-react patch"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.52.0",
|
||||
"deepmerge-ts": "^7.1.0",
|
||||
"flowbite": "^2.4.1",
|
||||
"flowbite-react": "^0.11.7",
|
||||
"flowbite-react": "^0.12.7",
|
||||
"hls-video-element": "^1.5.0",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"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}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
".flowbite-react\\class-list.json"
|
||||
".flowbite-react/class-list.json"
|
||||
],
|
||||
plugins: [
|
||||
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands", "solar"]),
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next.config.js"
|
||||
"next.config.mjs"
|
||||
],
|
||||
"exclude": ["node_modules", "player-parser", "api-prox"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue