feat: search and filter

This commit is contained in:
2025-04-30 16:03:05 +03:00
parent 23c0f43526
commit 5ece8ca7eb
2 changed files with 292 additions and 101 deletions

View File

@@ -19,9 +19,10 @@ import {
MagnifyingGlassIcon,
PencilIcon,
TrashIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
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 { SubmitHandler, useForm } from "react-hook-form";
import { v4 as uuid } from "uuid";
@@ -37,6 +38,8 @@ const ProfileTasks: FunctionComponent = () => {
const [openModal, setIsOpen] = useState(false); // Открыта модалка
const [openModalCalendar, setOpenModalCalendar] = useState(false); // Открыта модалка календаря
const [openModalTags, setOpenModalTags] = useState(false); // Открыта модалка тегов
const [openSearchModal, setOpenSearchModal] = useState(false); // Открыто окно поиска
const [openFilterModal, setOpenFilterModal] = useState(false);
const [isEdit, setIsEdit] = useState(false); // Открыта задача
const [isEditModal, setIsEditModal] = useState(false); // Включено редактирование задачи
const [isCreating, setIsCreating] = useState(false); // Включено создание задачи
@@ -44,6 +47,10 @@ const ProfileTasks: FunctionComponent = () => {
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>(); // Выбранная в календаре дата
const [tags, setTags] = useState<ITags>({ first: "", second: "" });
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 date = new Date();
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
@@ -172,6 +179,35 @@ const ProfileTasks: FunctionComponent = () => {
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 (
<div class={classes.container}>
<ModalTags
@@ -389,110 +425,264 @@ const ProfileTasks: FunctionComponent = () => {
confirmText="Удалить"
cancelText="Отмена"
/>
{tasks.length > 0 ? (
<>
<div class={classes.header}>Сегодня: {getDate}</div>
<div class={classes.tasks_container}>
{groupTasksByDate.today.length > 0 ? (
<div class="w-full">
{groupTasksByDate.today.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>
) : (
<div class="w-full text-center text-xl">Задач на сегодня нет</div>
)}
{groupTasksByDate.tomorrow.length > 0 && (
<div class="flex w-full flex-col gap-3 md:gap-10">
<div class="w-full text-3xl font-semibold md:text-5xl">Завтра</div>
{groupTasksByDate.tomorrow.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>
)}
{groupTasksByDate.future.map((group) => (
<div class="flex w-full flex-col gap-3 md:gap-10" key={group.date.getTime()}>
<div class="w-full text-3xl font-semibold md:text-5xl">{formatDate(group.date)}</div>
{group.tasks.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>
))}
</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="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)]"
onClick={() => {
setIsCreating(true);
setIsOpen(true);
}}
>
<PlusIcon />
{!searchQuery && !filterTags.first && !filterTags.second ? (
filteredTasks.length > 0 ? (
<>
<div class={classes.header}>Сегодня: {getDate}</div>
<div class={classes.tasks_container}>
{groupTasksByDate.today.length > 0 ? (
<div class="w-full">
{groupTasksByDate.today.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>
) : (
<div class="w-full text-center text-xl">Задач на сегодня нет</div>
)}
{groupTasksByDate.tomorrow.length > 0 && (
<div class="flex w-full flex-col gap-3 md:gap-10">
<div class="w-full text-3xl font-semibold md:text-5xl">Завтра</div>
{groupTasksByDate.tomorrow.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>
)}
{groupTasksByDate.future.map((group) => (
<div class="flex w-full flex-col gap-3 md:gap-10" key={group.date.getTime()}>
<div class="w-full text-3xl font-semibold md:text-5xl">{formatDate(group.date)}</div>
{group.tasks.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>
))}
</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="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)]">
<MagnifyingGlassIcon class="size-12" />
</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)]">
<FunnelIcon class="size-12" />
</div>
</div>
</div>
</>
) : (
<>
</>
) : (
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</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="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)]"
onClick={() => {
setIsCreating(true);
setIsOpen(true);
)
) : (
<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="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)]"
onClick={() => {
setIsCreating(true);
setIsOpen(true);
}}
>
<PlusIcon />
</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="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" />
</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)]">
<FunnelIcon
class="size-12"
onClick={(e) => {
e.stopPropagation();
if (!openFilterModal) setOpenSearchModal(false);
setOpenFilterModal(!openFilterModal);
}}
>
<PlusIcon />
/>
</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 items-center gap-2">
<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
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={() => {
setOpenFirstList(!openFirstList);
setOpenSecondList(false);
}}
>
<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>
);