detail modal task

This commit is contained in:
hamid zarghami
2026-06-16 16:31:49 +03:30
parent 8518662eee
commit c6559b3054
9 changed files with 194 additions and 5 deletions
+10 -1
View File
@@ -989,6 +989,15 @@
"upload_background": "تصویر مورد نظر را دراپ کنید", "upload_background": "تصویر مورد نظر را دراپ کنید",
"select_date": "انتخاب تاریخ", "select_date": "انتخاب تاریخ",
"projects": "پروژه‌ها", "projects": "پروژه‌ها",
"delete_project": "پاک کردن" "delete_project": "پاک کردن",
"task_detail": {
"labels": "برچسب ها",
"user_management": "مدیریت کاربران",
"checklist": "چک لیست",
"attachment": "ضمیمه",
"date_settings": "تنظیمات تاریخ",
"description": "توضیحات",
"description_placeholder": "توضیحات خود را اضافه کنید"
}
} }
} }
@@ -10,6 +10,7 @@ type Props = {
onDragStart: (taskId: string) => void; onDragStart: (taskId: string) => void;
onDragEnd: () => void; onDragEnd: () => void;
onDrop: (columnId: string, index: number) => void; onDrop: (columnId: string, index: number) => void;
onTaskClick?: (task: TaskType) => void;
}; };
const Column: FC<Props> = ({ const Column: FC<Props> = ({
@@ -19,6 +20,7 @@ const Column: FC<Props> = ({
onDragStart, onDragStart,
onDragEnd, onDragEnd,
onDrop, onDrop,
onTaskClick,
}) => { }) => {
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
@@ -72,6 +74,7 @@ const Column: FC<Props> = ({
isDragging={draggedTaskId === task.id} isDragging={draggedTaskId === task.id}
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onClick={onTaskClick}
/> />
</div> </div>
))} ))}
+21 -4
View File
@@ -1,5 +1,5 @@
import { AttachCircle, Calendar, TickSquare } from "iconsax-react"; import { AttachCircle, Calendar, TickSquare } from "iconsax-react";
import { type FC } from "react"; import { useRef, type FC } from "react";
import type { Task as TaskType } from "../types"; import type { Task as TaskType } from "../types";
type Props = { type Props = {
@@ -7,14 +7,31 @@ type Props = {
isDragging?: boolean; isDragging?: boolean;
onDragStart: (taskId: string) => void; onDragStart: (taskId: string) => void;
onDragEnd: () => void; onDragEnd: () => void;
onClick?: (task: TaskType) => void;
}; };
const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd }) => { const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick }) => {
const hasDraggedRef = useRef(false);
const handleClick = () => {
if (hasDraggedRef.current) return;
onClick?.(task);
};
return ( return (
<div <div
draggable draggable
onDragStart={() => onDragStart(task.id)} onDragStart={() => {
onDragEnd={onDragEnd} hasDraggedRef.current = true;
onDragStart(task.id);
}}
onDragEnd={() => {
onDragEnd();
requestAnimationFrame(() => {
hasDraggedRef.current = false;
});
}}
onClick={handleClick}
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity ${ className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity ${
isDragging ? "opacity-40" : "" isDragging ? "opacity-40" : ""
}`} }`}
@@ -0,0 +1,49 @@
import { Calendar, Paperclip2, Profile2User, Tag, TickSquare, type Icon } from "iconsax-react";
import { type FC } from "react";
import { useTranslation } from "react-i18next";
import type { TaskDetailTab } from "./types";
type TabItem = {
id: TaskDetailTab;
icon: Icon;
labelKey: string;
};
const TABS: TabItem[] = [
{ id: "labels", icon: Tag, labelKey: "taskmanager.task_detail.labels" },
{ id: "users", icon: Profile2User, labelKey: "taskmanager.task_detail.user_management" },
{ id: "checklist", icon: TickSquare, labelKey: "taskmanager.task_detail.checklist" },
{ id: "attachment", icon: Paperclip2, labelKey: "taskmanager.task_detail.attachment" },
{ id: "date", icon: Calendar, labelKey: "taskmanager.task_detail.date_settings" },
];
type Props = {
activeTab: TaskDetailTab | null;
onTabChange: (tab: TaskDetailTab) => void;
};
const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange }) => {
const { t } = useTranslation("global");
return (
<div className="flex flex-wrap items-center gap-2">
{TABS.map(({ id, icon: Icon, labelKey }) => {
const isActive = activeTab === id;
return (
<button
key={id}
type="button"
onClick={() => onTabChange(id)}
className={`flex items-center gap-1.5 border border-white rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-white text-black" : "bg-white/50 text-[#292D32]"}`}
>
<Icon size={16} color="#292D32" />
<span>{t(labelKey)}</span>
</button>
);
})}
</div>
);
};
export default TaskDetailActionBar;
@@ -0,0 +1,26 @@
import { type FC } from "react";
import { useTranslation } from "react-i18next";
type Props = {
value?: string;
onChange?: (value: string) => void;
};
const TaskDetailDescription: FC<Props> = ({ value = "", onChange }) => {
const { t } = useTranslation("global");
return (
<div className="mt-6">
<div className="text-sm font-medium mb-2">{t("taskmanager.task_detail.description")}</div>
<textarea
value={value}
onChange={(e) => onChange?.(e.target.value)}
placeholder={t("taskmanager.task_detail.description_placeholder")}
rows={5}
className="w-full bg-white/25 rounded-xl px-4 py-3 text-xs outline-none resize-none placeholder:text-description"
/>
</div>
);
};
export default TaskDetailDescription;
@@ -0,0 +1,26 @@
import { ArrowDown2, CloseCircle } from "iconsax-react";
import { type FC } from "react";
type Props = {
statusLabel: string;
onClose: () => void;
};
const TaskDetailHeader: FC<Props> = ({ statusLabel, onClose }) => {
return (
<div className="flex items-center justify-between border-b border-border pb-4">
<button type="button" className="flex items-center gap-1.5 bg-white/60 rounded-full px-4 py-2 text-xs font-medium cursor-pointer">
<span>{statusLabel}</span>
<ArrowDown2 size={14} color="#292D32" />
</button>
<div className="flex items-center gap-2">
<button type="button" onClick={onClose} className="size-8 rounded-full bg-white/60 flex items-center justify-center cursor-pointer" aria-label="بستن">
<CloseCircle size={20} color="#292D32" />
</button>
</div>
</div>
);
};
export default TaskDetailHeader;
@@ -0,0 +1,44 @@
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 type { TaskDetailTab } from "./types";
type Props = {
open: boolean;
task: Task | null;
statusLabel: string;
onClose: () => void;
};
const TaskDetailModal: FC<Props> = ({ open, task, statusLabel, onClose }) => {
const [activeTab, setActiveTab] = useState<TaskDetailTab | null>(null);
const [description, setDescription] = useState("");
useEffect(() => {
if (!open) {
setActiveTab(null);
setDescription("");
}
}, [open]);
if (!task) return null;
return (
<DefaulModal open={open} close={onClose} width={680}>
<TaskDetailHeader statusLabel={statusLabel} onClose={onClose} />
<h2 className="mt-6 text-sm font-bold">{task.title}</h2>
<div className="mt-4">
<TaskDetailActionBar activeTab={activeTab} onTabChange={setActiveTab} />
</div>
<TaskDetailDescription value={description} onChange={setDescription} />
</DefaulModal>
);
};
export default TaskDetailModal;
@@ -0,0 +1 @@
export type TaskDetailTab = "labels" | "users" | "checklist" | "attachment" | "date";
+14
View File
@@ -2,6 +2,7 @@ import { useMemo, useState, type FC } from "react";
import AddNewColumn from "./components/AddNewColumn"; import AddNewColumn from "./components/AddNewColumn";
import Column from "./components/Column"; import Column from "./components/Column";
import HeaderWorkspace from "./components/HeaderWorkspace"; import HeaderWorkspace from "./components/HeaderWorkspace";
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
import tasksData from "./data/tasks.json"; import tasksData from "./data/tasks.json";
import type { Task, WorkspaceData } from "./types"; import type { Task, WorkspaceData } from "./types";
import { reorderTasks } from "./utils/reorderTasks"; import { reorderTasks } from "./utils/reorderTasks";
@@ -10,6 +11,7 @@ const Workspace: FC = () => {
const { columns } = tasksData as WorkspaceData; const { columns } = tasksData as WorkspaceData;
const [tasks, setTasks] = useState<Task[]>(tasksData.tasks as Task[]); const [tasks, setTasks] = useState<Task[]>(tasksData.tasks as Task[]);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null); const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const tasksByColumn = useMemo(() => { const tasksByColumn = useMemo(() => {
return columns.reduce<Record<string, Task[]>>((acc, column) => { return columns.reduce<Record<string, Task[]>>((acc, column) => {
@@ -34,6 +36,10 @@ const Workspace: FC = () => {
setDraggedTaskId(null); setDraggedTaskId(null);
}; };
const selectedTaskColumn = selectedTask
? columns.find((column) => column.id === selectedTask.columnId)
: null;
return ( return (
<div className="bg-[#01347A] h-full rounded-[32px] overflow-hidden flex flex-col"> <div className="bg-[#01347A] h-full rounded-[32px] overflow-hidden flex flex-col">
<HeaderWorkspace /> <HeaderWorkspace />
@@ -47,11 +53,19 @@ const Workspace: FC = () => {
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDrop={handleDrop} onDrop={handleDrop}
onTaskClick={setSelectedTask}
/> />
))} ))}
<AddNewColumn /> <AddNewColumn />
</div> </div>
<TaskDetailModal
open={selectedTask !== null}
task={selectedTask}
statusLabel={selectedTaskColumn?.title ?? ""}
onClose={() => setSelectedTask(null)}
/>
</div> </div>
); );
}; };