feat: search and filter
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -8,5 +8,6 @@
|
|||||||
"([\"'`][^\"'`]*.*?[\"'`])",
|
"([\"'`][^\"'`]*.*?[\"'`])",
|
||||||
"[\"'`]([^\"'`]*).*?[\"'`]"
|
"[\"'`]([^\"'`]*).*?[\"'`]"
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
||||||
@@ -19,9 +19,10 @@ import {
|
|||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { FunctionComponent } from "preact";
|
import { FunctionComponent } from "preact";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { Nullable } from "primereact/ts-helpers";
|
import { Nullable } from "primereact/ts-helpers";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
@@ -37,6 +38,8 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
const [openModal, setIsOpen] = useState(false); // Открыта модалка
|
const [openModal, setIsOpen] = useState(false); // Открыта модалка
|
||||||
const [openModalCalendar, setOpenModalCalendar] = useState(false); // Открыта модалка календаря
|
const [openModalCalendar, setOpenModalCalendar] = useState(false); // Открыта модалка календаря
|
||||||
const [openModalTags, setOpenModalTags] = useState(false); // Открыта модалка тегов
|
const [openModalTags, setOpenModalTags] = useState(false); // Открыта модалка тегов
|
||||||
|
const [openSearchModal, setOpenSearchModal] = useState(false); // Открыто окно поиска
|
||||||
|
const [openFilterModal, setOpenFilterModal] = useState(false);
|
||||||
const [isEdit, setIsEdit] = useState(false); // Открыта задача
|
const [isEdit, setIsEdit] = useState(false); // Открыта задача
|
||||||
const [isEditModal, setIsEditModal] = useState(false); // Включено редактирование задачи
|
const [isEditModal, setIsEditModal] = useState(false); // Включено редактирование задачи
|
||||||
const [isCreating, setIsCreating] = useState(false); // Включено создание задачи
|
const [isCreating, setIsCreating] = useState(false); // Включено создание задачи
|
||||||
@@ -44,6 +47,10 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>(); // Выбранная в календаре дата
|
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>(); // Выбранная в календаре дата
|
||||||
const [tags, setTags] = useState<ITags>({ first: "", second: "" });
|
const [tags, setTags] = useState<ITags>({ first: "", second: "" });
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState(""); // Текст поиска
|
||||||
|
const [filterTags, setFilterTags] = useState<ITags>({ first: "", second: "" });
|
||||||
|
const [openFirstList, setOpenFirstList] = useState(false);
|
||||||
|
const [openSecondList, setOpenSecondList] = useState(false);
|
||||||
const getDate = useMemo(() => {
|
const getDate = useMemo(() => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
||||||
@@ -172,6 +179,35 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
setShowDeleteDialog(false);
|
setShowDeleteDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredTasks = useMemo(() => {
|
||||||
|
let filtered = tasks;
|
||||||
|
|
||||||
|
// Фильтрация по поиску
|
||||||
|
if (searchQuery) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(task) =>
|
||||||
|
task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
task.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтрация по тегам
|
||||||
|
if (filterTags.first || filterTags.second) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(task) =>
|
||||||
|
(!filterTags.first || task.tags[0] === filterTags.first) &&
|
||||||
|
(!filterTags.second || task.tags[1] === filterTags.second)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, [tasks, searchQuery, filterTags]);
|
||||||
|
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchInputRef.current && openSearchModal) searchInputRef.current.focus();
|
||||||
|
}, [searchInputRef, openSearchModal]);
|
||||||
return (
|
return (
|
||||||
<div class={classes.container}>
|
<div class={classes.container}>
|
||||||
<ModalTags
|
<ModalTags
|
||||||
@@ -389,7 +425,8 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
confirmText="Удалить"
|
confirmText="Удалить"
|
||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
/>
|
/>
|
||||||
{tasks.length > 0 ? (
|
{!searchQuery && !filterTags.first && !filterTags.second ? (
|
||||||
|
filteredTasks.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div class={classes.header}>Сегодня: {getDate}</div>
|
<div class={classes.header}>Сегодня: {getDate}</div>
|
||||||
<div class={classes.tasks_container}>
|
<div class={classes.tasks_container}>
|
||||||
@@ -458,6 +495,39 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div class={classes.tasks_container}>
|
||||||
|
<div class="mb-8 w-full text-3xl font-semibold md:text-5xl">
|
||||||
|
{searchQuery ? "Результаты поиска" : "Результаты фильтрации"}
|
||||||
|
</div>
|
||||||
|
{filteredTasks.length > 0 ? (
|
||||||
|
filteredTasks.map((task) => (
|
||||||
|
<Task
|
||||||
|
name={task.name}
|
||||||
|
key={task.id}
|
||||||
|
checked={task.checked}
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setIsEdit(true);
|
||||||
|
setEditContent(task);
|
||||||
|
setCalendarDate(task.date);
|
||||||
|
}}
|
||||||
|
onMarkClick={() => {
|
||||||
|
setTasks(tasks.map((t) => (t.id === task.id ? { ...t, checked: !t.checked } : t)));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div class="w-full text-center text-xl">
|
||||||
|
{searchQuery ? "Ничего не найдено" : "Нет задач с выбранными фильтрами"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div class="group fixed right-1 bottom-16 flex flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:right-[22rem] md:bottom-4">
|
<div class="group fixed right-1 bottom-16 flex flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:right-[22rem] md:bottom-4">
|
||||||
<div
|
<div
|
||||||
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out group-hover:ml-[12rem] hover:bg-[rgb(251,194,199,0.7)]"
|
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out group-hover:ml-[12rem] hover:bg-[rgb(251,194,199,0.7)]"
|
||||||
@@ -469,30 +539,150 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 my-auto hidden flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100 md:flex">
|
<div class="absolute left-0 my-auto hidden flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100 md:flex">
|
||||||
<div class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]">
|
<div
|
||||||
|
class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!openSearchModal) setOpenFilterModal(false);
|
||||||
|
setOpenSearchModal(!openSearchModal);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MagnifyingGlassIcon class="size-12" />
|
<MagnifyingGlassIcon class="size-12" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]">
|
<div class="pointer-events-none flex aspect-square h-20 cursor-pointer flex-col items-center justify-center rounded-full bg-[rgba(206,232,251,0.7)] text-xl text-gray-600 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] group-hover:pointer-events-auto hover:bg-[rgba(206,232,251,0.9)]">
|
||||||
<FunnelIcon class="size-12" />
|
<FunnelIcon
|
||||||
|
class="size-12"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!openFilterModal) setOpenSearchModal(false);
|
||||||
|
setOpenFilterModal(!openFilterModal);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
{openSearchModal && (
|
||||||
) : (
|
<div class="absolute right-24 bottom-28 z-50 w-80 rounded-lg bg-white p-4 shadow-lg md:right-[24rem] md:bottom-28">
|
||||||
<>
|
{/* Пофиксить разфокус при вводе */}
|
||||||
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="group fixed right-1 bottom-16 flex flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:right-[22rem] md:bottom-4">
|
<MagnifyingGlassIcon class="size-6 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border-0 px-4 py-2 outline-none"
|
||||||
|
placeholder="Поиск"
|
||||||
|
ref={searchInputRef}
|
||||||
|
value={searchQuery}
|
||||||
|
onInput={(e) => {
|
||||||
|
setSearchQuery((e.target as HTMLInputElement).value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<XMarkIcon
|
||||||
|
class="size-5 cursor-pointer text-gray-400 hover:text-gray-600"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{openFilterModal && (
|
||||||
|
<div class="absolute right-24 bottom-28 z-50 w-80 rounded-lg bg-white p-4 shadow-lg md:right-[24rem] md:bottom-28">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="text-lg font-semibold">Фильтры</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out hover:bg-[rgb(251,194,199,0.7)]"
|
class={cn("flex cursor-pointer items-center gap-2 rounded-lg bg-[rgba(206,232,251,0.7)] px-4 py-2", {
|
||||||
|
"bg-[rgba(206,232,251,0.9)]": filterTags.first,
|
||||||
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCreating(true);
|
setOpenFirstList(!openFirstList);
|
||||||
setIsOpen(true);
|
setOpenSecondList(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<BookOpenIcon class="size-5" />
|
||||||
|
<span>{filterTags.first || "Предмет"}</span>
|
||||||
|
</div>
|
||||||
|
{openFirstList && (
|
||||||
|
<div class="absolute top-0 right-full z-50 mr-2 w-64 rounded-lg bg-white p-2 shadow-lg">
|
||||||
|
{example_tags.first.map((tag) => (
|
||||||
|
<div
|
||||||
|
key={tag}
|
||||||
|
class="group flex cursor-pointer items-center gap-2 rounded-lg px-4 py-2 hover:bg-[rgba(206,232,251,0.3)]"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterTags({ ...filterTags, first: tag });
|
||||||
|
setOpenFirstList(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={cn("flex h-5 w-5 items-center justify-center rounded-full border-2", {
|
||||||
|
"border-[#FBC2C7] bg-[#FBC2C7]": filterTags.first === tag,
|
||||||
|
"border-[#D4D4D4]": filterTags.first !== tag,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filterTags.first === tag && <div class="h-2.5 w-2.5 rounded-full bg-white" />}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-[#404040]">{tag}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class={cn("flex cursor-pointer items-center gap-2 rounded-lg bg-[rgba(206,232,251,0.7)] px-4 py-2", {
|
||||||
|
"bg-[rgba(206,232,251,0.9)]": filterTags.second,
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenSecondList(!openSecondList);
|
||||||
|
setOpenFirstList(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DocumentDuplicateIcon class="size-5" />
|
||||||
|
<span>{filterTags.second || "Задача"}</span>
|
||||||
|
</div>
|
||||||
|
{openSecondList && (
|
||||||
|
<div class="absolute top-0 right-full z-50 mr-2 w-64 rounded-lg bg-white p-2 shadow-lg">
|
||||||
|
{example_tags.second.map((tag) => (
|
||||||
|
<div
|
||||||
|
key={tag}
|
||||||
|
class="group flex cursor-pointer items-center gap-2 rounded-lg px-4 py-2 hover:bg-[rgba(206,232,251,0.3)]"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterTags({ ...filterTags, second: tag });
|
||||||
|
setOpenSecondList(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={cn("flex h-5 w-5 items-center justify-center rounded-full border-2", {
|
||||||
|
"border-[#FBC2C7] bg-[#FBC2C7]": filterTags.second === tag,
|
||||||
|
"border-[#D4D4D4]": filterTags.second !== tag,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{filterTags.second === tag && <div class="h-2.5 w-2.5 rounded-full bg-white" />}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-[#404040]">{tag}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(filterTags.first || filterTags.second) && (
|
||||||
|
<button
|
||||||
|
class="mt-2 w-full rounded-lg bg-red-100 px-4 py-2 text-red-600 hover:bg-red-200"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterTags({ first: "", second: "" });
|
||||||
|
setOpenFirstList(false);
|
||||||
|
setOpenSecondList(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сбросить фильтры
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user