Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3153c6793 | |||
| f836abd8f4 |
@@ -3,7 +3,7 @@ import { type FC } from "react";
|
|||||||
|
|
||||||
const AddNewColumn: FC = () => {
|
const AddNewColumn: FC = () => {
|
||||||
return (
|
return (
|
||||||
<button type="button" className="bg-[#F0F3F7BF] bg-opacity-75 rounded-full px-6 py-4 flex items-center gap-2 shrink-0 self-start cursor-pointer">
|
<button type="button" className="bg-[#F0F3F7BF] bg-opacity-75 rounded-full px-4 sm:px-6 py-3 sm:py-4 flex items-center gap-2 shrink-0 self-start cursor-pointer w-[272px] sm:w-[288px] xl:w-[310px] justify-center">
|
||||||
<AddCircle size={16} color="black" />
|
<AddCircle size={16} color="black" />
|
||||||
<span className="text-xs text-black">اضافه کردن ستون جدید</span>
|
<span className="text-xs text-black">اضافه کردن ستون جدید</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Add, AddCircle, CloseCircle, More } from "iconsax-react";
|
import { Add, AddCircle, CloseCircle, More } from "iconsax-react";
|
||||||
import { useState, type FC } from "react";
|
import { useRef, useState, type FC } from "react";
|
||||||
import type { Column as ColumnType, Task as TaskType } from "../types";
|
import type { Column as ColumnType, Task as TaskType } from "../types";
|
||||||
import Task from "./Task";
|
import Task from "./Task";
|
||||||
|
|
||||||
@@ -7,9 +7,13 @@ type Props = {
|
|||||||
column: ColumnType;
|
column: ColumnType;
|
||||||
tasks: TaskType[];
|
tasks: TaskType[];
|
||||||
draggedTaskId: string | null;
|
draggedTaskId: string | null;
|
||||||
|
draggedColumnId: string | null;
|
||||||
onDragStart: (taskId: string) => void;
|
onDragStart: (taskId: string) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
onDrop: (columnId: string, index: number) => void;
|
onDrop: (columnId: string, index: number) => void;
|
||||||
|
onColumnDragStart: (columnId: string) => void;
|
||||||
|
onColumnDragEnd: () => void;
|
||||||
|
onColumnDrop: (overColumnId: string) => void;
|
||||||
onTaskClick?: (task: TaskType) => void;
|
onTaskClick?: (task: TaskType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,72 +21,158 @@ const Column: FC<Props> = ({
|
|||||||
column,
|
column,
|
||||||
tasks,
|
tasks,
|
||||||
draggedTaskId,
|
draggedTaskId,
|
||||||
|
draggedColumnId,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDrop,
|
onDrop,
|
||||||
|
onColumnDragStart,
|
||||||
|
onColumnDragEnd,
|
||||||
|
onColumnDrop,
|
||||||
onTaskClick,
|
onTaskClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
||||||
|
const columnRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dragImageRef = useRef<HTMLElement | null>(null);
|
||||||
|
const isDraggingColumn = draggedColumnId === column.id;
|
||||||
|
|
||||||
|
const handleColumnDragStart = (e: React.DragEvent) => {
|
||||||
|
const columnEl = columnRef.current;
|
||||||
|
if (columnEl) {
|
||||||
|
const clone = columnEl.cloneNode(true) as HTMLElement;
|
||||||
|
clone.style.position = "fixed";
|
||||||
|
clone.style.top = "-10000px";
|
||||||
|
clone.style.left = "-10000px";
|
||||||
|
clone.style.width = `${columnEl.offsetWidth}px`;
|
||||||
|
clone.style.opacity = "0.95";
|
||||||
|
clone.style.boxShadow = "0 8px 24px rgba(0, 0, 0, 0.15)";
|
||||||
|
clone.style.pointerEvents = "none";
|
||||||
|
clone.style.zIndex = "9999";
|
||||||
|
document.body.appendChild(clone);
|
||||||
|
dragImageRef.current = clone;
|
||||||
|
|
||||||
|
const rect = columnEl.getBoundingClientRect();
|
||||||
|
e.dataTransfer.setDragImage(
|
||||||
|
clone,
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top,
|
||||||
|
);
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
}
|
||||||
|
|
||||||
|
onColumnDragStart(column.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDragEnd = () => {
|
||||||
|
if (dragImageRef.current) {
|
||||||
|
document.body.removeChild(dragImageRef.current);
|
||||||
|
dragImageRef.current = null;
|
||||||
|
}
|
||||||
|
onColumnDragEnd();
|
||||||
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
if (draggedColumnId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = "move";
|
e.dataTransfer.dropEffect = "move";
|
||||||
setIsDragOver(true);
|
setIsDragOver(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
if (draggedColumnId) return;
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent, index: number) => {
|
const handleDrop = (e: React.DragEvent, index: number) => {
|
||||||
|
if (draggedColumnId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
onDrop(column.id, index);
|
onDrop(column.id, index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleColumnDragOver = (e: React.DragEvent) => {
|
||||||
|
if (!draggedColumnId || draggedColumnId === column.id) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
setIsColumnDragOver(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDragLeave = (e: React.DragEvent) => {
|
||||||
|
if (!draggedColumnId) return;
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||||
|
setIsColumnDragOver(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDrop = (e: React.DragEvent) => {
|
||||||
|
if (!draggedColumnId || draggedColumnId === column.id) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsColumnDragOver(false);
|
||||||
|
onColumnDrop(column.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-[#F0F3F7] rounded-xl p-6 max-w-[310px] w-full flex flex-col self-start transition-colors ${
|
ref={columnRef}
|
||||||
|
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col max-h-[calc(100dvh-140px)] sm:max-h-[calc(100dvh-152px)] xl:max-h-[calc(100dvh-220px)] transition-colors ${
|
||||||
isDragOver ? "ring-2 ring-primary/40" : ""
|
isDragOver ? "ring-2 ring-primary/40" : ""
|
||||||
|
} ${isColumnDragOver ? "ring-2 ring-black/30" : ""} ${
|
||||||
|
isDraggingColumn ? "opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={(e) => {
|
||||||
onDragLeave={handleDragLeave}
|
handleColumnDragOver(e);
|
||||||
onDrop={(e) => handleDrop(e, tasks.length)}
|
handleDragOver(e);
|
||||||
|
}}
|
||||||
|
onDragLeave={(e) => {
|
||||||
|
handleColumnDragLeave(e);
|
||||||
|
handleDragLeave(e);
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
handleColumnDrop(e);
|
||||||
|
if (!draggedColumnId) {
|
||||||
|
handleDrop(e, tasks.length);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center shrink-0">
|
<div
|
||||||
<div className="font-bold text-sm">{column.title}</div>
|
draggable
|
||||||
<More size={20} color="black" />
|
onDragStart={handleColumnDragStart}
|
||||||
|
onDragEnd={handleColumnDragEnd}
|
||||||
|
className="flex justify-between items-center shrink-0 cursor-grab active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<div className="font-bold text-sm truncate">{column.title}</div>
|
||||||
|
<More size={20} color="black" className="shrink-0 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tasks.length > 0 && (
|
<div className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4">
|
||||||
<div className="mt-5 flex flex-col gap-4">
|
{tasks.map((task, index) => (
|
||||||
{tasks.map((task, index) => (
|
<div
|
||||||
<div
|
key={task.id}
|
||||||
key={task.id}
|
onDragOver={(e) => {
|
||||||
onDragOver={(e) => {
|
if (draggedColumnId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => handleDrop(e, index)}
|
onDrop={(e) => handleDrop(e, index)}
|
||||||
>
|
>
|
||||||
<Task
|
<Task
|
||||||
task={task}
|
task={task}
|
||||||
isDragging={draggedTaskId === task.id}
|
isDragging={draggedTaskId === task.id}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onClick={onTaskClick}
|
onClick={onTaskClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{isAdding ? (
|
{isAdding ? (
|
||||||
<div className="mt-5 shrink-0 flex flex-col gap-3">
|
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="عنوان تسک"
|
placeholder="عنوان تسک"
|
||||||
@@ -111,7 +201,7 @@ const Column: FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsAdding(true)}
|
onClick={() => setIsAdding(true)}
|
||||||
className="mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
<AddCircle size={18} color="#888888" />
|
<AddCircle size={18} color="#888888" />
|
||||||
<span className="text-description text-[13px] mt-0.5">اضافه کردن تسک</span>
|
<span className="text-description text-[13px] mt-0.5">اضافه کردن تسک</span>
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import { InfoCircle, Setting2, UserAdd } from "iconsax-react";
|
|||||||
|
|
||||||
const HeaderWorkspace = () => {
|
const HeaderWorkspace = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center bg-primary h-[102px] px-8">
|
<div className="flex justify-between items-center bg-primary h-14 sm:h-16 xl:h-[102px] px-3 sm:px-6 xl:px-8 shrink-0 gap-3">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-2 sm:gap-4 items-center min-w-0">
|
||||||
<div className="text-white text-xl">طراحی Dpage</div>
|
<div className="text-white text-base sm:text-lg xl:text-xl truncate">طراحی Dpage</div>
|
||||||
<InfoCircle size={20} color="white" />
|
<InfoCircle size={18} color="white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-2 sm:gap-4 items-center shrink-0">
|
||||||
<div className="flex gap-1 items-center px-2 rounded-lg bg-white h-8">
|
<div className="flex gap-1 items-center px-2 sm:px-3 rounded-lg bg-white h-8">
|
||||||
<UserAdd size={18} color="black" />
|
<UserAdd size={18} color="black" />
|
||||||
<div className="text-black text-xs">اشتراک گذاری</div>
|
<div className="text-black text-xs hidden sm:block">اشتراک گذاری</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Setting2 size={20} color="white" />
|
<Setting2 size={18} color="white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,17 +41,17 @@ const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd, onClick })
|
|||||||
{task.tag}
|
{task.tag}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold text-xs mt-2">{task.title}</div>
|
<div className="font-bold text-xs mt-2">{task.title}</div>
|
||||||
<div className="mt-2 flex items-center gap-4 text-xs">
|
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs">
|
||||||
<div className="flex items-center gap-1 text-[10px]">
|
<div className="flex items-center gap-1 text-[10px] min-w-0">
|
||||||
<Calendar size={16} color="#292D32" />
|
<Calendar size={16} color="#292D32" className="shrink-0" />
|
||||||
<div>{task.dateRange}</div>
|
<div className="truncate">{task.dateRange}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1 shrink-0">
|
||||||
<AttachCircle size={16} color="#292D32" />
|
<AttachCircle size={16} color="#292D32" />
|
||||||
<div>{task.attachments}</div>
|
<div>{task.attachments}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1 shrink-0">
|
||||||
<TickSquare size={16} color="#292D32" />
|
<TickSquare size={16} color="#292D32" />
|
||||||
<div>
|
<div>
|
||||||
{task.checklistDone}/{task.checklistTotal}
|
{task.checklistDone}/{task.checklistTotal}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const TaskDetailActionBar: FC<Props> = ({ activeTab, onTabChange, labelsPopover,
|
|||||||
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
|
className={`flex items-center gap-1.5 border rounded-lg px-3 py-2 text-[11px] cursor-pointer transition-colors ${isActive ? "bg-[#4A4A4A] text-white border-transparent" : "bg-white/50 text-[#292D32] border-white"}`}
|
||||||
>
|
>
|
||||||
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
|
<Icon size={16} color={isActive ? "#FFFFFF" : "#292D32"} />
|
||||||
<span>{t(labelKey)}</span>
|
<span className="hidden sm:inline">{t(labelKey)}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers, s
|
|||||||
const dateLabel = formatDateRange(startDate, endDate);
|
const dateLabel = formatDateRange(startDate, endDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-7 flex gap-10">
|
<div className="mt-5 sm:mt-7 grid grid-cols-[auto_auto] gap-x-6 gap-y-4 sm:flex sm:flex-wrap sm:gap-x-10 sm:gap-y-4">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.labels")}</div>
|
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.labels")}</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{selectedLabels.map((label) => (
|
{selectedLabels.map((label) => (
|
||||||
@@ -34,7 +34,7 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers, s
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="size-7 rounded-lg border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer"
|
className="size-7 rounded-lg border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer shrink-0"
|
||||||
aria-label={t("taskmanager.task_detail.new_label")}
|
aria-label={t("taskmanager.task_detail.new_label")}
|
||||||
>
|
>
|
||||||
<Add
|
<Add
|
||||||
@@ -45,13 +45,13 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers, s
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="text-xs font-medium mb-2">{t("taskmanager.users")}</div>
|
<div className="text-xs font-medium mb-2">{t("taskmanager.users")}</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{selectedUsers.map((user) => (
|
{selectedUsers.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="size-8 rounded-full bg-[#D0D0D0] overflow-hidden"
|
className="size-8 rounded-full bg-[#D0D0D0] overflow-hidden shrink-0"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={user.avatar ?? AvatarImage}
|
src={user.avatar ?? AvatarImage}
|
||||||
@@ -62,7 +62,7 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers, s
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="size-7 rounded-full border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer"
|
className="size-7 rounded-full border border-[#D0D0D0] bg-white/60 flex items-center justify-center cursor-pointer shrink-0"
|
||||||
aria-label={t("taskmanager.users")}
|
aria-label={t("taskmanager.users")}
|
||||||
>
|
>
|
||||||
<Add
|
<Add
|
||||||
@@ -73,9 +73,13 @@ const TaskDetailMetadataPreview: FC<Props> = ({ selectedLabels, selectedUsers, s
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="col-span-2 sm:col-span-1 min-w-0">
|
||||||
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.date")}</div>
|
<div className="text-xs font-medium mb-2">{t("taskmanager.task_detail.date")}</div>
|
||||||
{dateLabel ? <span className="inline-flex items-center h-7 px-3 rounded-lg text-xs font-medium bg-[#E8E8E8] text-[#292D32]">{dateLabel}</span> : null}
|
{dateLabel ? (
|
||||||
|
<span className="inline-flex items-center min-h-7 px-3 py-1 rounded-lg text-xs font-medium bg-[#E8E8E8] text-[#292D32] whitespace-nowrap">
|
||||||
|
{dateLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Column } from "../types";
|
||||||
|
|
||||||
|
export const reorderColumns = (
|
||||||
|
columns: Column[],
|
||||||
|
activeId: string,
|
||||||
|
overId: string,
|
||||||
|
): Column[] => {
|
||||||
|
const oldIndex = columns.findIndex((column) => column.id === activeId);
|
||||||
|
const newIndex = columns.findIndex((column) => column.id === overId);
|
||||||
|
|
||||||
|
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [...columns];
|
||||||
|
const [removed] = result.splice(oldIndex, 1);
|
||||||
|
result.splice(newIndex, 0, removed);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -5,12 +5,16 @@ import HeaderWorkspace from "./components/HeaderWorkspace";
|
|||||||
import TaskDetailModal from "./components/task-detail/TaskDetailModal";
|
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 { reorderColumns } from "./utils/reorderColumns";
|
||||||
import { reorderTasks } from "./utils/reorderTasks";
|
import { reorderTasks } from "./utils/reorderTasks";
|
||||||
|
|
||||||
const Workspace: FC = () => {
|
const Workspace: FC = () => {
|
||||||
const { columns } = tasksData as WorkspaceData;
|
const [columns, setColumns] = useState(
|
||||||
|
(tasksData as WorkspaceData).columns,
|
||||||
|
);
|
||||||
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 [draggedColumnId, setDraggedColumnId] = useState<string | null>(null);
|
||||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||||
|
|
||||||
const tasksByColumn = useMemo(() => {
|
const tasksByColumn = useMemo(() => {
|
||||||
@@ -36,28 +40,48 @@ const Workspace: FC = () => {
|
|||||||
setDraggedTaskId(null);
|
setDraggedTaskId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleColumnDragStart = (columnId: string) => {
|
||||||
|
setDraggedColumnId(columnId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDragEnd = () => {
|
||||||
|
setDraggedColumnId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDrop = (overColumnId: string) => {
|
||||||
|
if (!draggedColumnId || draggedColumnId === overColumnId) return;
|
||||||
|
setColumns((prev) => reorderColumns(prev, draggedColumnId, overColumnId));
|
||||||
|
setDraggedColumnId(null);
|
||||||
|
};
|
||||||
|
|
||||||
const selectedTaskColumn = selectedTask
|
const selectedTaskColumn = selectedTask
|
||||||
? columns.find((column) => column.id === selectedTask.columnId)
|
? columns.find((column) => column.id === selectedTask.columnId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#01347A] h-full rounded-[32px] overflow-hidden flex flex-col">
|
<div className="bg-[#01347A] h-full rounded-none xl:rounded-[32px] overflow-hidden flex flex-col">
|
||||||
<HeaderWorkspace />
|
<HeaderWorkspace />
|
||||||
<div className="px-8 flex gap-7 mt-6 flex-1 min-h-0 overflow-auto pb-6 items-start">
|
<div className="flex-1 min-h-0 overflow-x-auto overflow-y-hidden overscroll-x-contain">
|
||||||
{columns.map((column) => (
|
<div className="flex gap-3 sm:gap-4 xl:gap-7 px-3 sm:px-4 xl:px-8 pt-4 xl:pt-6 pb-4 xl:pb-6 h-full items-start w-max min-w-full">
|
||||||
<Column
|
{columns.map((column) => (
|
||||||
key={column.id}
|
<Column
|
||||||
column={column}
|
key={column.id}
|
||||||
tasks={tasksByColumn[column.id] ?? []}
|
column={column}
|
||||||
draggedTaskId={draggedTaskId}
|
tasks={tasksByColumn[column.id] ?? []}
|
||||||
onDragStart={handleDragStart}
|
draggedTaskId={draggedTaskId}
|
||||||
onDragEnd={handleDragEnd}
|
draggedColumnId={draggedColumnId}
|
||||||
onDrop={handleDrop}
|
onDragStart={handleDragStart}
|
||||||
onTaskClick={setSelectedTask}
|
onDragEnd={handleDragEnd}
|
||||||
/>
|
onDrop={handleDrop}
|
||||||
))}
|
onColumnDragStart={handleColumnDragStart}
|
||||||
|
onColumnDragEnd={handleColumnDragEnd}
|
||||||
|
onColumnDrop={handleColumnDrop}
|
||||||
|
onTaskClick={setSelectedTask}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<AddNewColumn />
|
<AddNewColumn />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
|
|||||||
+10
-5
@@ -106,12 +106,17 @@ const MainRouter: FC = () => {
|
|||||||
const isWorkspace = pathname.startsWith(Pages.taskmanager.workspace);
|
const isWorkspace = pathname.startsWith(Pages.taskmanager.workspace);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 overflow-hidden">
|
<div className={clx("overflow-hidden", isWorkspace ? "p-0 xl:p-4" : "p-4")}>
|
||||||
{!isWorkspace && <SideBar />}
|
{!isWorkspace && <SideBar />}
|
||||||
<Header />
|
<Header />
|
||||||
<div className={clx("flex-1 mt-[68px] xl:mt-[81px]", !isWorkspace && "xl:ms-[269px]", !isWorkspace && hasSubMenu && "xl:ms-[305px]")}>
|
<div className={clx("flex-1 mt-[68px] xl:mt-[81px]", !isWorkspace && "xl:ms-[269px]", !isWorkspace && hasSubMenu && "xl:ms-[305px]")}>
|
||||||
<div className={clx(`overflow-auto w-[${window.innerWidth}] h-[calc(100vh-113px)] flex flex-col`)}>
|
<div
|
||||||
<div className="flex-1 min-h-0">
|
className={clx(
|
||||||
|
"flex flex-col",
|
||||||
|
isWorkspace ? "h-[calc(100dvh-68px)] xl:h-[calc(100dvh-97px)] overflow-hidden" : "overflow-auto h-[calc(100vh-113px)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={clx("flex-1 min-h-0", isWorkspace && "overflow-hidden")}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Pages.dashboard} element={<Home />} />
|
<Route path={Pages.dashboard} element={<Home />} />
|
||||||
<Route path={Pages.services.mine} element={<MyServices />} />
|
<Route path={Pages.services.mine} element={<MyServices />} />
|
||||||
@@ -215,10 +220,10 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.taskmanager.projectList} element={<ProjectList />} />
|
<Route path={Pages.taskmanager.projectList} element={<ProjectList />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-20 shrink-0 xl:hidden" />
|
{!isWorkspace && <div className="h-20 shrink-0 xl:hidden" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
{!isWorkspace && <Footer />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user