From e8ded7f2ae9b297f03fac67e485cdc6dea15b197 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Fri, 25 Apr 2025 15:20:20 +0300 Subject: [PATCH] feat: tags on tasks --- bun.lock | 6 ++ package.json | 4 +- src/components/ModalTags.tsx | 125 +++++++++++++++++++++++++++++++++ src/components/calendar.tsx | 3 + src/components/ui/Button.tsx | 16 ++--- src/pages/profile_tasks.dto.ts | 3 +- src/pages/profile_tasks.tsx | 94 ++++++++++++++++--------- 7 files changed, 203 insertions(+), 48 deletions(-) create mode 100644 src/components/ModalTags.tsx diff --git a/bun.lock b/bun.lock index a99175a..ecaf550 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@preact/signals": "^2.0.2", "@tailwindcss/postcss": "^4.0.17", "@tailwindcss/vite": "^4.0.17", + "@types/uuid": "^10.0.0", "clsx": "^2.1.1", "postcss": "^8.5.3", "preact": "^10.26.2", @@ -18,6 +19,7 @@ "tailwind-merge": "^3.0.2", "tailwind-variants": "^1.0.0", "tailwindcss": "^4.0.17", + "uuid": "^11.1.0", }, "devDependencies": { "@eslint/js": "^9.23.0", @@ -281,6 +283,8 @@ "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.28.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/type-utils": "8.28.0", "@typescript-eslint/utils": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.28.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/types": "8.28.0", "@typescript-eslint/typescript-estree": "8.28.0", "@typescript-eslint/visitor-keys": "8.28.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ=="], @@ -735,6 +739,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], "vite": ["vite@6.2.3", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg=="], diff --git a/package.json b/package.json index 6966121..41b6e65 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@preact/signals": "^2.0.2", "@tailwindcss/postcss": "^4.0.17", "@tailwindcss/vite": "^4.0.17", + "@types/uuid": "^10.0.0", "clsx": "^2.1.1", "postcss": "^8.5.3", "preact": "^10.26.2", @@ -22,7 +23,8 @@ "react-hook-form": "^7.56.1", "tailwind-merge": "^3.0.2", "tailwind-variants": "^1.0.0", - "tailwindcss": "^4.0.17" + "tailwindcss": "^4.0.17", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/js": "^9.23.0", diff --git a/src/components/ModalTags.tsx b/src/components/ModalTags.tsx new file mode 100644 index 0000000..bdd0103 --- /dev/null +++ b/src/components/ModalTags.tsx @@ -0,0 +1,125 @@ +import { cn } from "@/utils/class-merge"; +import { BookOpenIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; +import { FunctionComponent } from "preact"; +import { Dispatch, StateUpdater, useEffect, useState } from "preact/hooks"; +import Button from "./ui/Button"; +import ModalWindow from "./ui/Modal"; + +export interface ITags { + first: string; + second: string; +} + +interface ModalTagsProps { + isOpen?: boolean; + setIsOpen?: Dispatch>; + onClose?: () => void; + value?: ITags; + onChange?: Dispatch>; + tagsList?: { + first: string[]; + second: string[]; + }; +} + +const ModalTags: FunctionComponent = ({ isOpen, setIsOpen, onClose, onChange, value, tagsList }) => { + const [showFirstTags, setShowFirstTags] = useState(false); + const [showSecondTags, setShowSecondTags] = useState(false); + useEffect(() => { + if (showFirstTags && showSecondTags) setShowFirstTags(false); + }, [showSecondTags]); + useEffect(() => { + if (showFirstTags && showSecondTags) setShowSecondTags(false); + }, [showFirstTags]); + return ( + { + onClose!(); + setShowFirstTags(false); + setShowSecondTags(false); + }} + className="relative h-[14rem] justify-between py-4 md:h-[14rem] md:w-[25rem]" + zIndex={70} + > +

Теги

+
+ + + {showFirstTags && ( +
+
+ {tagsList?.first.map((tag) => ( +
+ onChange?.((prev) => { + return { ...prev, first: tag }; + }) + } + > +
+ ✓ +
+

{tag}

+
+ ))} +
+
+ )} + {showSecondTags && ( +
+
+ {tagsList?.second.map((tag) => ( +
+ onChange?.((prev) => { + return { ...prev, second: tag }; + }) + } + > +
+ ✓ +
+

{tag}

+
+ ))} +
+
+ )} +
+
+ ); +}; + +ModalTags.displayName = "AHModalTags"; + +export default ModalTags; diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx index 044300b..828035a 100644 --- a/src/components/calendar.tsx +++ b/src/components/calendar.tsx @@ -220,6 +220,9 @@ const BigCalendar: FunctionComponent = ({ return (
+
+ Закрыто +
); -}; +}); Button.displayName = "AHButton"; diff --git a/src/pages/profile_tasks.dto.ts b/src/pages/profile_tasks.dto.ts index 40d8d52..6bc7b88 100644 --- a/src/pages/profile_tasks.dto.ts +++ b/src/pages/profile_tasks.dto.ts @@ -1,10 +1,11 @@ export interface ITask { - id: number; + id: string; name: string; checked: boolean; date: Date; description: string; tags: string[]; + new?: boolean; } export interface ITaskForm extends Omit { diff --git a/src/pages/profile_tasks.tsx b/src/pages/profile_tasks.tsx index ca8a2d7..3c5ce01 100644 --- a/src/pages/profile_tasks.tsx +++ b/src/pages/profile_tasks.tsx @@ -1,6 +1,7 @@ import Task from "@/components/task"; import ModalCalendar from "@/components/ModalCalendar"; +import ModalTags, { ITags } from "@/components/ModalTags"; import Button from "@/components/ui/Button"; import ModalWindow from "@/components/ui/Modal"; import { withTitle } from "@/constructors/Component"; @@ -21,56 +22,38 @@ import { FunctionComponent } from "preact"; import { useEffect, useMemo, useState } from "preact/hooks"; import { Nullable } from "primereact/ts-helpers"; import { SubmitHandler, useForm } from "react-hook-form"; +import { v4 as uuid } from "uuid"; import { ITask, ITaskForm } from "./profile_tasks.dto"; import classes from "./profile_tasks.module.scss"; -const example_tasks: ITask[] = [ - { - checked: false, - date: new Date(), - description: "test", - id: 1, - name: "test1", - tags: ["Программирование", "Лабораторная работа"], - }, - { - checked: true, - date: new Date(2025, 6, 2), - description: "test2", - id: 3, - name: "test3", - tags: ["Информатика", "Практическая работа"], - }, - { - checked: false, - date: new Date(2025, 5, 1), - description: "test3", - id: 2, - name: "test2", - tags: ["Математика", "Домашнее задание"], - }, -]; +const example_tags: { first: string[]; second: string[] } = { + first: ["Программирование", "Информатика", "Физика", "Математика"], + second: ["Лабораторная работа", "Практическая работа", "Домашнее задание", "Экзамен"], +}; const ProfileTasks: FunctionComponent = () => { const [openModal, setIsOpen] = useState(false); // Открыта модалка const [openModalCalendar, setOpenModalCalendar] = useState(false); // Открыта модалка календаря + const [openModalTags, setOpenModalTags] = useState(false); // Открыта модалка тегов const [isEdit, setIsEdit] = useState(false); // Открыта задача const [isEditModal, setIsEditModal] = useState(false); // Включено редактирование задачи const [isCreating, setIsCreating] = useState(false); // Включено создание задачи const [editContent, setEditContent] = useState(null); // Содержимое редактируемой задачи const [calendarDate, setCalendarDate] = useState>(); // Выбранная в календаре дата + const [tags, setTags] = useState({ first: "", second: "" }); const getDate = useMemo(() => { const date = new Date(); const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" }); return formatter.format(date); }, []); - const init_tasks: ITask[] = localStorage.getItem("tasks") - ? JSON.parse(localStorage.getItem("tasks") as string) - : example_tasks; + const init_tasks: ITask[] = localStorage.getItem("tasks") ? JSON.parse(localStorage.getItem("tasks") as string) : []; + let clear = false; init_tasks.forEach((task) => { + clear = clear || (task.new == undefined ? true : false); + if (!clear) task.new = true; task.date = new Date(task.date); }); - const [tasks, setTasks] = useState(init_tasks); + const [tasks, setTasks] = useState(clear ? [] : init_tasks); useEffect(() => { localStorage.setItem("tasks", JSON.stringify(tasks)); }, [tasks]); @@ -91,10 +74,21 @@ const ProfileTasks: FunctionComponent = () => { setError("date", { message: "Выберите дату" }); return; } - const eTask: ITask = { ...data, date: calendarDate }; + console.log(tags); + if ((!editContent?.tags[0] || !editContent.tags[1]) && (!tags.first || !tags.second)) { + setError("tags", { message: "Выберите теги" }); + return; + } + const eTask: ITask = { + ...data, + date: calendarDate, + tags: editContent?.tags.length ? editContent.tags : [tags.first, tags.second], + new: true, + }; if (isCreating) setTasks([...tasks, eTask]); else setTasks(tasks.map((task) => (task.id === eTask.id ? eTask : task))); if (isCreating) setIsOpen(false); + setTags({ first: "", second: "" }); }; useEffect(() => { if (editContent) reset({ ...editContent, date: editContent.date.toISOString().slice(0, 16) }); @@ -105,12 +99,29 @@ const ProfileTasks: FunctionComponent = () => { name: "", description: "", date: "", - tags: ["Тег1", "Тег2"], + tags: [], checked: false, }); }, [isCreating]); + useEffect(() => { + if (!editContent) return; + const newEditContent = editContent; + if (tags.first) newEditContent.tags = [tags.first, newEditContent.tags[1]]; + if (tags.second) newEditContent.tags = [newEditContent.tags[0], tags.second]; + setEditContent(newEditContent); + }, [tags]); return (
+ { + if (!isCreating) setTags({ first: "", second: "" }); + }} + onChange={setTags} + /> { setEditContent(null); setIsCreating(false); setIsEditModal(false); + setTags({ first: "", second: "" }); setCalendarDate(null); }} > @@ -192,6 +204,8 @@ const ProfileTasks: FunctionComponent = () => {
{errors.name &&

{errors.name.message}

} {errors.description &&

{errors.description.message}

} + {errors.date &&

{errors.date.message}

} + {errors.tags &&

{errors.tags.message}

}
{ }).format(calendarDate!)}

-
+
{ + if (!isEditModal) return; + setTags({ first: editContent.tags[0], second: editContent.tags[1] }); + setOpenModalTags(true); + }} + >

{editContent.tags[0]} @@ -232,7 +255,7 @@ const ProfileTasks: FunctionComponent = () => { class="flex h-full w-full flex-col items-start justify-between" onSubmit={(e) => { e.preventDefault(); - handleSubmit((data) => saveTask({ ...data, id: tasks.length + 1 }))(); + handleSubmit((data) => saveTask({ ...data, id: uuid() }))(); }} >

@@ -265,11 +288,12 @@ const ProfileTasks: FunctionComponent = () => { setCalendarDate(calendarDate ?? new Date()); }} /> - + setOpenModalTags(true)} />
{errors.name &&

{errors.name.message}

} {errors.date &&

{errors.date.message}

} + {errors.tags &&

{errors.tags.message}

}