feat: tag delete

This commit is contained in:
2025-05-12 01:43:10 +03:00
parent 95a264f5a4
commit 19ce2bf278
4 changed files with 218 additions and 149 deletions

View File

@@ -2,9 +2,11 @@ import { IAPITag } from "@/pages/profile_tasks.dto";
import apiClient from "@/services/api"; import apiClient from "@/services/api";
import { cn } from "@/utils/class-merge"; import { cn } from "@/utils/class-merge";
import { BookOpenIcon, CheckIcon, DocumentDuplicateIcon, PlusCircleIcon } from "@heroicons/react/24/outline"; import { BookOpenIcon, CheckIcon, DocumentDuplicateIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import Button from "./ui/Button"; import Button from "./ui/Button";
import Dialog from "./ui/Dialog";
import ModalWindow, { ModalWindowProps } from "./ui/Modal"; import ModalWindow, { ModalWindowProps } from "./ui/Modal";
export interface ITags { export interface ITags {
@@ -40,6 +42,8 @@ const ModalTags: FunctionComponent<ModalTagsProps> = ({
const [showSecondTags, setShowSecondTags] = useState(false); const [showSecondTags, setShowSecondTags] = useState(false);
const [showAddFirstTag, setShowAddFirstTag] = useState(false); const [showAddFirstTag, setShowAddFirstTag] = useState(false);
const [showAddSecondTag, setShowAddSecondTag] = useState(false); const [showAddSecondTag, setShowAddSecondTag] = useState(false);
const [showErrorDialog, setShowErrorDialog] = useState(false);
const [errorMessage, setErrorMessage] = useState<string[]>([]);
const addFirstTagRef = useRef<HTMLInputElement>(null); const addFirstTagRef = useRef<HTMLInputElement>(null);
const addSecondTagRef = useRef<HTMLInputElement>(null); const addSecondTagRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@@ -51,6 +55,7 @@ const ModalTags: FunctionComponent<ModalTagsProps> = ({
const handleCreateTag = async () => { const handleCreateTag = async () => {
if (!addFirstTagRef.current?.value && !addSecondTagRef.current?.value) return; if (!addFirstTagRef.current?.value && !addSecondTagRef.current?.value) return;
const data: { subject_name?: string; taskType_name?: string } = {}; const data: { subject_name?: string; taskType_name?: string } = {};
if (addFirstTagRef.current && addFirstTagRef.current.value) data.subject_name = addFirstTagRef.current.value; if (addFirstTagRef.current && addFirstTagRef.current.value) data.subject_name = addFirstTagRef.current.value;
if (addSecondTagRef.current && addSecondTagRef.current.value) data.taskType_name = addSecondTagRef.current.value; if (addSecondTagRef.current && addSecondTagRef.current.value) data.taskType_name = addSecondTagRef.current.value;
@@ -71,139 +76,193 @@ const ModalTags: FunctionComponent<ModalTagsProps> = ({
setShowAddSecondTag(false); setShowAddSecondTag(false);
} }
} }
if (addFirstTagRef.current) addFirstTagRef.current.value = "";
if (addSecondTagRef.current) addSecondTagRef.current.value = "";
};
const handleDeleteTag = async (id: number) => {
const data: { subject_id?: number; taskType_id?: number } = {};
if (showFirstTags) data.subject_id = id;
if (showSecondTags) data.taskType_id = id;
const response = await apiClient<{ error?: string }>(`/api/tags/delete_tag/`, {
method: "DELETE",
body: JSON.stringify(data),
});
if (!response.error) {
refreshTags();
} else {
setShowErrorDialog(true);
const match = response.error?.match(/\[(.+)\]/);
if (match) setErrorMessage(match[1].split(", ").map((s) => s.trim().replace(/'/g, "")));
}
}; };
return ( return (
<ModalWindow <>
isOpen={isOpen} <Dialog
{...rest} isOpen={showErrorDialog}
setIsOpen={setIsOpen} setIsOpen={setShowErrorDialog}
onClose={() => { title="Ошибка!"
onClose!(); confirmation={false}
setShowFirstTags(false); cancelText="Ок"
setShowSecondTags(false); >
setShowAddFirstTag(false); <div class="flex flex-col items-start gap-1">
setShowAddSecondTag(false); <p class="mb-6 text-sm sm:text-[1rem]">
}} Данный тег есть в других задачах. Поменяйте теги в них и повторите операцию.
className="relative h-[14rem] justify-between py-4 md:h-[14rem] md:w-[25rem]" </p>
zIndex={70} <p class="text-sm sm:text-[1rem]">Задачи с этим тегом:</p>
> <ul class="ms-6 list-disc">
<p class="text-2xl font-semibold">Теги</p> {errorMessage.map((s, i) => (
<div class="flex w-[85%] flex-col gap-2 md:w-full"> <li key={i}>{s}</li>
<Button ))}
className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]!" </ul>
onClick={() => { </div>
setShowFirstTags(!showFirstTags); </Dialog>
setShowSecondTags(false); <ModalWindow
setShowAddFirstTag(false); isOpen={isOpen}
setShowAddSecondTag(false); {...rest}
}} setIsOpen={setIsOpen}
> onClose={() => {
<BookOpenIcon class="size-6" /> onClose!();
{tagsList?.first?.find((tag) => tag.id === value?.first)?.name || "Предмет"} setShowFirstTags(false);
</Button> setShowSecondTags(false);
<Button setShowAddFirstTag(false);
className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]! break-words" setShowAddSecondTag(false);
onClick={() => { }}
setShowSecondTags(!showSecondTags); className="relative h-[14rem] justify-between py-4 md:h-[14rem] md:w-[25rem]"
setShowFirstTags(false); zIndex={70}
setShowAddFirstTag(false); >
setShowAddSecondTag(false); <p class="text-2xl font-semibold">Теги</p>
}} <div class="flex w-[85%] flex-col gap-2 md:w-full">
> <Button
<DocumentDuplicateIcon class="size-6" /> className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]!"
{tagsList?.second?.find((tag) => tag.id === value?.second)?.name || "Тема"} onClick={() => {
</Button> setShowFirstTags(!showFirstTags);
{showFirstTags && ( setShowSecondTags(false);
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center gap-3 md:top-0 md:left-full md:mt-0 md:ml-5"> setShowAddFirstTag(false);
<div class="flex h-fit max-h-[45vh] w-full flex-col gap-3 overflow-y-auto rounded-[4rem] bg-white px-8 py-4 text-xs"> setShowAddSecondTag(false);
{tagsList?.first.map((tag) => ( }}
<div >
class="flex cursor-pointer flex-row gap-2" <BookOpenIcon class="size-6" />
onClick={() => {tagsList?.first?.find((tag) => tag.id === value?.first)?.name || "Предмет"}
onChange?.((prev) => { </Button>
return { ...prev, first: tag.id }; <Button
}) className="flex w-full flex-row items-center justify-center gap-1 text-[1rem]! break-words"
} onClick={() => {
> setShowSecondTags(!showSecondTags);
setShowFirstTags(false);
setShowAddFirstTag(false);
setShowAddSecondTag(false);
}}
>
<DocumentDuplicateIcon class="size-6" />
{tagsList?.second?.find((tag) => tag.id === value?.second)?.name || "Тема"}
</Button>
{showFirstTags && (
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center gap-3 md:top-0 md:left-full md:mt-0 md:ml-5">
<div class="flex h-fit max-h-[15rem] w-full flex-col gap-3 overflow-y-auto rounded-[4rem] bg-white px-8 py-4 text-xs">
{tagsList?.first.map((tag) => (
<div <div
class={cn( class="flex cursor-pointer flex-row items-center gap-2"
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none", onClick={() =>
{ onChange?.((prev) => {
"bg-black": value?.first === tag.id, return { ...prev, first: tag.id };
} })
)} }
> >
<div
class={cn(
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none",
{
"bg-black": value?.first === tag.id,
}
)}
>
</div>
<p class="flex-1">{tag.name}</p>
<XMarkIcon
class="size-4 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
handleDeleteTag(tag.id);
}}
/>
</div> </div>
<p>{tag.name}</p> ))}
</div> <button
))} class="mt-2 flex w-full cursor-pointer flex-row items-center gap-2"
<button onClick={() => {
class="mt-2 flex w-full cursor-pointer flex-row items-center gap-2" setShowAddFirstTag(!showAddFirstTag);
onClick={() => { setTimeout(() => addFirstTagRef.current?.focus(), 100);
setShowAddFirstTag(!showAddFirstTag); }}
setTimeout(() => addFirstTagRef.current?.focus(), 100);
}}
>
<PlusCircleIcon class="size-5" />
<span>Добавить</span>
</button>
</div>
{showAddFirstTag && (
<div class="flex w-full flex-row items-center rounded-[4rem] bg-white px-8 py-4 text-xs">
<input placeholder="Введите текст" class="flex-1 outline-0" ref={addFirstTagRef} />
<CheckIcon class="size-6 cursor-pointer" onClick={handleCreateTag} />
</div>
)}
</div>
)}
{showSecondTags && (
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center gap-3 md:top-0 md:left-full md:mt-0 md:ml-5">
<div class="flex h-fit max-h-[45vh] w-full flex-col gap-3 overflow-y-auto rounded-[4rem] bg-white px-8 py-4 text-xs">
{tagsList?.second.map((tag) => (
<div
class="flex cursor-pointer flex-row gap-2"
onClick={() =>
onChange?.((prev) => {
return { ...prev, second: tag.id };
})
}
> >
<div <PlusCircleIcon class="size-5" />
class={cn( <span>Добавить</span>
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none", </button>
{
"bg-black": value?.second === tag.id,
}
)}
>
</div>
<p>{tag.name}</p>
</div>
))}
<button
class="mt-2 flex w-full cursor-pointer flex-row items-center gap-2"
onClick={() => {
setShowAddSecondTag(!showAddSecondTag);
setTimeout(() => addSecondTagRef.current?.focus(), 100);
}}
>
<PlusCircleIcon class="size-5" />
<span>Добавить</span>
</button>
</div>
{showAddSecondTag && (
<div class="flex w-full flex-row items-center rounded-[4rem] bg-white px-8 py-4 text-xs">
<input placeholder="Введите текст" class="flex-1 outline-0" ref={addSecondTagRef} />
<CheckIcon class="size-6 cursor-pointer" onClick={handleCreateTag} />
</div> </div>
)} {showAddFirstTag && (
</div> <div class="flex w-full flex-row items-center rounded-[4rem] bg-white px-8 py-4 text-xs">
)} <input placeholder="Введите текст" class="flex-1 outline-0" ref={addFirstTagRef} />
</div> <CheckIcon class="size-6 cursor-pointer" onClick={handleCreateTag} />
</ModalWindow> </div>
)}
</div>
)}
{showSecondTags && (
<div class="absolute top-full left-0 mt-3 ml-2 flex h-fit w-[18rem] flex-col items-center gap-3 md:top-0 md:left-full md:mt-0 md:ml-5">
<div class="flex h-fit max-h-[15rem] w-full flex-col gap-3 overflow-y-auto rounded-[4rem] bg-white px-8 py-4 text-xs">
{tagsList?.second.map((tag) => (
<div
class="flex cursor-pointer flex-row gap-2"
onClick={() =>
onChange?.((prev) => {
return { ...prev, second: tag.id };
})
}
>
<div
class={cn(
"pointer-events-none flex aspect-square size-4 flex-col items-center justify-center rounded-full border-1 border-black text-white select-none",
{
"bg-black": value?.second === tag.id,
}
)}
>
</div>
<p class="flex-1">{tag.name}</p>
<XMarkIcon
class="size-4 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
handleDeleteTag(tag.id);
}}
/>
</div>
))}
<button
class="mt-2 flex w-full cursor-pointer flex-row items-center gap-2"
onClick={() => {
setShowAddSecondTag(!showAddSecondTag);
setTimeout(() => addSecondTagRef.current?.focus(), 100);
}}
>
<PlusCircleIcon class="size-5" />
<span>Добавить</span>
</button>
</div>
{showAddSecondTag && (
<div class="flex w-full flex-row items-center rounded-[4rem] bg-white px-8 py-4 text-xs">
<input placeholder="Введите текст" class="flex-1 outline-0" ref={addSecondTagRef} />
<CheckIcon class="size-6 cursor-pointer" onClick={handleCreateTag} />
</div>
)}
</div>
)}
</div>
</ModalWindow>
</>
); );
}; };

View File

@@ -5,20 +5,21 @@ interface DialogProps {
isOpen: boolean; isOpen: boolean;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
title: string; title: string;
content: string; onConfirm?: () => void;
onConfirm: () => void;
confirmText?: string; confirmText?: string;
cancelText?: string; cancelText?: string;
confirmation?: boolean;
} }
const Dialog: FunctionComponent<DialogProps> = ({ const Dialog: FunctionComponent<DialogProps> = ({
isOpen, isOpen,
setIsOpen, setIsOpen,
title, title,
content, children,
onConfirm, onConfirm = () => {},
confirmText = "Подтвердить", confirmText = "Подтвердить",
cancelText = "Отмена", cancelText = "Отмена",
confirmation = true,
}) => { }) => {
if (!isOpen) return null; if (!isOpen) return null;
@@ -26,20 +27,28 @@ const Dialog: FunctionComponent<DialogProps> = ({
<div class="fixed inset-0 z-150 flex items-center justify-center bg-black/50"> <div class="fixed inset-0 z-150 flex items-center justify-center bg-black/50">
<div class="w-full max-w-md rounded-[4rem] bg-white p-6 shadow-lg"> <div class="w-full max-w-md rounded-[4rem] bg-white p-6 shadow-lg">
<h2 class="mb-4 text-xl font-semibold">{title}</h2> <h2 class="mb-4 text-xl font-semibold">{title}</h2>
<p class="mb-6 text-sm sm:text-[1rem]">{content}</p> {children}
<div class="flex justify-end gap-4"> <div class="flex justify-end gap-4">
<Button onClick={() => setIsOpen(false)} className="bg-gray-200 text-gray-800 hover:bg-gray-300"> {confirmation ? (
{cancelText} <>
</Button> <Button onClick={() => setIsOpen(false)} className="bg-gray-200 text-gray-800 hover:bg-gray-300">
<Button {cancelText}
onClick={() => { </Button>
onConfirm(); <Button
setIsOpen(false); onClick={() => {
}} onConfirm();
color="red" setIsOpen(false);
> }}
{confirmText} color="red"
</Button> >
{confirmText}
</Button>
</>
) : (
<Button onClick={() => setIsOpen(false)} className="bg-gray-200 text-gray-800 hover:bg-gray-300">
{cancelText}
</Button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -538,11 +538,12 @@ const ProfileCalendar: FunctionComponent = () => {
isOpen={showDeleteDialog} isOpen={showDeleteDialog}
setIsOpen={setShowDeleteDialog} setIsOpen={setShowDeleteDialog}
title="Удаление задачи" title="Удаление задачи"
content="Вы уверены, что хотите удалить эту задачу?"
onConfirm={handleDeleteTask} onConfirm={handleDeleteTask}
confirmText="Удалить" confirmText="Удалить"
cancelText="Отмена" cancelText="Отмена"
/> >
<p class="mb-6 text-sm sm:text-[1rem]">"Вы уверены, что хотите удалить эту задачу?"</p>
</Dialog>
<Calendar <Calendar
value={currentDate} value={currentDate}
onChange={(e) => setCurrentDate(e ? e.value! : new Date())} onChange={(e) => setCurrentDate(e ? e.value! : new Date())}

View File

@@ -154,7 +154,6 @@ const ProfileTasks: FunctionComponent = () => {
} }
if ((!editContent?.subject.id || !editContent.taskType.id) && (!tags.first || !tags.second)) { if ((!editContent?.subject.id || !editContent.taskType.id) && (!tags.first || !tags.second)) {
setError("subject", { message: "Выберите теги" }); setError("subject", { message: "Выберите теги" });
setError("taskType", { message: "Выберите теги" });
return; return;
} }
@@ -631,11 +630,12 @@ const ProfileTasks: FunctionComponent = () => {
isOpen={showDeleteDialog} isOpen={showDeleteDialog}
setIsOpen={setShowDeleteDialog} setIsOpen={setShowDeleteDialog}
title="Удаление задачи" title="Удаление задачи"
content="Вы уверены, что хотите удалить эту задачу?"
onConfirm={handleDeleteTask} onConfirm={handleDeleteTask}
confirmText="Удалить" confirmText="Удалить"
cancelText="Отмена" cancelText="Отмена"
/> >
<p class="mb-6 text-sm sm:text-[1rem]">"Вы уверены, что хотите удалить эту задачу?"</p>
</Dialog>
{!searchQuery && !filterTags.first && !filterTags.second && !filterTags.overdue ? ( {!searchQuery && !filterTags.first && !filterTags.second && !filterTags.overdue ? (
filteredTasks.length > 0 ? ( filteredTasks.length > 0 ? (
<> <>
@@ -892,7 +892,7 @@ const ProfileTasks: FunctionComponent = () => {
<span>{subjectChoices.find((s) => s.id === filterTags.first)?.name || "Предмет"}</span> <span>{subjectChoices.find((s) => s.id === filterTags.first)?.name || "Предмет"}</span>
</div> </div>
{openFirstList && ( {openFirstList && (
<div class="absolute top-full right-0 z-50 w-64 rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2"> <div class="absolute top-full right-0 z-50 max-h-[15rem] w-64 overflow-y-auto rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2">
{example_tags.first.map((tag) => { {example_tags.first.map((tag) => {
return ( return (
<div <div
@@ -936,7 +936,7 @@ const ProfileTasks: FunctionComponent = () => {
<span>{taskTypeChoices.find((s) => s.id === filterTags.second)?.name || "Задача"}</span> <span>{taskTypeChoices.find((s) => s.id === filterTags.second)?.name || "Задача"}</span>
</div> </div>
{openSecondList && ( {openSecondList && (
<div class="absolute top-full right-0 z-50 w-64 rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2"> <div class="absolute top-full right-0 z-50 max-h-[15rem] w-64 overflow-y-auto rounded-lg bg-white p-2 shadow-lg md:top-0 md:right-full md:mr-2">
{example_tags.second.map((tag) => ( {example_tags.second.map((tag) => (
<div <div
key={`taskType_${tag.id}`} key={`taskType_${tag.id}`}