anix/feat: add filters modal with country, category, genre and lists exclusion filters

This commit is contained in:
Kentai Radiquum 2025-08-28 04:05:40 +05:00
parent d3b198c6bc
commit 777fb5b82b
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
9 changed files with 396 additions and 18 deletions

View file

@ -288,13 +288,6 @@ export function minutesToTime(min: number) {
if (minutes > 0) return minuteDisplay;
}
const StatusList: Record<string, null | number> = {
last: null,
finished: 1,
ongoing: 2,
announce: 3,
};
export const FilterCountry = ["Япония", "Китай", "Южная Корея"];
export const FilterCategoryIdToString: Record<number, string> = {
1: "Сериал",

View file

@ -0,0 +1,124 @@
"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);
function toggleGenre(string: string) {
if (newGenres.includes(string)) {
setNewGenres(newGenres.filter((genre) => genre != string));
} else {
setNewGenres([...newGenres, string]);
}
}
useEffect(() => {
setNewGenres(genres);
setNewExclusionMode(exclusionMode);
}, [genres, exclusionMode]);
return (
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible>
<ModalHeader>Жанры</ModalHeader>
<ModalBody>
<div>
{Object.entries(FilterGenre).map(([key, value]) => {
return (
<div key={`filter-genre-category-${value.name}`} className="mb-4">
<p className="mb-2">{value.name}</p>
{value.genres.map((genre) => {
return (
<div
className="flex items-center gap-2"
key={`filter-genre-category-${value.name}-${genre}`}
>
<Checkbox
id={`filter-genre-category-${value.name}-genre-${genre}`}
onChange={() => toggleGenre(genre)}
checked={newGenres.includes(genre)}
color="blue"
/>
<Label
htmlFor={`filter-genre-category-${value.name}-genre-${genre}`}
>
{genre}
</Label>
</div>
);
})}
</div>
);
})}
</div>
</ModalBody>
<ModalFooter>
<div className="flex justify-between w-full">
<div className="flex items-center gap-2">
<div>
<p className="mb-1 font-bold">Режим исключения</p>
<p className="text-sm text-gray-400 dark:text-gray-300 max-w-52">
Фильтр будет искать релизы не содержащие ни один из указанных
выше жанров
</p>
</div>
<ToggleSwitch
color="blue"
onChange={() => setNewExclusionMode(!newExclusionMode)}
checked={newExclusionMode}
/>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => {
save([], false);
setNewGenres([]);
setNewExclusionMode(false);
setIsOpen(false);
}}
color="red"
>
Сбросить
</Button>
<Button
onClick={() => {
save(newGenres, newExclusionMode);
setNewGenres([]);
setNewExclusionMode(false);
setIsOpen(false);
}}
color="blue"
>
Применить
</Button>
</div>
</div>
</ModalFooter>
</Modal>
);
};

View file

@ -0,0 +1,87 @@
"use client";
import { FilterProfileListIdToString } from "#/api/utils";
import {
Button,
Checkbox,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from "flowbite-react";
import { useEffect, useState } from "react";
type Props = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
lists: number[];
setLists: (lists: number[]) => void;
};
export const FiltersListExcludeModal = ({
isOpen,
setIsOpen,
lists,
setLists,
}: Props) => {
const [newList, setNewList] = useState(lists);
function toggleList(number: number) {
if (newList.includes(number)) {
setNewList(newList.filter((list) => list != number));
} else {
setNewList([...newList, number]);
}
}
useEffect(() => {
setNewList(lists);
}, [lists]);
return (
<Modal show={isOpen} onClose={() => setIsOpen(false)} dismissible>
<ModalHeader>Выберите списки</ModalHeader>
<ModalBody>
{Object.entries(FilterProfileListIdToString).map(([key, value]) => {
return (
<div
className="flex items-center gap-2"
key={`filter-list-exclude-${value}`}
>
<Checkbox
id={`filter-list-exclude-${value}`}
onChange={() => toggleList(Number(key))}
checked={newList.includes(Number(key))}
color="blue"
/>
<Label htmlFor={`filter-list-exclude-${value}`}>{value}</Label>
</div>
);
})}
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
setLists([]);
setNewList([]);
setIsOpen(false);
}}
color="red"
>
Сбросить
</Button>
<Button
onClick={() => {
setLists(newList);
setNewList([]);
setIsOpen(false);
}}
color="blue"
>
Применить
</Button>
</ModalFooter>
</Modal>
);
}

View file

@ -0,0 +1,172 @@
"use client";
import {
Filter,
FilterCategoryIdToString,
FilterCountry,
FilterDefault,
FilterProfileListIdToString,
} from "#/api/utils";
import {
Button,
Dropdown,
DropdownItem,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "flowbite-react";
import { useState } from "react";
import { FiltersGenreModal } from "./FiltersGenreModal";
import { useUserStore } from "#/store/auth";
import { FiltersListExcludeModal } from "./FiltersListExcludeModal";
type ModalProps = {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
filter?: Filter;
};
export const FiltersModal = ({ isOpen, setIsOpen, filter }: ModalProps) => {
const userStore = useUserStore();
const [newFilter, setNewFilter] = useState(filter || FilterDefault);
const [isGenreModalOpen, setIsGenreModalOpen] = useState(false);
const [isListExcludeModalOpen, setIsListExcludeModalOpen] = useState(false);
function saveGenres(genres, is_genres_exclude_mode_enabled) {
setNewFilter({ ...newFilter, genres, is_genres_exclude_mode_enabled });
}
return (
<>
<Modal
// show={isOpen}
show={true}
onClose={() => setIsOpen(false)}
size="4xl"
dismissible
>
<ModalHeader>Фильтр</ModalHeader>
<ModalBody>
<div className="space-y-4">
<div className="space-y-2">
<p>Страна</p>
<Dropdown
label={newFilter.country || "Неважно"}
color="blue"
className="w-full"
>
<DropdownItem
key={`filter-modal-country-none`}
onClick={() => setNewFilter({ ...newFilter, country: null })}
>
Неважно
</DropdownItem>
{FilterCountry.map((item) => {
return (
<DropdownItem
key={`filter-modal-country-${item}`}
onClick={() =>
setNewFilter({ ...newFilter, country: item })
}
>
{item}
</DropdownItem>
);
})}
</Dropdown>
</div>
<div className="space-y-2">
<p>Категория</p>
<Dropdown
label={
newFilter.category_id ?
FilterCategoryIdToString[newFilter.category_id]
: "Неважно"
}
color="blue"
className="w-full"
>
<DropdownItem
key={`filter-modal-category-none`}
onClick={() =>
setNewFilter({ ...newFilter, category_id: null })
}
>
Неважно
</DropdownItem>
{Object.entries(FilterCategoryIdToString).map(
([key, value]) => {
return (
<DropdownItem
key={`filter-modal-category-${key}`}
onClick={() =>
setNewFilter({
...newFilter,
category_id: Number(key),
})
}
>
{value}
</DropdownItem>
);
}
)}
</Dropdown>
</div>
<div className="space-y-2">
<p>Жанры</p>
<Button
color={"blue"}
className="w-full min-h-10 h-fit"
onClick={() => setIsGenreModalOpen(true)}
>
{newFilter.genres.length > 0 ?
newFilter.genres.join(", ")
: "Неважно"}
</Button>
<p className="text-sm">
Будет искать релизы, содержащие каждый из указанных жанров.
Рекомендуется выбирать не более 3 жанров
</p>
</div>
{userStore.isAuth ? <div className="space-y-2">
<p>Исключить закладки</p>
<Button
color={"blue"}
className="w-full min-h-10 h-fit"
onClick={() => setIsListExcludeModalOpen(true)}
>
{newFilter.profile_list_exclusions.length > 0 ?
newFilter.profile_list_exclusions
.map((id) => FilterProfileListIdToString[id])
.join(", ")
: "Неважно"}
</Button>
<p className="text-sm">
Исключит из выдачи релизы, входящие в указанные закладки
</p>
</div> : ""}
</div>
</ModalBody>
<ModalFooter></ModalFooter>
</Modal>
<FiltersGenreModal
isOpen={isGenreModalOpen}
setIsOpen={setIsGenreModalOpen}
genres={newFilter.genres}
exclusionMode={newFilter.is_genres_exclude_mode_enabled}
save={saveGenres}
/>
<FiltersListExcludeModal
isOpen={isListExcludeModalOpen}
setIsOpen={setIsListExcludeModalOpen}
lists={newFilter.profile_list_exclusions}
setLists={(profile_list_exclusions) =>
setNewFilter({ ...newFilter, profile_list_exclusions })
}
/>
</>
);
};

View file

@ -69,7 +69,7 @@ export const NavBarMobile = (props: { setIsSettingModalOpen: any }) => {
<footer className="fixed bottom-0 left-0 right-0 z-50 block w-full h-[70px] font-medium text-white bg-black rounded-t-lg lg:hidden">
<div className="flex items-center justify-center h-full gap-4">
{NavbarItems.map((item) => {
if (item.auth && !userStore.isAuth) return <></>;
if (item.auth && !userStore.isAuth) return;
return (
<Link
href={item.href}
@ -90,7 +90,7 @@ export const NavBarMobile = (props: { setIsSettingModalOpen: any }) => {
<></>
: <Link
href={FifthButton[preferenceStore.flags.showFifthButton].href}
key={`navbar-mobile-${FifthButton[preferenceStore.flags.showFifthButton].title}`}
key={`navbar-mobile-fifthbutton-${FifthButton[preferenceStore.flags.showFifthButton].title}`}
className="flex flex-col items-center justify-center gap-1"
>
<span
@ -133,9 +133,8 @@ export const NavBarMobile = (props: { setIsSettingModalOpen: any }) => {
<span className="ml-2">Профиль</span>
</DropdownItem>
{Object.entries(FifthButton).map(([key, item]) => {
if (item.auth && !userStore.isAuth) return <></>;
if (preferenceStore.flags.showFifthButton === key)
return <></>;
if (item.auth && !userStore.isAuth) return;
if (preferenceStore.flags.showFifthButton === key) return;
return (
<DropdownItem
key={`navbar-mobile-${item.title}`}

View file

@ -65,7 +65,7 @@ export const NavBarPc = (props: { setIsSettingModalOpen: any }) => {
<div className="container flex items-center justify-between h-full px-2 mx-auto">
<div className="flex items-center h-full gap-3">
{NavbarItems.map((item) => {
if (item.auth && !userStore.isAuth) return <></>;
if (item.auth && !userStore.isAuth) return;
return (
<Link
href={item.href}

View file

@ -2,6 +2,7 @@
import { CollectionsOfTheWeek } from "#/components/Discovery/CollectionsOfTheWeek";
import { DiscussingToday } from "#/components/Discovery/DiscussingToday";
import { InterestingCarousel } from "#/components/Discovery/InterestingCarousel";
import { FiltersModal } from "#/components/Discovery/Modal/FiltersModal";
import { PopularModal } from "#/components/Discovery/Modal/PopularModal";
import { ScheduleModal } from "#/components/Discovery/Modal/ScheduleModal";
import { RecommendedCarousel } from "#/components/Discovery/RecommendedCarousel";
@ -14,6 +15,7 @@ export const DiscoverPage = () => {
const router = useRouter();
const [PopularModalOpen, setPopularModalOpen] = useState(false);
const [ScheduleModalOpen, setScheduleModalOpen] = useState(false);
const [FiltersModalOpen, setFiltersModalOpen] = useState(false);
return (
<>
@ -58,6 +60,7 @@ export const DiscoverPage = () => {
isOpen={ScheduleModalOpen}
setIsOpen={setScheduleModalOpen}
/>
<FiltersModal isOpen={FiltersModalOpen} setIsOpen={setFiltersModalOpen} />
</>
);
};

8
package-lock.json generated
View file

@ -12,7 +12,7 @@
"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 +3348,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",

View file

@ -14,7 +14,7 @@
"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",