mirror of
https://github.com/Radiquum/AniX.git
synced 2025-09-04 13:35:36 +05:00
anix/feat: add filters modal with country, category, genre and lists exclusion filters
This commit is contained in:
parent
d3b198c6bc
commit
777fb5b82b
9 changed files with 396 additions and 18 deletions
|
@ -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: "Сериал",
|
||||
|
|
124
app/components/Discovery/Modal/FiltersGenreModal.tsx
Normal file
124
app/components/Discovery/Modal/FiltersGenreModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
87
app/components/Discovery/Modal/FiltersListExcludeModal.tsx
Normal file
87
app/components/Discovery/Modal/FiltersListExcludeModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
172
app/components/Discovery/Modal/FiltersModal.tsx
Normal file
172
app/components/Discovery/Modal/FiltersModal.tsx
Normal 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 })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}`}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
8
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue