diff --git a/.flowbite-react/config.json b/.flowbite-react/config.json index d04ad92..3ebc21b 100644 --- a/.flowbite-react/config.json +++ b/.flowbite-react/config.json @@ -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 } \ No newline at end of file diff --git a/.flowbite-react/init.tsx b/.flowbite-react/init.tsx new file mode 100644 index 0000000..43ac927 --- /dev/null +++ b/.flowbite-react/init.tsx @@ -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 ; +} + +ThemeInit.displayName = "ThemeInit"; \ No newline at end of file diff --git a/api-prox/index.ts b/api-prox/index.ts index 98856c1..2f399f0 100644 --- a/api-prox/index.ts +++ b/api-prox/index.ts @@ -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` diff --git a/app/App.tsx b/app/App.tsx index 8cccbd0..eaf472a 100644 --- a/app/App.tsx +++ b/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) => { - -
+ +
{props.children}
{ theme="colored" transition={Bounce} /> + + ); }; diff --git a/app/api/config.ts b/app/api/config.ts index 316d19e..f406316 100644 --- a/app/api/config.ts +++ b/app/api/config.ts @@ -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`, } }; diff --git a/app/api/utils.ts b/app/api/utils.ts index fabc2b7..a3e6907 100644 --- a/app/api/utils.ts +++ b/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 = { - last: null, - finished: 1, - ongoing: 2, - announce: 3, +export const FilterCountry = ["Япония", "Китай", "Южная Корея"]; +export const FilterCategoryIdToString: Record = { + 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 = { + 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 = { diff --git a/app/components/CollectionCourusel/CollectionCourusel.module.css b/app/components/CollectionCourusel/CollectionCourusel.module.css index fce52de..1f56115 100644 --- a/app/components/CollectionCourusel/CollectionCourusel.module.css +++ b/app/components/CollectionCourusel/CollectionCourusel.module.css @@ -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; diff --git a/app/components/CollectionCourusel/CollectionCourusel.tsx b/app/components/CollectionCourusel/CollectionCourusel.tsx index c3b920e..e53bcd8 100644 --- a/app/components/CollectionCourusel/CollectionCourusel.tsx +++ b/app/components/CollectionCourusel/CollectionCourusel.tsx @@ -55,7 +55,7 @@ export const CollectionCourusel = (props: { )}
-
+
{props.isMyCollections && (
diff --git a/app/components/CollectionLink/CollectionLink.tsx b/app/components/CollectionLink/CollectionLink.tsx index aee2010..a7887d1 100644 --- a/app/components/CollectionLink/CollectionLink.tsx +++ b/app/components/CollectionLink/CollectionLink.tsx @@ -5,60 +5,43 @@ import Image from "next/image"; export const CollectionLink = (props: any) => { return ( -
-
- {props.title + {""} +
+
+ -
+ {props.comment_count && ( - {props.comment_count && ( - - )} - {props.is_private && ( -
- -
- )} - {props.is_favorite && ( -
- -
- )} -
-
-
-

- {props.title} -

+ )} + {props.is_private && ( +
+
- {props.description && ( -

- {`${props.description.slice(0, 125)}${ - props.description.length > 125 ? "..." : "" - }`} -

- )} -
+ )} + {props.is_favorite && ( +
+ +
+ )} +
+
+

+ {props.title} +

+

+ {props.description} +

diff --git a/app/components/CollectionsSection/CollectionsSection.tsx b/app/components/CollectionsSection/CollectionsSection.tsx index c7b0775..6e03911 100644 --- a/app/components/CollectionsSection/CollectionsSection.tsx +++ b/app/components/CollectionsSection/CollectionsSection.tsx @@ -16,7 +16,7 @@ export const CollectionsSection = (props: {
)}
-
+
{props.isMyCollections && } {props.content.map((collection) => { return ( @@ -25,7 +25,6 @@ export const CollectionsSection = (props: {
); })} - {props.content.length == 1 && !props.isMyCollections &&
}
diff --git a/app/components/Discovery/CollectionsOfTheWeek.tsx b/app/components/Discovery/CollectionsOfTheWeek.tsx new file mode 100644 index 0000000..cec280a --- /dev/null +++ b/app/components/Discovery/CollectionsOfTheWeek.tsx @@ -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 ( + + ); +}; diff --git a/app/components/Discovery/DiscussingToday.tsx b/app/components/Discovery/DiscussingToday.tsx new file mode 100644 index 0000000..948273b --- /dev/null +++ b/app/components/Discovery/DiscussingToday.tsx @@ -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 ( +
+
+

+ Обсуждаемое сегодня +

+
+
+ {data.content.map((item) => { + return ( + + + + ); + })} +
+
+ ); +}; diff --git a/app/components/Discovery/InterestingCarousel.module.css b/app/components/Discovery/InterestingCarousel.module.css new file mode 100644 index 0000000..ed205c5 --- /dev/null +++ b/app/components/Discovery/InterestingCarousel.module.css @@ -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; + } +} diff --git a/app/components/Discovery/InterestingCarousel.tsx b/app/components/Discovery/InterestingCarousel.tsx new file mode 100644 index 0000000..fa4e366 --- /dev/null +++ b/app/components/Discovery/InterestingCarousel.tsx @@ -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 ( +
+ + {data.content.map((item) => { + return ( + + +
+ +
+
+

{item.title}

+

{item.description}

+
+
+ +
+ ); + })} +
+
+
+
+ ); +}; diff --git a/app/components/Discovery/Modal/FiltersAgeRatingModal.tsx b/app/components/Discovery/Modal/FiltersAgeRatingModal.tsx new file mode 100644 index 0000000..167a2fe --- /dev/null +++ b/app/components/Discovery/Modal/FiltersAgeRatingModal.tsx @@ -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 ( + setIsOpen(false)} dismissible> + Выберите списки + + {Object.entries(FilterAgeRatingToString).map(([key, value]) => { + return ( +
+ toggleRating(Number(key))} + checked={newAgeRatings.includes(Number(key))} + color="blue" + /> + +
+ ); + })} +
+ + + + + +
+ ); +}; diff --git a/app/components/Discovery/Modal/FiltersGenreModal.tsx b/app/components/Discovery/Modal/FiltersGenreModal.tsx new file mode 100644 index 0000000..7ee4362 --- /dev/null +++ b/app/components/Discovery/Modal/FiltersGenreModal.tsx @@ -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 ( + setIsOpen(false)} dismissible size="6xl"> + Жанры + +
+ {Object.entries(FilterGenre).map(([key, value]) => { + return ( +
+

{value.name}

+ {value.genres.map((genre) => { + return ( +
+ toggleGenre(genre)} + checked={newGenres.includes(genre)} + color="blue" + /> + +
+ ); + })} +
+ ); + })} +
+
+ +
+
+
+

Режим исключения

+

+ Фильтр будет искать релизы не содержащие ни один из указанных + выше жанров +

+
+ setNewExclusionMode(!newExclusionMode)} + checked={newExclusionMode} + /> +
+
+ + + +
+
+
+
+ ); +}; diff --git a/app/components/Discovery/Modal/FiltersListExcludeModal.tsx b/app/components/Discovery/Modal/FiltersListExcludeModal.tsx new file mode 100644 index 0000000..4c72cf4 --- /dev/null +++ b/app/components/Discovery/Modal/FiltersListExcludeModal.tsx @@ -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 ( + setIsOpen(false)} dismissible> + Выберите списки + + {Object.entries(FilterProfileListIdToString).map(([key, value]) => { + return ( +
+ toggleList(Number(key))} + checked={newList.includes(Number(key))} + color="blue" + /> + +
+ ); + })} +
+ + + + + +
+ ); +}; diff --git a/app/components/Discovery/Modal/FiltersModal.tsx b/app/components/Discovery/Modal/FiltersModal.tsx new file mode 100644 index 0000000..f65aa4e --- /dev/null +++ b/app/components/Discovery/Modal/FiltersModal.tsx @@ -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 ( + <> + setIsOpen(false)} + size="4xl" + dismissible + > + Фильтр + +
+
+

Страна

+ + setNewFilter({ ...newFilter, country: null })} + > + Неважно + + {FilterCountry.map((item) => { + return ( + + setNewFilter({ ...newFilter, country: item }) + } + > + {item} + + ); + })} + +
+
+

Категория

+ + + setNewFilter({ ...newFilter, category_id: null }) + } + > + Неважно + + {Object.entries(FilterCategoryIdToString).map( + ([key, value]) => { + return ( + + setNewFilter({ + ...newFilter, + category_id: Number(key), + }) + } + > + {value} + + ); + } + )} + +
+
+

Жанры

+ +

+ Будет искать релизы, содержащие каждый из указанных жанров. + Рекомендуется выбирать не более 3 жанров +

+
+ {userStore.isAuth ? +
+

Исключить закладки

+ +

+ Исключит из выдачи релизы, входящие в указанные закладки +

+
+ : ""} +
+

Варианты озвучек

+ +
+
+

Студия

+ + setNewFilter({ ...newFilter, studio: null })} + > + Неважно + + {FilterStudio.map((value) => { + return ( + + setNewFilter({ + ...newFilter, + studio: value, + }) + } + > + {value} + + ); + })} + +
+
+

Первоисточник

+ + setNewFilter({ ...newFilter, source: null })} + > + Неважно + + {FilterSource.map((value) => { + return ( + + setNewFilter({ + ...newFilter, + source: value, + }) + } + > + {value} + + ); + })} + +
+
+

Года

+
+

С

+ + + setNewFilter({ ...newFilter, start_year: null }) + } + > + Неважно + + {FilterYear.map((value) => { + return ( + + setNewFilter({ + ...newFilter, + start_year: value, + }) + } + > + {value} + + ); + })} + +

По

+ + + setNewFilter({ ...newFilter, end_year: null }) + } + > + Неважно + + {FilterYear.map((value) => { + return ( + + setNewFilter({ + ...newFilter, + end_year: value, + }) + } + > + {value} + + ); + })} + +

Сезон

+ + setNewFilter({ ...newFilter, season: null })} + > + Неважно + + {Object.entries(FilterSeasonIdToString).map( + ([key, value]) => { + return ( + + setNewFilter({ + ...newFilter, + season: Number(key), + }) + } + > + {value} + + ); + } + )} + +
+
+
+
+
+

Эпизодов

+ + 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 ( + + setNewFilter({ + ...newFilter, + episodes_from: value.episodes_from, + episodes_to: value.episodes_to, + }) + } + > + {value.name} + + ); + })} + +
+
+

Длительность эпизода

+ + 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 ( + + setNewFilter({ + ...newFilter, + episode_duration_from: + value.episode_duration_from, + episode_duration_to: value.episode_duration_to, + }) + } + > + {value.name} + + ); + })} + +
+
+

Статус

+ + + setNewFilter({ ...newFilter, status_id: null }) + } + > + Неважно + + {Object.entries(FilterStatusIdToString).map( + ([key, value]) => { + return ( + + setNewFilter({ + ...newFilter, + status_id: Number(key), + }) + } + > + {value} + + ); + } + )} + +
+
+
+
+

Возрастное ограничение

+ +
+
+

Сортировка

+ + {Object.entries(FilterSortToString).map(([key, value]) => { + return ( + + setNewFilter({ + ...newFilter, + sort: Number(key), + }) + } + > + {value} + + ); + })} + +
+
+
+ + + +
+ + + setNewFilter({ ...newFilter, profile_list_exclusions }) + } + /> + setNewFilter({ ...newFilter, types })} + /> + + setNewFilter({ ...newFilter, age_ratings }) + } + /> + + ); +}; diff --git a/app/components/Discovery/Modal/FiltersTypesModal.tsx b/app/components/Discovery/Modal/FiltersTypesModal.tsx new file mode 100644 index 0000000..80b50bb --- /dev/null +++ b/app/components/Discovery/Modal/FiltersTypesModal.tsx @@ -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 ( + setIsOpen(false)} dismissible> + Выберите списки + + {typesData.map((item) => { + return ( +
+ toggleType(Number(item.id))} + checked={newTypes.includes(Number(item.id))} + color="blue" + /> + +
+ ); + })} +
+ + + + + +
+ ); +}; diff --git a/app/components/Discovery/Modal/PopularFilters.ts b/app/components/Discovery/Modal/PopularFilters.ts new file mode 100644 index 0000000..82d0948 --- /dev/null +++ b/app/components/Discovery/Modal/PopularFilters.ts @@ -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 }; diff --git a/app/components/Discovery/Modal/PopularModal.tsx b/app/components/Discovery/Modal/PopularModal.tsx new file mode 100644 index 0000000..36616ab --- /dev/null +++ b/app/components/Discovery/Modal/PopularModal.tsx @@ -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 ( + setIsOpen(false)} + size="7xl" + dismissible + > + Популярное + +
+ + {tabs.map((item) => ( + + ))} + +
+
+ {content ? + content.map((release) => { + return ( +
+ +
+ ); + }) + : } +
+
+
+ ); +}; diff --git a/app/components/Discovery/Modal/ScheduleModal.tsx b/app/components/Discovery/Modal/ScheduleModal.tsx new file mode 100644 index 0000000..1b60d49 --- /dev/null +++ b/app/components/Discovery/Modal/ScheduleModal.tsx @@ -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 ( + setIsOpen(false)} + size="7xl" + dismissible + > + Расписание + + {!error ? + isLoading ? +
+ +
+ :
+ + + + + + + +
+ + : "Ошибка загрузки"} +
+
+ ); +}; diff --git a/app/components/Discovery/RecommendedCarousel.tsx b/app/components/Discovery/RecommendedCarousel.tsx new file mode 100644 index 0000000..5fa2949 --- /dev/null +++ b/app/components/Discovery/RecommendedCarousel.tsx @@ -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 ; +}; diff --git a/app/components/Discovery/WatchingNowCarousel.tsx b/app/components/Discovery/WatchingNowCarousel.tsx new file mode 100644 index 0000000..0bb20a1 --- /dev/null +++ b/app/components/Discovery/WatchingNowCarousel.tsx @@ -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 ; +}; diff --git a/app/components/Navbar/NavBarMobile.tsx b/app/components/Navbar/NavBarMobile.tsx new file mode 100644 index 0000000..6108483 --- /dev/null +++ b/app/components/Navbar/NavBarMobile.tsx @@ -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 ( + <> +
+
+ {NavbarItems.map((item) => { + if (item.auth && !userStore.isAuth) return; + return ( + + + {item.title} + + ); + })} + {preferenceStore.flags.showFifthButton ? + <> + {( + !userStore.isAuth && + FifthButton[preferenceStore.flags.showFifthButton].auth + ) ? + <> + : + + + {FifthButton[preferenceStore.flags.showFifthButton].title} + + + } + + : ""} + + +

+ {userStore.isAuth ? userStore.user.login : "Аноним"} +

+
+ } + > + {userStore.isAuth && ( + <> + router.push(`/profile/${userStore.user.id}`)} + > + + Профиль + + {Object.entries(FifthButton).map(([key, item]) => { + if (item.auth && !userStore.isAuth) return; + if (preferenceStore.flags.showFifthButton === key) return; + return ( + router.push(item.href)} + > + + {item.title} + + ); + })} + + + )} + props.setIsSettingModalOpen(true)} + className="relative flex" + > + + Настройки + + {userStore.isAuth ? + userStore.logout()}> + + Выйти + + : router.push(`/login?redirect=${pathname}`)} + > + + Войти + + } + +
+ + + ); +}; diff --git a/app/components/Navbar/NavBarPc.tsx b/app/components/Navbar/NavBarPc.tsx new file mode 100644 index 0000000..902a6ef --- /dev/null +++ b/app/components/Navbar/NavBarPc.tsx @@ -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 ( + <> +
+
+
+ {NavbarItems.map((item) => { + if (item.auth && !userStore.isAuth) return; + return ( + + + {item.title} + + ); + })} +
+ + +

+ {userStore.isAuth ? userStore.user.login : "Аноним"} +

+
+ } + > + {userStore.isAuth ? + router.push(`/profile/${userStore.user.id}`)} + className="relative flex" + > + + Профиль + + : ""} + props.setIsSettingModalOpen(true)} + className="relative flex" + > + + Настройки + + {userStore.isAuth ? + userStore.logout()} + className="relative flex" + > + + Выйти + + : router.push(`/login?redirect=${pathname}`)} + className="relative flex" + > + + Войти + + } + +
+ + + ); +}; diff --git a/app/components/Navbar/NavbarUpdate.tsx b/app/components/Navbar/NavbarUpdate.tsx deleted file mode 100644 index a6219c0..0000000 --- a/app/components/Navbar/NavbarUpdate.tsx +++ /dev/null @@ -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 ( - <> -
-
-
- {menuItems.map((item) => { - return ( - - - - {item.title} - - - ); - })} -
-
- {!userStore.isAuth ? - - - - Войти - - - : <> - - - - {userStore.user.login} - - - {preferenceStore.flags.showFifthButton ? - - - - {menuItems[preferenceStore.flags.showFifthButton].title} - - - : ""} - - - - {userStore.user.login} - - - - } - - {userStore.isAuth && ( - - )} -
-
-
- - - ); -}; diff --git a/app/components/Profile/Profile.Stats.tsx b/app/components/Profile/Profile.Stats.tsx index 214c1cb..659ea91 100644 --- a/app/components/Profile/Profile.Stats.tsx +++ b/app/components/Profile/Profile.Stats.tsx @@ -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; watched_count: number; watched_time: number; - profile_id: number + profile_id: number; + preferred_genres: Array; + preferred_audiences: Array; + preferred_themes: Array; }) => { const getChartOptions = () => { return { @@ -81,41 +90,95 @@ export const ProfileStats = (props: {
-

- {" "} - Смотрю {props.lists[0]} -

-

- {" "} - В планах {props.lists[1]} -

-

- {" "} - Просмотрено {props.lists[2]} -

-

- {" "} - Отложено {props.lists[3]} -

-

- {" "} - Брошено {props.lists[4]} -

+
+

+ {" "} + Смотрю {props.lists[0]} +

+

+ {" "} + В планах {props.lists[1]} +

+

+ {" "} + Просмотрено {props.lists[2]} +

+

+ {" "} + Отложено {props.lists[3]} +

+

+ {" "} + Брошено {props.lists[4]} +

+
+
+

+ Жанры:{" "} + + {props.preferred_genres.map((item, index) => { + return ( +

+ {index > 0 && ", "} + {" "} + {item.percentage}% +
+ ); + })} + +

+

+ Аудитория:{" "} + + {props.preferred_audiences.map((item, index) => { + return ( +

+ {index > 0 && ", "} + {" "} + {item.percentage}% +
+ ); + })} + +

+

+ Тематика:{" "} + + {props.preferred_themes.map((item, index) => { + return ( +

+ {index > 0 && ", "} + {" "} + {item.percentage}% +
+ ); + })} + +

+ +

+ Просмотрено серий:{" "} + {props.watched_count} +

+

+ Время просмотра:{" "} + + ~{minutesToTime(props.watched_time)} + +

+
-
-

- Просмотрено серий:{" "} - {props.watched_count} -

-

- Время просмотра:{" "} - - ~{minutesToTime(props.watched_time, "daysHours")} - -

-
); }; diff --git a/app/components/RelatedSection/RelatedSection.tsx b/app/components/RelatedSection/RelatedSection.tsx index 368793d..fa640b8 100644 --- a/app/components/RelatedSection/RelatedSection.tsx +++ b/app/components/RelatedSection/RelatedSection.tsx @@ -15,7 +15,7 @@ export const RelatedSection = (props: any) => {
{props.images.map((item, index) => { return ( -
+
{props.content.map((release) => { diff --git a/app/components/ReleaseInfo/ReleaseInfo.Info.tsx b/app/components/ReleaseInfo/ReleaseInfo.Info.tsx index 631ca88..0ec3cd3 100644 --- a/app/components/ReleaseInfo/ReleaseInfo.Info.tsx +++ b/app/components/ReleaseInfo/ReleaseInfo.Info.tsx @@ -59,7 +59,7 @@ export const ReleaseInfoInfo = (props: { {"/"} {props.episodes.total ? props.episodes.total + " эп. " : "? эп. "} {props.duration != 0 && - `по ${minutesToTime(props.duration, "daysHours")}`} + `по ${minutesToTime(props.duration)}`} diff --git a/app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx b/app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx index db19124..f7524a8 100644 --- a/app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx +++ b/app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx @@ -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 ( {props.title} diff --git a/app/components/ReleasePoster/PosterWithStuff.tsx b/app/components/ReleasePoster/PosterWithStuff.tsx index 7ffbee2..c4049ac 100644 --- a/app/components/ReleasePoster/PosterWithStuff.tsx +++ b/app/components/ReleasePoster/PosterWithStuff.tsx @@ -83,7 +83,7 @@ export const PosterWithStuff = (props: { return ( {index > 0 && ", "} {genre} @@ -91,18 +91,18 @@ export const PosterWithStuff = (props: { ); })} {props.title_ru && ( -

+

{props.title_ru}

)} {props.title_original && ( -

+

{props.title_original}

)}
{settings.showDescription && props.description && ( -

+

{props.description}

)} diff --git a/app/components/ReleaseSection/ReleaseSection.tsx b/app/components/ReleaseSection/ReleaseSection.tsx index b826085..95c4763 100644 --- a/app/components/ReleaseSection/ReleaseSection.tsx +++ b/app/components/ReleaseSection/ReleaseSection.tsx @@ -14,7 +14,7 @@ export const ReleaseSection = (props: {
)}
-
+
{props.content.map((release) => { return (
@@ -23,8 +23,8 @@ export const ReleaseSection = (props: { chipsSettings={{ enabled: true, lastWatchedHidden: - (props.sectionTitle && - props.sectionTitle.toLowerCase() != "история") + props.sectionTitle && + props.sectionTitle.toLowerCase() != "история", }} />
diff --git a/app/components/SettingsModal/SettingsModal.tsx b/app/components/SettingsModal/SettingsModal.tsx index 56d9291..54a26a8 100644 --- a/app/components/SettingsModal/SettingsModal.tsx +++ b/app/components/SettingsModal/SettingsModal.tsx @@ -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 }) => {
: ""} -
-

- Показывать название пункта в навигации -

- - {Object.keys(NavbarTitles).map( - (key: "always" | "links" | "selected" | "never") => { - return ( - - preferenceStore.setFlags({ - showNavbarTitles: key, - }) - } - > - {NavbarTitles[key]} - - ); - } - )} - -
{userStore.isAuth ? -
+

Пятый пункт в навигации

@@ -230,9 +198,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => { - 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 }) => {

Сохранять историю просмотра

-

+

При отключении, история не будет сохранятся как локально, так и на аккаунте

@@ -285,7 +251,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {

Новый плеер

-

+

Поддерживаемые источники: Kodik, Sibnet, Libria

diff --git a/app/components/UserSection/UserSection.tsx b/app/components/UserSection/UserSection.tsx index 4dc7bfe..ece64fc 100644 --- a/app/components/UserSection/UserSection.tsx +++ b/app/components/UserSection/UserSection.tsx @@ -12,20 +12,19 @@ export const UserSection = (props: { sectionTitle?: string; content: any }) => {
)}
-
+
{props.content.map((user) => { return ( - - - -
- {user.login} -
+ + +
+ +

{user.login}

+
); })} - {props.content.length == 1 &&
}
diff --git a/app/discovery/collections/page.tsx b/app/discovery/collections/page.tsx new file mode 100644 index 0000000..ff026f3 --- /dev/null +++ b/app/discovery/collections/page.tsx @@ -0,0 +1,12 @@ +import { DiscoverCollectionsPage } from "#/pages/DiscoverCollections"; + +export const metadata = { + title: "Обзор - Коллекции", + description: "", +}; + +export const dynamic = "force-static"; + +export default function Discover() { + return ; +} diff --git a/app/discovery/filter/page.tsx b/app/discovery/filter/page.tsx new file mode 100644 index 0000000..a7144c4 --- /dev/null +++ b/app/discovery/filter/page.tsx @@ -0,0 +1,12 @@ +import { DiscoverFilterPage } from "#/pages/DiscoverFilter"; + +export const metadata = { + title: "Фильтр", + description: "Поиск по фильтру", +}; + +export const dynamic = "force-static"; + +export default function Discover() { + return ; +} diff --git a/app/discovery/page.tsx b/app/discovery/page.tsx new file mode 100644 index 0000000..448f517 --- /dev/null +++ b/app/discovery/page.tsx @@ -0,0 +1,12 @@ +import { DiscoverPage } from "#/pages/Discover"; + +export const metadata = { + title: "Обзор", + description: "Рекомендации и популярное", +}; + +export const dynamic = "force-static"; + +export default function Discover() { + return ; +} diff --git a/app/discovery/recommendations/page.tsx b/app/discovery/recommendations/page.tsx new file mode 100644 index 0000000..68ef980 --- /dev/null +++ b/app/discovery/recommendations/page.tsx @@ -0,0 +1,12 @@ +import { DiscoverRecommendationsPage } from "#/pages/DiscoverRecommendations"; + +export const metadata = { + title: "Обзор - Рекомендации", + description: "", +}; + +export const dynamic = "force-static"; + +export default function Discover() { + return ; +} diff --git a/app/discovery/watching/page.tsx b/app/discovery/watching/page.tsx new file mode 100644 index 0000000..799d3f0 --- /dev/null +++ b/app/discovery/watching/page.tsx @@ -0,0 +1,12 @@ +import { DiscoverWatchingPage } from "#/pages/DiscoverWatching"; + +export const metadata = { + title: "Обзор - Смотрят сейчас", + description: "Релизы которые сейчас смотрят", +}; + +export const dynamic = "force-static"; + +export default function Discover() { + return ; +} diff --git a/app/layout.tsx b/app/layout.tsx index 0c0415f..99240ee 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 }) { + {children} diff --git a/app/menu/page.tsx b/app/menu/page.tsx deleted file mode 100644 index 61305cc..0000000 --- a/app/menu/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export const metadata = { - title: "Меню", -}; - -import { MenuPage } from "#/pages/MobileMenuPage"; - -export const dynamic = "force-static"; - -export default function Index() { - return ; -} diff --git a/app/pages/BookmarksCategory.tsx b/app/pages/BookmarksCategory.tsx index b53b3fa..3590e50 100644 --- a/app/pages/BookmarksCategory.tsx +++ b/app/pages/BookmarksCategory.tsx @@ -82,7 +82,7 @@ export function BookmarksCategoryPage(props: any) {
); - }; + } 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}"}` + ); }} >
- {content && content.length > 0 ? ( + {content && content.length > 0 ? - ) : isLoading ? ( + : isLoading ?
- ) : ( -
+ :

В избранном пока ничего нет...

- )} + } {data && data[data.length - 1].current_page < data[data.length - 1].total_page_count && ( diff --git a/app/pages/History.tsx b/app/pages/History.tsx index 73c9f10..7b4c495 100644 --- a/app/pages/History.tsx +++ b/app/pages/History.tsx @@ -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"}` + ); }} >
- {content && content.length > 0 ? ( + {content && content.length > 0 ? <> {data && data[0].total_count != content.length && ( @@ -122,16 +123,15 @@ export function HistoryPage() { )} - ) : isLoading ? ( + : isLoading ?
- ) : ( -
+ :

В истории пока ничего нет...

- )} + } ); } diff --git a/app/pages/Index.tsx b/app/pages/Index.tsx index c40c057..ef6eef8 100644 --- a/app/pages/Index.tsx +++ b/app/pages/Index.tsx @@ -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 && ( - ) : ( -
- -
)} {finishedReleasesData && ( )} - {!isLoading && - !lastReleasesData && - !finishedReleasesData && - !ongoingReleasesData && - !announceReleasesData && ( -
-

Ошибка загрузки контента...

-
- )} + {isLoading && ( +
+ +
+ )} ); } diff --git a/app/pages/IndexCategory.tsx b/app/pages/IndexCategory.tsx index f3ba11d..bf10bed 100644 --- a/app/pages/IndexCategory.tsx +++ b/app/pages/IndexCategory.tsx @@ -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); } diff --git a/app/pages/IndexFilters.ts b/app/pages/IndexFilters.ts new file mode 100644 index 0000000..0432d99 --- /dev/null +++ b/app/pages/IndexFilters.ts @@ -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, +} \ No newline at end of file diff --git a/app/pages/MobileMenuPage.tsx b/app/pages/MobileMenuPage.tsx deleted file mode 100644 index f1f73b8..0000000 --- a/app/pages/MobileMenuPage.tsx +++ /dev/null @@ -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 && ( -
-
- - -
- -
-

- {userStore.user.login} -

-

- {userStore.user.status} -

-
-
-
- -
- - -
-
- {preferenceStore.flags.showFifthButton != 3 ? - - -
- -

Избранное

-
-
- - : ""} - {preferenceStore.flags.showFifthButton != 4 ? - - -
- -

Коллекции

-
-
- - : ""} - {preferenceStore.flags.showFifthButton != 5 ? - - -
- -

История

-
-
- - : ""} - -
- )} - - ); -}; diff --git a/app/pages/Profile.tsx b/app/pages/Profile.tsx index fdd44b4..6eab266 100644 --- a/app/pages/Profile.tsx +++ b/app/pages/Profile.tsx @@ -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 || []} />
diff --git a/app/pages/Search.tsx b/app/pages/Search.tsx index c5ee065..0f58262 100644 --- a/app/pages/Search.tsx +++ b/app/pages/Search.tsx @@ -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 (
diff --git a/app/store/preferences.ts b/app/store/preferences.ts index 1225c2d..056038b 100644 --- a/app/store/preferences.ts +++ b/app/store/preferences.ts @@ -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()( (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()( persistedState as preferencesState ); }, - } + version: 2, + }, ) ); diff --git a/next.config.js b/next.config.mjs similarity index 93% rename from next.config.js rename to next.config.mjs index 24aa345..08b5070 100644 --- a/next.config.js +++ b/next.config.mjs @@ -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); diff --git a/package-lock.json b/package-lock.json index 6309d00..cfb8bd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 10c8582..25d6ab0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/changelog/3.9.0.md b/public/changelog/3.9.0.md new file mode 100644 index 0000000..c782610 --- /dev/null +++ b/public/changelog/3.9.0.md @@ -0,0 +1,21 @@ +# 3.9.0 + +## Добавлено + +- Статистика тематика, жанры и аудитория в статистике профиля +- Страница "Обзор" + +## Изменено + +- Секции карточек релизов теперь в 2 колонки на телефонах +- Вид карточек в поиске пользователей +- По стандарту пятой кнопкой в мобильном навбаре стоит пункт "обзор" + +## Исправлено + +- Неправильное время просмотра в статистике профиле в некоторых случаях +- Ссылки на переход в поиск с страницы релиза, закладок, избранных, истории, коллекциях теперь работают для нового поиска + +## Удалено + +- Настройки показа названий страниц в навигации diff --git a/tailwind.config.js b/tailwind.config.js index d5a11ca..83bda1a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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"]), diff --git a/tsconfig.json b/tsconfig.json index b0d0bcc..f7fcd08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", - "next.config.js" + "next.config.mjs" ], "exclude": ["node_modules", "player-parser", "api-prox"] }