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.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 (
+ <>
+
+
+ >
+ );
+};
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 (
+ <>
+