fixes and more ui

This commit is contained in:
Desktop
2025-11-12 21:52:22 +05:00
parent fa47cfa7ff
commit 408b12b607
3 changed files with 302 additions and 8 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { PlusIcon, HorizontaLDots } from "../../icons";
import RelationshipMap from "./RelationshipMap";
export interface Task {
id: string;
@@ -15,6 +16,13 @@ export interface Task {
};
description?: string;
image?: string;
// Relationship data
clusterId?: number | null;
clusterName?: string | null;
ideaId?: number | null;
ideaTitle?: string | null;
keywordIds?: number[];
keywordNames?: string[];
}
interface KanbanBoardProps {
@@ -34,6 +42,8 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
}) => {
const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All");
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
const [draggedTask, setDraggedTask] = useState<Task | null>(null);
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
const filteredTasks = selectedFilter === "All"
? tasks
@@ -61,6 +71,32 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
setOpenDropdowns(prev => ({ ...prev, [id]: false }));
};
const handleDragStart = (e: React.DragEvent, task: Task) => {
setDraggedTask(task);
e.dataTransfer.effectAllowed = "move";
};
const handleDragOver = (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setDragOverColumn(status);
};
const handleDragLeave = () => {
setDragOverColumn(null);
};
const handleDrop = (e: React.DragEvent, targetStatus: "todo" | "in_progress" | "completed") => {
e.preventDefault();
setDragOverColumn(null);
if (draggedTask && draggedTask.status !== targetStatus) {
onTaskMove?.(draggedTask.id, targetStatus);
}
setDraggedTask(null);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
{/* Task header Start */}
@@ -180,6 +216,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["todo"] || false}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === "todo"}
/>
{/* Progress list */}
@@ -193,6 +234,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["in_progress"] || false}
borderX
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === "in_progress"}
/>
{/* Completed list */}
@@ -205,6 +251,11 @@ const KanbanBoard: React.FC<KanbanBoardProps> = ({
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["completed"] || false}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === "completed"}
/>
</div>
{/* Task wrapper End */}
@@ -222,6 +273,11 @@ interface KanbanColumnProps {
onDropdownClose: (id: string) => void;
openDropdown: boolean;
borderX?: boolean;
onDragStart: (e: React.DragEvent, task: Task) => void;
onDragOver: (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, status: "todo" | "in_progress" | "completed") => void;
isDragOver?: boolean;
}
const KanbanColumn: React.FC<KanbanColumnProps> = ({
@@ -234,6 +290,11 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
onDropdownClose,
openDropdown,
borderX = false,
onDragStart,
onDragOver,
onDragLeave,
onDrop,
isDragOver = false,
}) => {
const getCountBadgeClass = () => {
if (status === "in_progress") {
@@ -246,7 +307,14 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
};
return (
<div className={`swim-lane flex flex-col gap-5 p-4 xl:p-6 ${borderX ? "border-x border-gray-200 dark:border-gray-800" : ""}`}>
<div
className={`swim-lane flex flex-col gap-5 p-4 xl:p-6 ${borderX ? "border-x border-gray-200 dark:border-gray-800" : ""} ${
isDragOver ? "bg-brand-50/50 dark:bg-brand-500/10 border-brand-300 dark:border-brand-500/30" : ""
} transition-colors`}
onDragOver={(e) => onDragOver(e, status)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, status)}
>
<div className="mb-1 flex items-center justify-between">
<h3 className="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
{title}
@@ -285,7 +353,12 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
</div>
{tasks.map((task) => (
<TaskCard key={task.id} task={task} onClick={() => onTaskClick?.(task)} />
<TaskCard
key={task.id}
task={task}
onClick={() => onTaskClick?.(task)}
onDragStart={(e) => onDragStart(e, task)}
/>
))}
</div>
);
@@ -294,9 +367,10 @@ const KanbanColumn: React.FC<KanbanColumnProps> = ({
interface TaskCardProps {
task: Task;
onClick?: () => void;
onDragStart?: (e: React.DragEvent) => void;
}
const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
const TaskCard: React.FC<TaskCardProps> = ({ task, onClick, onDragStart }) => {
const getTagClass = (color?: string) => {
switch (color) {
case "brand":
@@ -315,8 +389,9 @@ const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
return (
<div
draggable
onDragStart={onDragStart}
onClick={onClick}
className="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5 cursor-pointer hover:shadow-md transition-shadow"
className="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5 cursor-move hover:shadow-md transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<div className="flex items-start justify-between gap-6">
<div className="flex-1">
@@ -377,6 +452,24 @@ const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
))}
</div>
)}
{/* Relationship Mapping */}
{(task.clusterId || task.ideaId || (task.keywordNames && task.keywordNames.length > 0)) && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-800">
<RelationshipMap
task={{
taskId: Number(task.id),
taskTitle: task.title,
clusterId: task.clusterId,
clusterName: task.clusterName,
ideaId: task.ideaId,
ideaTitle: task.ideaTitle,
keywordIds: task.keywordIds,
keywordNames: task.keywordNames,
}}
/>
</div>
)}
</div>
{task.assignee?.avatar && (

View File

@@ -0,0 +1,131 @@
import React from "react";
import { Link } from "react-router-dom";
import { ArrowRightIcon } from "../../icons";
export interface RelationshipData {
taskId: number;
taskTitle: string;
clusterId?: number | null;
clusterName?: string | null;
ideaId?: number | null;
ideaTitle?: string | null;
keywordIds?: number[];
keywordNames?: string[];
}
interface RelationshipMapProps {
task: RelationshipData;
onNavigate?: (type: "cluster" | "idea" | "keyword", id: number) => void;
className?: string;
}
const RelationshipMap: React.FC<RelationshipMapProps> = ({
task,
onNavigate,
className = "",
}) => {
const hasRelationships = task.clusterId || task.ideaId || (task.keywordIds && task.keywordIds.length > 0);
if (!hasRelationships) {
return (
<div className={`text-xs text-gray-500 dark:text-gray-400 ${className}`}>
No relationships mapped
</div>
);
}
return (
<div className={`space-y-2 ${className}`}>
{/* Keywords → Cluster → Idea → Task Flow */}
<div className="flex flex-wrap items-center gap-2 text-xs">
{/* Keywords */}
{task.keywordIds && task.keywordIds.length > 0 && (
<>
<div className="flex flex-wrap items-center gap-1">
{task.keywordNames?.slice(0, 3).map((keyword, idx) => (
<button
key={idx}
onClick={() => onNavigate?.("keyword", task.keywordIds![idx])}
className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
{keyword}
</button>
))}
{task.keywordIds.length > 3 && (
<span className="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
+{task.keywordIds.length - 3}
</span>
)}
</div>
{task.clusterId && <ArrowRightIcon className="size-3 text-gray-400" />}
</>
)}
{/* Cluster */}
{task.clusterId && (
<>
<Link
to={`/planner/clusters?highlight=${task.clusterId}`}
onClick={(e) => {
if (onNavigate) {
e.preventDefault();
onNavigate("cluster", task.clusterId!);
}
}}
className="px-2 py-0.5 rounded bg-brand-50 dark:bg-brand-500/15 text-brand-600 dark:text-brand-400 hover:bg-brand-100 dark:hover:bg-brand-500/25 transition-colors font-medium"
>
{task.clusterName || `Cluster #${task.clusterId}`}
</Link>
{task.ideaId && <ArrowRightIcon className="size-3 text-gray-400" />}
</>
)}
{/* Idea */}
{task.ideaId && (
<>
<Link
to={`/planner/ideas?highlight=${task.ideaId}`}
onClick={(e) => {
if (onNavigate) {
e.preventDefault();
onNavigate("idea", task.ideaId!);
}
}}
className="px-2 py-0.5 rounded bg-success-50 dark:bg-success-500/15 text-success-600 dark:text-success-400 hover:bg-success-100 dark:hover:bg-success-500/25 transition-colors font-medium"
>
{task.ideaTitle || `Idea #${task.ideaId}`}
</Link>
<ArrowRightIcon className="size-3 text-gray-400" />
</>
)}
{/* Task (current) */}
<span className="px-2 py-0.5 rounded bg-warning-50 dark:bg-warning-500/15 text-warning-600 dark:text-warning-400 font-medium">
Task
</span>
</div>
{/* Relationship Summary */}
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 pt-1 border-t border-gray-200 dark:border-gray-800">
{task.clusterId && (
<span>
Cluster: <span className="font-medium text-gray-700 dark:text-gray-300">{task.clusterName || `#${task.clusterId}`}</span>
</span>
)}
{task.ideaId && (
<span>
Idea: <span className="font-medium text-gray-700 dark:text-gray-300">{task.ideaTitle || `#${task.ideaId}`}</span>
</span>
)}
{task.keywordIds && task.keywordIds.length > 0 && (
<span>
Keywords: <span className="font-medium text-gray-700 dark:text-gray-300">{task.keywordIds.length}</span>
</span>
)}
</div>
</div>
);
};
export default RelationshipMap;