detail modal task
This commit is contained in:
+10
-1
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user