From 021285bc09b8231697977c66bd6d270840d2966f Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 17 Jun 2026 09:45:04 +0330 Subject: [PATCH] task detail Labels --- src/langs/fa.json | 5 +- .../task-detail/TaskDetailActionBar.tsx | 27 ++++-- .../task-detail/TaskDetailMetadataPreview.tsx | 57 +++++++++++ .../task-detail/TaskDetailModal.tsx | 8 +- .../task-detail/TaskDetailToolbar.tsx | 76 +++++++++++++++ .../labels/TaskDetailLabelCheckbox.tsx | 29 ++++++ .../labels/TaskDetailLabelsList.tsx | 53 +++++++++++ .../labels/TaskDetailLabelsPopover.tsx | 43 +++++++++ .../labels/TaskDetailNewLabelForm.tsx | 95 +++++++++++++++++++ .../task-detail/labels/constants.ts | 19 ++++ .../components/task-detail/labels/types.ts | 7 ++ 11 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 src/pages/taskmanager/components/task-detail/TaskDetailMetadataPreview.tsx create mode 100644 src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx create mode 100644 src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelCheckbox.tsx create mode 100644 src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsList.tsx create mode 100644 src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx create mode 100644 src/pages/taskmanager/components/task-detail/labels/TaskDetailNewLabelForm.tsx create mode 100644 src/pages/taskmanager/components/task-detail/labels/constants.ts create mode 100644 src/pages/taskmanager/components/task-detail/labels/types.ts diff --git a/src/langs/fa.json b/src/langs/fa.json index 7455f67..d5e7577 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -997,7 +997,10 @@ "attachment": "ضمیمه", "date_settings": "تنظیمات تاریخ", "description": "توضیحات", - "description_placeholder": "توضیحات خود را اضافه کنید" + "description_placeholder": "توضیحات خود را اضافه کنید", + "new_label": "برچسب جدید", + "label_title": "عنوان", + "choose_custom_color": "انتخاب رنگ دلخواه" } } } diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx index e65930d..6adf460 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailActionBar.tsx @@ -1,5 +1,5 @@ import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react"; -import { type FC } from "react"; +import { type FC, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { TaskDetailTab } from "./types"; @@ -20,9 +20,10 @@ const TABS: TabItem[] = [ type Props = { activeTab: TaskDetailTab | null; onTabChange: (tab: TaskDetailTab) => void; + labelsPopover?: ReactNode; }; -const TaskDetailActionBar: FC = ({ activeTab, onTabChange }) => { +const TaskDetailActionBar: FC = ({ activeTab, onTabChange, labelsPopover }) => { const { t } = useTranslation("global"); return ( @@ -30,17 +31,31 @@ const TaskDetailActionBar: FC = ({ activeTab, onTabChange }) => { {TABS.map(({ id, icon: Icon, labelKey }) => { const isActive = activeTab === id; - return ( + const button = ( ); + + if (id === "labels") { + return ( +
+ {button} + {labelsPopover} +
+ ); + } + + return ( +
+ {button} +
+ ); })} ); diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailMetadataPreview.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailMetadataPreview.tsx new file mode 100644 index 0000000..4e60392 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/TaskDetailMetadataPreview.tsx @@ -0,0 +1,57 @@ +import { Add } from "iconsax-react"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import AvatarImage from "../../../../assets/images/avatar_image.png"; +import type { TaskLabel } from "./labels/types"; + +type Props = { + selectedLabels: TaskLabel[]; +}; + +const TaskDetailMetadataPreview: FC = ({ selectedLabels }) => { + const { t } = useTranslation("global"); + + return ( +
+
+
{t("taskmanager.task_detail.labels")}
+
+ {selectedLabels.map((label) => ( + + {label.title} + + ))} + +
+
+ +
+
{t("taskmanager.users")}
+
+
+ +
+ +
+
+
+ ); +}; + +export default TaskDetailMetadataPreview; diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx index 725cafe..029c52f 100644 --- a/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx +++ b/src/pages/taskmanager/components/task-detail/TaskDetailModal.tsx @@ -1,9 +1,9 @@ import { type FC, useEffect, useState } from "react"; import DefaulModal from "../../../../components/DefaulModal"; import type { Task } from "../../types"; -import TaskDetailActionBar from "./TaskDetailActionBar"; import TaskDetailDescription from "./TaskDetailDescription"; import TaskDetailHeader from "./TaskDetailHeader"; +import TaskDetailToolbar from "./TaskDetailToolbar"; import type { TaskDetailTab } from "./types"; type Props = { @@ -24,6 +24,10 @@ const TaskDetailModal: FC = ({ open, task, statusLabel, onClose }) => { } }, [open]); + const handleTabChange = (tab: TaskDetailTab) => { + setActiveTab((prev) => (prev === tab ? null : tab)); + }; + if (!task) return null; return ( @@ -33,7 +37,7 @@ const TaskDetailModal: FC = ({ open, task, statusLabel, onClose }) => {

{task.title}

- +
diff --git a/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx new file mode 100644 index 0000000..e65f3cc --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/TaskDetailToolbar.tsx @@ -0,0 +1,76 @@ +import { type FC, useState } from "react"; +import TaskDetailActionBar from "./TaskDetailActionBar"; +import TaskDetailMetadataPreview from "./TaskDetailMetadataPreview"; +import { DEFAULT_LABELS } from "./labels/constants"; +import TaskDetailLabelsPopover from "./labels/TaskDetailLabelsPopover"; +import type { LabelsView, TaskLabel } from "./labels/types"; +import type { TaskDetailTab } from "./types"; + +type Props = { + activeTab: TaskDetailTab | null; + onTabChange: (tab: TaskDetailTab) => void; +}; + +const TaskDetailToolbar: FC = ({ activeTab, onTabChange }) => { + const [labelsView, setLabelsView] = useState("list"); + const [labels, setLabels] = useState(DEFAULT_LABELS); + const [selectedIds, setSelectedIds] = useState>(new Set(["1"])); + + const selectedLabels = labels.filter((label) => selectedIds.has(label.id)); + const isLabelsOpen = activeTab === "labels"; + + const handleTabChange = (tab: TaskDetailTab) => { + if (tab === "labels" && activeTab !== "labels") { + setLabelsView("list"); + } + onTabChange(tab); + }; + + const handleToggle = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const handleCreate = (title: string, color: string) => { + const newLabel: TaskLabel = { + id: crypto.randomUUID(), + title, + color, + }; + setLabels((prev) => [...prev, newLabel]); + setSelectedIds((prev) => new Set([...prev, newLabel.id])); + setLabelsView("list"); + }; + + return ( +
+ + ) : null + } + /> + + +
+ ); +}; + +export default TaskDetailToolbar; diff --git a/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelCheckbox.tsx b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelCheckbox.tsx new file mode 100644 index 0000000..280a672 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelCheckbox.tsx @@ -0,0 +1,29 @@ +import { type FC } from "react"; +import { clx } from "../../../../../helpers/utils"; + +type Props = { + checked: boolean; + onToggle: () => void; + ariaLabel: string; +}; + +const TaskDetailLabelCheckbox: FC = ({ checked, onToggle, ariaLabel }) => { + return ( + + ); +}; + +export default TaskDetailLabelCheckbox; diff --git a/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsList.tsx b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsList.tsx new file mode 100644 index 0000000..ba0fc6f --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsList.tsx @@ -0,0 +1,53 @@ +import { AddCircle, Edit } from "iconsax-react"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import Button from "../../../../../components/Button"; +import TaskDetailLabelCheckbox from "./TaskDetailLabelCheckbox"; +import type { TaskLabel } from "./types"; + +type Props = { + labels: TaskLabel[]; + selectedIds: Set; + onToggle: (id: string) => void; + onEdit: (label: TaskLabel) => void; + onCreateClick: () => void; +}; + +const TaskDetailLabelsList: FC = ({ labels, selectedIds, onToggle, onEdit, onCreateClick }) => { + const { t } = useTranslation("global"); + + return ( +
+

{t("taskmanager.task_detail.labels")}

+ +
+ {labels.map((label) => { + const isSelected = selectedIds.has(label.id); + + return ( +
+ onToggle(label.id)} ariaLabel={t("taskmanager.task_detail.labels")} /> + +
+ {label.title && {label.title}} +
+ + onEdit(label)} size={16} color="#0047FF" /> +
+ ); + })} +
+ + +
+ ); +}; + +export default TaskDetailLabelsList; diff --git a/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx new file mode 100644 index 0000000..727797a --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/TaskDetailLabelsPopover.tsx @@ -0,0 +1,43 @@ +import { type FC } from "react"; +import TaskDetailLabelsList from "./TaskDetailLabelsList"; +import TaskDetailNewLabelForm from "./TaskDetailNewLabelForm"; +import type { LabelsView, TaskLabel } from "./types"; + +type Props = { + view: LabelsView; + labels: TaskLabel[]; + selectedIds: Set; + onViewChange: (view: LabelsView) => void; + onToggle: (id: string) => void; + onCreate: (title: string, color: string) => void; +}; + +const TaskDetailLabelsPopover: FC = ({ + view, + labels, + selectedIds, + onViewChange, + onToggle, + onCreate, +}) => { + return ( +
+ {view === "list" ? ( + undefined} + onCreateClick={() => onViewChange("create")} + /> + ) : ( + onViewChange("list")} + onSubmit={onCreate} + /> + )} +
+ ); +}; + +export default TaskDetailLabelsPopover; diff --git a/src/pages/taskmanager/components/task-detail/labels/TaskDetailNewLabelForm.tsx b/src/pages/taskmanager/components/task-detail/labels/TaskDetailNewLabelForm.tsx new file mode 100644 index 0000000..bfe9cdb --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/TaskDetailNewLabelForm.tsx @@ -0,0 +1,95 @@ +import { AddCircle, ArrowRight2 } from "iconsax-react"; +import { type FC, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import ColorsImage from "../../../../../assets/images/colors.png"; +import Button from "../../../../../components/Button"; +import Input from "../../../../../components/Input"; +import { clx } from "../../../../../helpers/utils"; +import { LABEL_COLOR_OPTIONS } from "./constants"; + +type Props = { + onBack: () => void; + onSubmit: (title: string, color: string) => void; +}; + +const TaskDetailNewLabelForm: FC = ({ onBack, onSubmit }) => { + const { t } = useTranslation("global"); + const colorInputRef = useRef(null); + const [title, setTitle] = useState(""); + const [selectedColor, setSelectedColor] = useState(LABEL_COLOR_OPTIONS[1]); + + const handleSubmit = () => { + const trimmed = title.trim(); + if (!trimmed) return; + onSubmit(trimmed, selectedColor); + }; + + return ( +
+
+ +

{t("taskmanager.task_detail.new_label")}

+
+ + setTitle(e.target.value)} + className="h-8 bg-white/63 border-white" + /> + + + +
+
{t("taskmanager.task_detail.choose_custom_color")}
+ +
+ + {LABEL_COLOR_OPTIONS.map((color) => ( +
+
+
+ ); +}; + +export default TaskDetailNewLabelForm; diff --git a/src/pages/taskmanager/components/task-detail/labels/constants.ts b/src/pages/taskmanager/components/task-detail/labels/constants.ts new file mode 100644 index 0000000..4dca17f --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/constants.ts @@ -0,0 +1,19 @@ +import type { TaskLabel } from "./types"; + +export const LABEL_COLOR_OPTIONS = [ + "#A8E6CF", + "#F5A3B8", + "#F5C4A3", + "#8B9A8B", + "#292D32", + "#E86A4A", + "#FFFFFF", +] as const; + +export const DEFAULT_LABELS: TaskLabel[] = [ + { id: "1", title: "عنوان", color: "#F9A8BB" }, + { id: "2", title: "", color: "#AED9F4" }, + { id: "3", title: "", color: "#B2E7CC" }, + { id: "4", title: "", color: "#F9E7A8" }, + { id: "5", title: "", color: "#F9C8A8" }, +]; diff --git a/src/pages/taskmanager/components/task-detail/labels/types.ts b/src/pages/taskmanager/components/task-detail/labels/types.ts new file mode 100644 index 0000000..e7d0477 --- /dev/null +++ b/src/pages/taskmanager/components/task-detail/labels/types.ts @@ -0,0 +1,7 @@ +export type TaskLabel = { + id: string; + title: string; + color: string; +}; + +export type LabelsView = "list" | "create";