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,
|
||||
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user