enhanced ui

This commit is contained in:
Desktop
2025-11-12 21:37:41 +05:00
parent 9692a5ed2e
commit fa47cfa7ff
12 changed files with 2341 additions and 428 deletions

View File

@@ -0,0 +1,45 @@
import React from "react";
import { GridIcon, ListIcon, TableIcon } from "../../icons";
export type ViewType = "table" | "kanban" | "list";
interface ViewToggleProps {
currentView: ViewType;
onViewChange: (view: ViewType) => void;
className?: string;
}
const ViewToggle: React.FC<ViewToggleProps> = ({
currentView,
onViewChange,
className = "",
}) => {
const views: { type: ViewType; icon: React.ReactNode; label: string }[] = [
{ type: "table", icon: <TableIcon className="size-4" />, label: "Table" },
{ type: "kanban", icon: <GridIcon className="size-4" />, label: "Kanban" },
{ type: "list", icon: <ListIcon className="size-4" />, label: "List" },
];
return (
<div className={`inline-flex items-center gap-1 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900 ${className}`}>
{views.map((view) => (
<button
key={view.type}
onClick={() => onViewChange(view.type)}
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
currentView === view.type
? "bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm"
: "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
}`}
title={view.label}
>
{view.icon}
<span className="hidden sm:inline">{view.label}</span>
</button>
))}
</div>
);
};
export default ViewToggle;

View File

@@ -0,0 +1,169 @@
import { ReactNode, useState } from "react";
import { Link } from "react-router";
import { ArrowUpIcon, ArrowDownIcon } from "../../icons";
import { EnhancedTooltip } from "../ui/tooltip/EnhancedTooltip";
export interface MetricCardProps {
title: string;
value: string | number;
subtitle?: string;
trend?: number; // Positive or negative trend value
icon: ReactNode;
accentColor: "blue" | "green" | "orange" | "purple" | "red";
href?: string;
onClick?: () => void;
tooltip?: string | ReactNode; // Enhanced tooltip content
details?: Array<{ label: string; value: string | number }>; // Breakdown for tooltip
className?: string;
}
const accentColors = {
blue: {
bg: "bg-blue-50 dark:bg-blue-500/10",
hover: "hover:bg-blue-100 dark:hover:bg-blue-500/20",
border: "bg-brand-500",
icon: "text-brand-500",
},
green: {
bg: "bg-green-50 dark:bg-green-500/10",
hover: "hover:bg-green-100 dark:hover:bg-green-500/20",
border: "bg-success-500",
icon: "text-success-500",
},
orange: {
bg: "bg-amber-50 dark:bg-amber-500/10",
hover: "hover:bg-amber-100 dark:hover:bg-amber-500/20",
border: "bg-warning-500",
icon: "text-warning-500",
},
purple: {
bg: "bg-purple-50 dark:bg-purple-500/10",
hover: "hover:bg-purple-100 dark:hover:bg-purple-500/20",
border: "bg-purple-500",
icon: "text-purple-500",
},
red: {
bg: "bg-red-50 dark:bg-red-500/10",
hover: "hover:bg-red-100 dark:hover:bg-red-500/20",
border: "bg-error-500",
icon: "text-error-500",
},
};
export default function EnhancedMetricCard({
title,
value,
subtitle,
trend,
icon,
accentColor,
href,
onClick,
tooltip,
details,
className = "",
}: MetricCardProps) {
const [isHovered, setIsHovered] = useState(false);
const colors = accentColors[accentColor];
const formatValue = (val: string | number): string => {
if (typeof val === "number") {
return val.toLocaleString();
}
return val;
};
const tooltipContent = tooltip || (
details ? (
<div className="space-y-1">
{details.map((detail, idx) => (
<div key={idx} className="flex justify-between gap-4 text-xs">
<span className="text-gray-300">{detail.label}:</span>
<span className="font-medium text-white">{detail.value}</span>
</div>
))}
</div>
) : null
);
const cardContent = (
<div
className={`
rounded-2xl border border-gray-200 bg-white p-5
dark:border-gray-800 dark:bg-white/[0.03] md:p-6
hover:shadow-lg transition-all cursor-pointer group
relative overflow-hidden
${className}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
>
{/* Accent Border */}
<div className={`absolute left-0 top-0 bottom-0 w-1 ${colors.border}`} />
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">{title}</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{formatValue(value)}
</h4>
{trend !== undefined && trend !== 0 && (
<div
className={`flex items-center gap-1 text-xs ${
trend > 0 ? "text-success-500" : "text-error-500"
}`}
>
{trend > 0 ? (
<ArrowUpIcon className="size-3" />
) : (
<ArrowDownIcon className="size-3" />
)}
<span>{Math.abs(trend)}</span>
</div>
)}
</div>
{subtitle && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{subtitle}
</p>
)}
</div>
<div
className={`flex items-center justify-center w-12 h-12 rounded-xl ${colors.bg} ${colors.hover} transition-colors`}
>
<div className={colors.icon}>{icon}</div>
</div>
</div>
{/* Hover Effect */}
{isHovered && tooltipContent && (
<div className="absolute inset-0 bg-black/5 dark:bg-white/5 rounded-2xl" />
)}
</div>
);
// Wrap with tooltip if provided
if (tooltipContent) {
const wrappedContent = href ? (
<Link to={href} className="block">
{cardContent}
</Link>
) : (
cardContent
);
return (
<EnhancedTooltip
content={tooltipContent}
placement="top"
>
{wrappedContent}
</EnhancedTooltip>
);
}
return href ? <Link to={href}>{cardContent}</Link> : cardContent;
}

View File

@@ -0,0 +1,150 @@
import { ReactNode } from "react";
import { Link } from "react-router";
import { CheckCircleIcon, TimeIcon, ArrowRightIcon } from "../../icons";
import { Tooltip } from "../ui/tooltip";
export interface WorkflowStep {
number: number;
title: string;
status: "completed" | "in_progress" | "pending";
count: number;
path: string;
description?: string;
details?: string; // Additional details for tooltip
}
interface WorkflowPipelineProps {
steps: WorkflowStep[];
onStepClick?: (step: WorkflowStep) => void;
showConnections?: boolean;
className?: string;
}
export default function WorkflowPipeline({
steps,
onStepClick,
showConnections = true,
className = "",
}: WorkflowPipelineProps) {
const getStatusColor = (status: WorkflowStep["status"]) => {
switch (status) {
case "completed":
return "bg-success-500 border-success-500 text-white";
case "in_progress":
return "bg-warning-500 border-warning-500 text-white";
case "pending":
return "bg-gray-200 border-gray-300 text-gray-600 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400";
}
};
const getStatusIcon = (status: WorkflowStep["status"], stepNumber: number) => {
switch (status) {
case "completed":
return <CheckCircleIcon className="size-5" />;
case "in_progress":
return <TimeIcon className="size-5" />;
case "pending":
return <span className="text-sm font-bold">{stepNumber}</span>;
}
};
const getConnectionColor = (currentStatus: WorkflowStep["status"], nextStatus?: WorkflowStep["status"]) => {
if (currentStatus === "completed" && nextStatus === "completed") {
return "bg-success-500";
}
if (currentStatus === "completed") {
return "bg-success-500";
}
return "bg-gray-300 dark:bg-gray-600";
};
return (
<div className={`relative ${className}`}>
<div className="flex items-center justify-between gap-4 overflow-x-auto pb-4">
{steps.map((step, index) => {
const isLast = index === steps.length - 1;
const nextStep = !isLast ? steps[index + 1] : null;
return (
<div key={step.number} className="flex items-center flex-shrink-0">
{/* Step Node */}
<div className="flex flex-col items-center">
<Tooltip
text={
step.details ||
`${step.title}: ${step.count} ${step.title.toLowerCase().includes('keyword') ? 'keywords' :
step.title.toLowerCase().includes('cluster') ? 'clusters' :
step.title.toLowerCase().includes('idea') ? 'ideas' :
step.title.toLowerCase().includes('task') ? 'tasks' : 'items'}`
}
placement="top"
>
<Link
to={step.path}
onClick={(e) => {
if (onStepClick) {
e.preventDefault();
onStepClick(step);
}
}}
className={`
relative flex flex-col items-center justify-center
w-20 h-20 rounded-full border-2 transition-all duration-300
${getStatusColor(step.status)}
hover:scale-110 hover:shadow-lg
cursor-pointer group
`}
>
<div className="flex items-center justify-center">
{getStatusIcon(step.status, step.number)}
</div>
{step.status === "in_progress" && (
<div className="absolute inset-0 rounded-full border-2 border-warning-400 animate-ping opacity-75" />
)}
</Link>
</Tooltip>
{/* Step Label */}
<div className="mt-3 text-center max-w-[120px]">
<p className="text-xs font-semibold text-gray-700 dark:text-gray-300">
{step.title}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{step.count} {step.title.toLowerCase().includes('keyword') ? 'keywords' :
step.title.toLowerCase().includes('cluster') ? 'clusters' :
step.title.toLowerCase().includes('idea') ? 'ideas' :
step.title.toLowerCase().includes('task') ? 'tasks' : 'items'}
</p>
{step.status === "pending" && (
<Link
to={step.path}
className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-brand-500 hover:text-brand-600 group-hover:translate-x-1 transition-transform"
onClick={(e) => {
if (onStepClick) {
e.preventDefault();
onStepClick(step);
}
}}
>
Start Now <ArrowRightIcon className="size-3" />
</Link>
)}
</div>
</div>
{/* Connection Line */}
{!isLast && showConnections && (
<div className="flex items-center mx-2 flex-shrink-0">
<div className={`h-0.5 w-16 ${getConnectionColor(step.status, nextStep?.status)} transition-colors duration-300`} />
<ArrowRightIcon className={`size-4 ${step.status === "completed" ? "text-success-500" : "text-gray-400"} transition-colors duration-300`} />
<div className={`h-0.5 w-16 ${getConnectionColor(step.status, nextStep?.status)} transition-colors duration-300`} />
</div>
)}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,546 @@
<div x-data="{selectedTaskGroup: 'All'}" class="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<!-- Task header Start -->
<div class="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
<div class="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
<div class="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md h group hover:text-gray-900 dark:hover:text-white text-gray-900 dark:text-white bg-white dark:bg-gray-800" :class="selectedTaskGroup === 'All' ? 'text-gray-900 dark:text-white bg-white dark:bg-gray-800' : 'text-gray-500 dark:text-gray-400'" @click="selectedTaskGroup = 'All' ">
All Tasks
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15" :class="selectedTaskGroup === 'All' ? 'text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15' : 'bg-white dark:bg-white/[0.03]'">
11
</span>
</button>
<button class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400" :class="selectedTaskGroup === 'Todo' ? 'text-gray-900 dark:text-white bg-white dark:bg-gray-800' : 'text-gray-500 dark:text-gray-400'" @click="selectedTaskGroup = 'Todo' ">
To do
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 bg-white dark:bg-white/[0.03]" :class="selectedTaskGroup === 'Todo' ? 'text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15' : 'bg-white dark:bg-white/[0.03]'">
3
</span>
</button>
<button class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400" :class="selectedTaskGroup === 'InProgress' ? 'text-gray-900 dark:text-white bg-white dark:bg-gray-800' : 'text-gray-500 dark:text-gray-400'" @click="selectedTaskGroup = 'InProgress' ">
In Progress
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 bg-white dark:bg-white/[0.03]" :class="selectedTaskGroup === 'InProgress' ? 'text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15' : 'bg-white dark:bg-white/[0.03]'">
4
</span>
</button>
<button class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white text-gray-500 dark:text-gray-400" :class="selectedTaskGroup === 'Completed' ? 'text-gray-900 dark:text-white bg-white dark:bg-gray-800' : 'text-gray-500 dark:text-gray-400'" @click="selectedTaskGroup = 'Completed' ">
Completed
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 bg-white dark:bg-white/[0.03]" :class="selectedTaskGroup === 'Completed' ? 'text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15' : 'bg-white dark:bg-white/[0.03]'">
4
</span>
</button>
</div>
<div class="flex flex-wrap items-center gap-3 xl:justify-end">
<button class="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
<svg class="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
</svg>
Filter &amp; Short
</button>
<button @click="isTaskModalModal = true" class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600">
Add New Task
<svg class="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.2502 4.99951C9.2502 4.5853 9.58599 4.24951 10.0002 4.24951C10.4144 4.24951 10.7502 4.5853 10.7502 4.99951V9.24971H15.0006C15.4148 9.24971 15.7506 9.5855 15.7506 9.99971C15.7506 10.4139 15.4148 10.7497 15.0006 10.7497H10.7502V15.0001C10.7502 15.4143 10.4144 15.7501 10.0002 15.7501C9.58599 15.7501 9.2502 15.4143 9.2502 15.0001V10.7497H5C4.58579 10.7497 4.25 10.4139 4.25 9.99971C4.25 9.5855 4.58579 9.24971 5 9.24971H9.2502V4.99951Z" fill=""></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Task header End -->
<!-- Task wrapper Start -->
<div class="mt-7 grid grid-cols-1 border-t border-gray-200 sm:mt-0 sm:grid-cols-2 xl:grid-cols-3 dark:border-gray-800">
<!-- To do list -->
<div class="swim-lane flex flex-col gap-5 p-4 xl:p-6">
<div class="mb-1 flex items-center justify-between">
<h3 class="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
To Do
<span class="text-theme-xs inline-flex rounded-full bg-gray-100 px-2 py-0.5 font-medium text-gray-700 dark:bg-white/[0.03] dark:text-white/80">
3
</span>
</h3>
<div x-data="{openDropDown: false}" class="relative">
<button @click="openDropDown = !openDropDown" class="text-gray-700 dark:text-gray-400">
<svg class="fill-current" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.99902 10.2451C6.96552 10.2451 7.74902 11.0286 7.74902 11.9951V12.0051C7.74902 12.9716 6.96552 13.7551 5.99902 13.7551C5.03253 13.7551 4.24902 12.9716 4.24902 12.0051V11.9951C4.24902 11.0286 5.03253 10.2451 5.99902 10.2451ZM17.999 10.2451C18.9655 10.2451 19.749 11.0286 19.749 11.9951V12.0051C19.749 12.9716 18.9655 13.7551 17.999 13.7551C17.0325 13.7551 16.249 12.9716 16.249 12.0051V11.9951C16.249 11.0286 17.0325 10.2451 17.999 10.2451ZM13.749 11.9951C13.749 11.0286 12.9655 10.2451 11.999 10.2451C11.0325 10.2451 10.249 11.0286 10.249 11.9951V12.0051C10.249 12.9716 11.0325 13.7551 11.999 13.7551C12.9655 13.7551 13.749 12.9716 13.749 12.0051V11.9951Z" fill=""></path>
</svg>
</button>
<div x-show="openDropDown" @click.outside="openDropDown = false" class="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800" style="display: none;">
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Edit
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Delete
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Clear All
</button>
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Finish user onboarding
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Tomorrow
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
1
</span>
</div>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-01.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Solve the Dribbble prioritisation issue with the team
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Jan 8, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
2
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.88066 3.10905C8.54039 1.44932 11.2313 1.44933 12.8911 3.10906C14.5508 4.76878 14.5508 7.45973 12.8911 9.11946L12.0657 9.94479L11.0051 8.88413L11.8304 8.0588C12.9043 6.98486 12.9043 5.24366 11.8304 4.16972C10.7565 3.09577 9.01526 3.09577 7.94132 4.16971L7.11599 4.99504L6.05533 3.93438L6.88066 3.10905ZM8.88376 11.0055L9.94442 12.0661L9.11983 12.8907C7.4601 14.5504 4.76915 14.5504 3.10942 12.8907C1.44969 11.231 1.44969 8.54002 3.10942 6.88029L3.93401 6.0557L4.99467 7.11636L4.17008 7.94095C3.09614 9.01489 3.09614 10.7561 4.17008 11.83C5.24402 12.904 6.98522 12.904 8.05917 11.83L8.88376 11.0055ZM9.94458 7.11599C10.2375 6.8231 10.2375 6.34823 9.94458 6.05533C9.65169 5.76244 9.17682 5.76244 8.88392 6.05533L6.0555 8.88376C5.7626 9.17665 5.7626 9.65153 6.0555 9.94442C6.34839 10.2373 6.82326 10.2373 7.11616 9.94442L9.94458 7.11599Z" fill=""></path>
</svg>
1
</span>
</div>
<span class="bg-brand-50 text-theme-xs text-brand-500 dark:bg-brand-500/15 dark:text-brand-400 mt-3 inline-flex rounded-full px-2 py-0.5 font-medium">
Marketing
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-07.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Change license and remove products
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Jan 8, 2027
</span>
</div>
<span class="text-theme-xs mt-3 inline-flex rounded-full bg-gray-100 px-2 py-0.5 font-medium text-gray-700 dark:bg-white/[0.03] dark:text-white/80">
Dev
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-08.jpg" alt="user">
</div>
</div>
</div>
</div>
<!-- Progress list -->
<div class="swim-lane flex flex-col gap-5 border-x border-gray-200 p-4 xl:p-6 dark:border-gray-800">
<div class="mb-1 flex items-center justify-between">
<h3 class="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
In Progress
<span class="bg-warning-50 text-theme-xs text-warning-700 dark:bg-warning-500/15 inline-flex rounded-full px-2 py-0.5 font-medium dark:text-orange-400">
5
</span>
</h3>
<div x-data="{openDropDown: false}" class="relative">
<button @click="openDropDown = !openDropDown" class="text-gray-700 dark:text-gray-400">
<svg class="fill-current" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.99902 10.2451C6.96552 10.2451 7.74902 11.0286 7.74902 11.9951V12.0051C7.74902 12.9716 6.96552 13.7551 5.99902 13.7551C5.03253 13.7551 4.24902 12.9716 4.24902 12.0051V11.9951C4.24902 11.0286 5.03253 10.2451 5.99902 10.2451ZM17.999 10.2451C18.9655 10.2451 19.749 11.0286 19.749 11.9951V12.0051C19.749 12.9716 18.9655 13.7551 17.999 13.7551C17.0325 13.7551 16.249 12.9716 16.249 12.0051V11.9951C16.249 11.0286 17.0325 10.2451 17.999 10.2451ZM13.749 11.9951C13.749 11.0286 12.9655 10.2451 11.999 10.2451C11.0325 10.2451 10.249 11.0286 10.249 11.9951V12.0051C10.249 12.9716 11.0325 13.7551 11.999 13.7551C12.9655 13.7551 13.749 12.9716 13.749 12.0051V11.9951Z" fill=""></path>
</svg>
</button>
<div x-show="openDropDown" @click.outside="openDropDown = false" class="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800" style="display: none;">
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Edit
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Delete
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Clear All
</button>
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Work In Progress (WIP) Dashboard
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Today
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
1
</span>
</div>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-09.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Kanban Flow Manager
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Feb 12, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
8
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.88066 3.10905C8.54039 1.44932 11.2313 1.44933 12.8911 3.10906C14.5508 4.76878 14.5508 7.45973 12.8911 9.11946L12.0657 9.94479L11.0051 8.88413L11.8304 8.0588C12.9043 6.98486 12.9043 5.24366 11.8304 4.16972C10.7565 3.09577 9.01526 3.09577 7.94132 4.16971L7.11599 4.99504L6.05533 3.93438L6.88066 3.10905ZM8.88376 11.0055L9.94442 12.0661L9.11983 12.8907C7.4601 14.5504 4.76915 14.5504 3.10942 12.8907C1.44969 11.231 1.44969 8.54002 3.10942 6.88029L3.93401 6.0557L4.99467 7.11636L4.17008 7.94095C3.09614 9.01489 3.09614 10.7561 4.17008 11.83C5.24402 12.904 6.98522 12.904 8.05917 11.83L8.88376 11.0055ZM9.94458 7.11599C10.2375 6.8231 10.2375 6.34823 9.94458 6.05533C9.65169 5.76244 9.17682 5.76244 8.88392 6.05533L6.0555 8.88376C5.7626 9.17665 5.7626 9.65153 6.0555 9.94442C6.34839 10.2373 6.82326 10.2373 7.11616 9.94442L9.94458 7.11599Z" fill=""></path>
</svg>
2
</span>
</div>
<span class="bg-success-50 text-theme-xs text-success-700 dark:bg-success-500/15 dark:text-success-500 mt-3 inline-flex rounded-full px-2 py-0.5 font-medium">
Template
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-10.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div>
<h4 class="mb-2 text-base text-gray-800 dark:text-white/90">
Product Update - Q4 2024
</h4>
<p class="text-sm text-gray-500 dark:text-gray-400">
Dedicated form for a category of users that will perform
actions.
</p>
<div class="my-4">
<img src="src/images/task/task.png" alt="task" class="overflow-hidden rounded-xl border-[0.5px] border-gray-200 dark:border-gray-800">
</div>
<div class="flex items-start justify-between gap-6">
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Feb 12, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
8
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-11.jpg" alt="user">
</div>
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Make figbot send comment when ticket is auto-moved
back to inbox
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Mar 08, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
1
</span>
</div>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-12.jpg" alt="user">
</div>
</div>
</div>
</div>
<!-- Completed list -->
<div class="swim-lane flex flex-col gap-5 p-4 xl:p-6">
<div class="mb-1 flex items-center justify-between">
<h3 class="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
Completed
<span class="bg-success-50 text-theme-xs text-success-700 dark:bg-success-500/15 dark:text-success-500 inline-flex rounded-full px-2 py-0.5 font-medium">
4
</span>
</h3>
<div x-data="{openDropDown: false}" class="relative">
<button @click="openDropDown = !openDropDown" class="text-gray-700 dark:text-gray-400">
<svg class="fill-current" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.99902 10.2451C6.96552 10.2451 7.74902 11.0286 7.74902 11.9951V12.0051C7.74902 12.9716 6.96552 13.7551 5.99902 13.7551C5.03253 13.7551 4.24902 12.9716 4.24902 12.0051V11.9951C4.24902 11.0286 5.03253 10.2451 5.99902 10.2451ZM17.999 10.2451C18.9655 10.2451 19.749 11.0286 19.749 11.9951V12.0051C19.749 12.9716 18.9655 13.7551 17.999 13.7551C17.0325 13.7551 16.249 12.9716 16.249 12.0051V11.9951C16.249 11.0286 17.0325 10.2451 17.999 10.2451ZM13.749 11.9951C13.749 11.0286 12.9655 10.2451 11.999 10.2451C11.0325 10.2451 10.249 11.0286 10.249 11.9951V12.0051C10.249 12.9716 11.0325 13.7551 11.999 13.7551C12.9655 13.7551 13.749 12.9716 13.749 12.0051V11.9951Z" fill=""></path>
</svg>
</button>
<div x-show="openDropDown" @click.outside="openDropDown = false" class="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800" style="display: none;">
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Edit
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Delete
</button>
<button class="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Clear All
</button>
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Manage internal feedback
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Tomorrow
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
1
</span>
</div>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-13.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Do some projects on React Native with Flutter
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Jan 8, 2027
</span>
</div>
<span class="text-theme-xs mt-3 inline-flex rounded-full bg-orange-400/10 px-2 py-0.5 font-medium text-orange-400">
Development
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-14.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Design marketing assets
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Jan 8, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
2
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.88066 3.10905C8.54039 1.44932 11.2313 1.44933 12.8911 3.10906C14.5508 4.76878 14.5508 7.45973 12.8911 9.11946L12.0657 9.94479L11.0051 8.88413L11.8304 8.0588C12.9043 6.98486 12.9043 5.24366 11.8304 4.16972C10.7565 3.09577 9.01526 3.09577 7.94132 4.16971L7.11599 4.99504L6.05533 3.93438L6.88066 3.10905ZM8.88376 11.0055L9.94442 12.0661L9.11983 12.8907C7.4601 14.5504 4.76915 14.5504 3.10942 12.8907C1.44969 11.231 1.44969 8.54002 3.10942 6.88029L3.93401 6.0557L4.99467 7.11636L4.17008 7.94095C3.09614 9.01489 3.09614 10.7561 4.17008 11.83C5.24402 12.904 6.98522 12.904 8.05917 11.83L8.88376 11.0055ZM9.94458 7.11599C10.2375 6.8231 10.2375 6.34823 9.94458 6.05533C9.65169 5.76244 9.17682 5.76244 8.88392 6.05533L6.0555 8.88376C5.7626 9.17665 5.7626 9.65153 6.0555 9.94442C6.34839 10.2373 6.82326 10.2373 7.11616 9.94442L9.94458 7.11599Z" fill=""></path>
</svg>
1
</span>
</div>
<span class="bg-brand-50 text-theme-xs text-brand-500 dark:bg-brand-500/15 dark:text-brand-400 mt-3 inline-flex rounded-full px-2 py-0.5 font-medium">
Marketing
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-15.jpg" alt="user">
</div>
</div>
</div>
<!-- task item -->
<div draggable="true" class="task shadow-theme-sm rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/5">
<div class="flex items-start justify-between gap-6">
<div>
<h4 class="mb-5 text-base text-gray-800 dark:text-white/90">
Kanban Flow Manager
</h4>
<div class="flex items-center gap-3">
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
Feb 12, 2027
</span>
<span class="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg class="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" stroke-width="1.5" stroke-linejoin="round"></path>
</svg>
8
</span>
</div>
<span class="bg-success-50 text-theme-xs text-success-700 dark:bg-success-500/15 dark:text-success-500 mt-3 inline-flex rounded-full px-2 py-0.5 font-medium">
Template
</span>
</div>
<div class="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src="src/images/user/user-16.jpg" alt="user">
</div>
</div>
</div>
</div>
</div>
<!-- Task wrapper End -->
</div>

View File

@@ -0,0 +1,393 @@
import React, { useState } from "react";
import { PlusIcon, HorizontaLDots } from "../../icons";
export interface Task {
id: string;
title: string;
status: "todo" | "in_progress" | "completed";
dueDate?: string;
commentsCount?: number;
attachmentsCount?: number;
tags?: { label: string; color?: "brand" | "success" | "warning" | "orange" | "gray" }[];
assignee?: {
avatar?: string;
name?: string;
};
description?: string;
image?: string;
}
interface KanbanBoardProps {
tasks: Task[];
onTaskClick?: (task: Task) => void;
onTaskMove?: (taskId: string, newStatus: "todo" | "in_progress" | "completed") => void;
onAddTask?: () => void;
onFilterChange?: (filter: "All" | "Todo" | "InProgress" | "Completed") => void;
}
const KanbanBoard: React.FC<KanbanBoardProps> = ({
tasks,
onTaskClick,
onTaskMove,
onAddTask,
onFilterChange,
}) => {
const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All");
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
const filteredTasks = selectedFilter === "All"
? tasks
: tasks.filter(task => {
if (selectedFilter === "Todo") return task.status === "todo";
if (selectedFilter === "InProgress") return task.status === "in_progress";
if (selectedFilter === "Completed") return task.status === "completed";
return true;
});
const todoTasks = filteredTasks.filter(t => t.status === "todo");
const inProgressTasks = filteredTasks.filter(t => t.status === "in_progress");
const completedTasks = filteredTasks.filter(t => t.status === "completed");
const handleFilterChange = (filter: "All" | "Todo" | "InProgress" | "Completed") => {
setSelectedFilter(filter);
onFilterChange?.(filter);
};
const toggleDropdown = (id: string) => {
setOpenDropdowns(prev => ({ ...prev, [id]: !prev[id] }));
};
const closeDropdown = (id: string) => {
setOpenDropdowns(prev => ({ ...prev, [id]: false }));
};
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
{/* Task header Start */}
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
onClick={() => handleFilterChange("All")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "All"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
All Tasks
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "All"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{tasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("Todo")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Todo"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
To do
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "Todo"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{todoTasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("InProgress")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "InProgress"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
In Progress
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "InProgress"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{inProgressTasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("Completed")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Completed"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
Completed
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "Completed"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{completedTasks.length}
</span>
</button>
</div>
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
</svg>
Filter & Sort
</button>
<button
onClick={onAddTask}
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
Add New Task
<PlusIcon className="fill-current" width={20} height={20} />
</button>
</div>
</div>
</div>
{/* Task header End */}
{/* Task wrapper Start */}
<div className="mt-7 grid grid-cols-1 border-t border-gray-200 sm:mt-0 sm:grid-cols-2 xl:grid-cols-3 dark:border-gray-800">
{/* To do list */}
<KanbanColumn
title="To Do"
tasks={todoTasks}
count={todoTasks.length}
status="todo"
onTaskClick={onTaskClick}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["todo"] || false}
/>
{/* Progress list */}
<KanbanColumn
title="In Progress"
tasks={inProgressTasks}
count={inProgressTasks.length}
status="in_progress"
onTaskClick={onTaskClick}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["in_progress"] || false}
borderX
/>
{/* Completed list */}
<KanbanColumn
title="Completed"
tasks={completedTasks}
count={completedTasks.length}
status="completed"
onTaskClick={onTaskClick}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["completed"] || false}
/>
</div>
{/* Task wrapper End */}
</div>
);
};
interface KanbanColumnProps {
title: string;
tasks: Task[];
count: number;
status: "todo" | "in_progress" | "completed";
onTaskClick?: (task: Task) => void;
onDropdownToggle: (id: string) => void;
onDropdownClose: (id: string) => void;
openDropdown: boolean;
borderX?: boolean;
}
const KanbanColumn: React.FC<KanbanColumnProps> = ({
title,
tasks,
count,
status,
onTaskClick,
onDropdownToggle,
onDropdownClose,
openDropdown,
borderX = false,
}) => {
const getCountBadgeClass = () => {
if (status === "in_progress") {
return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400";
}
if (status === "completed") {
return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500";
}
return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80";
};
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="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}
<span className={`text-theme-xs inline-flex rounded-full px-2 py-0.5 font-medium ${getCountBadgeClass()}`}>
{count}
</span>
</h3>
<div className="relative">
<button
onClick={() => onDropdownToggle(status)}
className="text-gray-700 dark:text-gray-400"
>
<HorizontaLDots className="fill-current" width={24} height={24} />
</button>
{openDropdown && (
<>
<div
className="fixed inset-0 z-30"
onClick={() => onDropdownClose(status)}
/>
<div className="shadow-theme-md dark:bg-gray-dark absolute top-full right-0 z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 dark:border-gray-800">
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Edit
</button>
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Delete
</button>
<button className="text-theme-xs flex w-full rounded-lg px-3 py-2 text-left font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Clear All
</button>
</div>
</>
)}
</div>
</div>
{tasks.map((task) => (
<TaskCard key={task.id} task={task} onClick={() => onTaskClick?.(task)} />
))}
</div>
);
};
interface TaskCardProps {
task: Task;
onClick?: () => void;
}
const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
const getTagClass = (color?: string) => {
switch (color) {
case "brand":
return "bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400";
case "success":
return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500";
case "warning":
return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400";
case "orange":
return "bg-orange-400/10 text-orange-400";
default:
return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80";
}
};
return (
<div
draggable
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"
>
<div className="flex items-start justify-between gap-6">
<div className="flex-1">
{task.image && (
<div className="my-4">
<img
src={task.image}
alt="task"
className="overflow-hidden rounded-xl border-[0.5px] border-gray-200 dark:border-gray-800 w-full"
/>
</div>
)}
<h4 className={`text-base text-gray-800 dark:text-white/90 ${task.image ? "mb-2" : "mb-5"}`}>
{task.title}
</h4>
{task.description && (
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">{task.description}</p>
)}
<div className="flex items-center gap-3">
{task.dueDate && (
<span className="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg className="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
{task.dueDate}
</span>
)}
{task.commentsCount !== undefined && (
<span className="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg className="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" strokeWidth="1.5" strokeLinejoin="round"></path>
</svg>
{task.commentsCount}
</span>
)}
{task.attachmentsCount !== undefined && (
<span className="flex cursor-pointer items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
<svg className="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M6.88066 3.10905C8.54039 1.44932 11.2313 1.44933 12.8911 3.10906C14.5508 4.76878 14.5508 7.45973 12.8911 9.11946L12.0657 9.94479L11.0051 8.88413L11.8304 8.0588C12.9043 6.98486 12.9043 5.24366 11.8304 4.16972C10.7565 3.09577 9.01526 3.09577 7.94132 4.16971L7.11599 4.99504L6.05533 3.93438L6.88066 3.10905ZM8.88376 11.0055L9.94442 12.0661L9.11983 12.8907C7.4601 14.5504 4.76915 14.5504 3.10942 12.8907C1.44969 11.231 1.44969 8.54002 3.10942 6.88029L3.93401 6.0557L4.99467 7.11636L4.17008 7.94095C3.09614 9.01489 3.09614 10.7561 4.17008 11.83C5.24402 12.904 6.98522 12.904 8.05917 11.83L8.88376 11.0055ZM9.94458 7.11599C10.2375 6.8231 10.2375 6.34823 9.94458 6.05533C9.65169 5.76244 9.17682 5.76244 8.88392 6.05533L6.0555 8.88376C5.7626 9.17665 5.7626 9.65153 6.0555 9.94442C6.34839 10.2373 6.82326 10.2373 7.11616 9.94442L9.94458 7.11599Z" fill=""></path>
</svg>
{task.attachmentsCount}
</span>
)}
</div>
{task.tags && task.tags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{task.tags.map((tag, index) => (
<span
key={index}
className={`text-theme-xs inline-flex rounded-full px-2 py-0.5 font-medium ${getTagClass(tag.color)}`}
>
{tag.label}
</span>
))}
</div>
)}
</div>
{task.assignee?.avatar && (
<div className="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src={task.assignee.avatar} alt={task.assignee.name || "user"} />
</div>
)}
</div>
</div>
);
};
export default KanbanBoard;

View File

@@ -0,0 +1,410 @@
import React, { useState } from "react";
import { PlusIcon, HorizontaLDots } from "../../icons";
import { Task } from "./KanbanBoard";
interface TaskListProps {
tasks: Task[];
onTaskClick?: (task: Task) => void;
onTaskToggle?: (taskId: string, completed: boolean) => void;
onAddTask?: () => void;
onFilterChange?: (filter: "All" | "Todo" | "InProgress" | "Completed") => void;
}
const TaskList: React.FC<TaskListProps> = ({
tasks,
onTaskClick,
onTaskToggle,
onAddTask,
onFilterChange,
}) => {
const [selectedFilter, setSelectedFilter] = useState<"All" | "Todo" | "InProgress" | "Completed">("All");
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
const [checkedTasks, setCheckedTasks] = useState<Set<string>>(new Set());
const filteredTasks = selectedFilter === "All"
? tasks
: tasks.filter(task => {
if (selectedFilter === "Todo") return task.status === "todo";
if (selectedFilter === "InProgress") return task.status === "in_progress";
if (selectedFilter === "Completed") return task.status === "completed";
return true;
});
const todoTasks = filteredTasks.filter(t => t.status === "todo");
const inProgressTasks = filteredTasks.filter(t => t.status === "in_progress");
const completedTasks = filteredTasks.filter(t => t.status === "completed");
const handleFilterChange = (filter: "All" | "Todo" | "InProgress" | "Completed") => {
setSelectedFilter(filter);
onFilterChange?.(filter);
};
const toggleDropdown = (id: string) => {
setOpenDropdowns(prev => ({ ...prev, [id]: !prev[id] }));
};
const closeDropdown = (id: string) => {
setOpenDropdowns(prev => ({ ...prev, [id]: false }));
};
const handleCheckboxChange = (taskId: string, checked: boolean) => {
if (checked) {
setCheckedTasks(prev => new Set(prev).add(taskId));
} else {
setCheckedTasks(prev => {
const newSet = new Set(prev);
newSet.delete(taskId);
return newSet;
});
}
onTaskToggle?.(taskId, checked);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
{/* Task header Start */}
<div className="flex flex-col items-center px-4 py-5 xl:px-6 xl:py-6">
<div className="flex flex-col w-full gap-5 sm:justify-between xl:flex-row xl:items-center">
<div className="flex flex-wrap items-center gap-x-1 gap-y-2 rounded-lg bg-gray-100 p-0.5 dark:bg-gray-900">
<button
onClick={() => handleFilterChange("All")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "All"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
All Tasks
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "All"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{tasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("Todo")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Todo"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
To do
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "Todo"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{todoTasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("InProgress")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "InProgress"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
In Progress
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "InProgress"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{inProgressTasks.length}
</span>
</button>
<button
onClick={() => handleFilterChange("Completed")}
className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md group hover:text-gray-900 dark:hover:text-white ${
selectedFilter === "Completed"
? "text-gray-900 dark:text-white bg-white dark:bg-gray-800"
: "text-gray-500 dark:text-gray-400"
}`}
>
Completed
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium leading-normal group-hover:bg-brand-50 group-hover:text-brand-500 dark:group-hover:bg-brand-500/15 dark:group-hover:text-brand-400 ${
selectedFilter === "Completed"
? "text-brand-500 dark:text-brand-400 bg-brand-50 dark:bg-brand-500/15"
: "bg-white dark:bg-white/[0.03]"
}`}
>
{completedTasks.length}
</span>
</button>
</div>
<div className="flex flex-wrap items-center gap-3 xl:justify-end">
<button className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-white/[0.03]">
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M12.0826 4.0835C11.0769 4.0835 10.2617 4.89871 10.2617 5.90433C10.2617 6.90995 11.0769 7.72516 12.0826 7.72516C13.0882 7.72516 13.9034 6.90995 13.9034 5.90433C13.9034 4.89871 13.0882 4.0835 12.0826 4.0835ZM2.29004 6.65409H8.84671C9.18662 8.12703 10.5063 9.22516 12.0826 9.22516C13.6588 9.22516 14.9785 8.12703 15.3184 6.65409H17.7067C18.1209 6.65409 18.4567 6.31831 18.4567 5.90409C18.4567 5.48988 18.1209 5.15409 17.7067 5.15409H15.3183C14.9782 3.68139 13.6586 2.5835 12.0826 2.5835C10.5065 2.5835 9.18691 3.68139 8.84682 5.15409H2.29004C1.87583 5.15409 1.54004 5.48988 1.54004 5.90409C1.54004 6.31831 1.87583 6.65409 2.29004 6.65409ZM4.6816 13.3462H2.29085C1.87664 13.3462 1.54085 13.682 1.54085 14.0962C1.54085 14.5104 1.87664 14.8462 2.29085 14.8462H4.68172C5.02181 16.3189 6.34142 17.4168 7.91745 17.4168C9.49348 17.4168 10.8131 16.3189 11.1532 14.8462H17.7075C18.1217 14.8462 18.4575 14.5104 18.4575 14.0962C18.4575 13.682 18.1217 13.3462 17.7075 13.3462H11.1533C10.8134 11.8733 9.49366 10.7752 7.91745 10.7752C6.34124 10.7752 5.02151 11.8733 4.6816 13.3462ZM9.73828 14.096C9.73828 13.0904 8.92307 12.2752 7.91745 12.2752C6.91183 12.2752 6.09662 13.0904 6.09662 14.096C6.09662 15.1016 6.91183 15.9168 7.91745 15.9168C8.92307 15.9168 9.73828 15.1016 9.73828 14.096Z" fill=""></path>
</svg>
Filter & Sort
</button>
<button
onClick={onAddTask}
className="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
Add New Task
<PlusIcon className="fill-current" width={20} height={20} />
</button>
</div>
</div>
</div>
{/* Task header End */}
{/* Task wrapper Start */}
<div className="p-4 space-y-8 border-t border-gray-200 mt-7 dark:border-gray-800 sm:mt-0 xl:p-6">
{/* To do list */}
<TaskListSection
title="To Do"
tasks={todoTasks}
count={todoTasks.length}
checkedTasks={checkedTasks}
onTaskClick={onTaskClick}
onCheckboxChange={handleCheckboxChange}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["todo"] || false}
/>
{/* Progress list */}
<TaskListSection
title="In Progress"
tasks={inProgressTasks}
count={inProgressTasks.length}
checkedTasks={checkedTasks}
onTaskClick={onTaskClick}
onCheckboxChange={handleCheckboxChange}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["in_progress"] || false}
/>
{/* Completed list */}
<TaskListSection
title="Completed"
tasks={completedTasks}
count={completedTasks.length}
checkedTasks={checkedTasks}
onTaskClick={onTaskClick}
onCheckboxChange={handleCheckboxChange}
onDropdownToggle={toggleDropdown}
onDropdownClose={closeDropdown}
openDropdown={openDropdowns["completed"] || false}
/>
</div>
{/* Task wrapper End */}
</div>
);
};
interface TaskListSectionProps {
title: string;
tasks: Task[];
count: number;
checkedTasks: Set<string>;
onTaskClick?: (task: Task) => void;
onCheckboxChange: (taskId: string, checked: boolean) => void;
onDropdownToggle: (id: string) => void;
onDropdownClose: (id: string) => void;
openDropdown: boolean;
}
const TaskListSection: React.FC<TaskListSectionProps> = ({
title,
tasks,
count,
checkedTasks,
onTaskClick,
onCheckboxChange,
onDropdownToggle,
onDropdownClose,
openDropdown,
}) => {
const getCountBadgeClass = () => {
if (title === "In Progress") {
return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400";
}
if (title === "Completed") {
return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500";
}
return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80";
};
return (
<div className="flex flex-col gap-4 swim-lane">
<div className="flex items-center justify-between mb-2">
<h3 className="flex items-center gap-3 text-base font-medium text-gray-800 dark:text-white/90">
{title}
<span className={`inline-flex rounded-full px-2 py-0.5 text-theme-xs font-medium ${getCountBadgeClass()}`}>
{count}
</span>
</h3>
<div className="relative">
<button
onClick={() => onDropdownToggle(title.toLowerCase().replace(" ", "_"))}
className="text-gray-700 dark:text-gray-400"
>
<HorizontaLDots className="fill-current" width={24} height={24} />
</button>
{openDropdown && (
<>
<div
className="fixed inset-0 z-30"
onClick={() => onDropdownClose(title.toLowerCase().replace(" ", "_"))}
/>
<div className="absolute right-0 top-full z-40 w-[140px] space-y-1 rounded-2xl border border-gray-200 bg-white p-2 shadow-theme-md dark:border-gray-800 dark:bg-gray-dark">
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Edit
</button>
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Delete
</button>
<button className="flex w-full px-3 py-2 font-medium text-left text-gray-500 rounded-lg text-theme-xs hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300">
Clear All
</button>
</div>
</>
)}
</div>
</div>
{tasks.map((task) => (
<TaskListItem
key={task.id}
task={task}
checked={checkedTasks.has(task.id)}
onClick={() => onTaskClick?.(task)}
onCheckboxChange={(checked) => onCheckboxChange(task.id, checked)}
/>
))}
</div>
);
};
interface TaskListItemProps {
task: Task;
checked: boolean;
onClick?: () => void;
onCheckboxChange: (checked: boolean) => void;
}
const TaskListItem: React.FC<TaskListItemProps> = ({ task, checked, onClick, onCheckboxChange }) => {
const getTagClass = (color?: string) => {
switch (color) {
case "brand":
return "bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400";
case "success":
return "bg-success-50 text-success-700 dark:bg-success-500/15 dark:text-success-500";
case "warning":
return "bg-warning-50 text-warning-700 dark:bg-warning-500/15 dark:text-orange-400";
case "orange":
return "bg-orange-400/10 text-orange-400";
default:
return "bg-gray-100 text-gray-700 dark:bg-white/[0.03] dark:text-white/80";
}
};
return (
<div
draggable
className="p-5 bg-white border border-gray-200 task rounded-xl shadow-theme-sm dark:border-gray-800 dark:bg-white/5"
>
<div className="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
<div className="flex items-start w-full gap-4">
<span className="text-gray-400">
<svg className="fill-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M2.43311 5.0001C2.43311 4.50304 2.83605 4.1001 3.33311 4.1001L16.6664 4.1001C17.1635 4.1001 17.5664 4.50304 17.5664 5.0001C17.5664 5.49715 17.1635 5.9001 16.6664 5.9001L3.33311 5.9001C2.83605 5.9001 2.43311 5.49716 2.43311 5.0001ZM2.43311 15.0001C2.43311 14.503 2.83605 14.1001 3.33311 14.1001L16.6664 14.1001C17.1635 14.1001 17.5664 14.503 17.5664 15.0001C17.5664 15.4972 17.1635 15.9001 16.6664 15.9001L3.33311 15.9001C2.83605 15.9001 2.43311 15.4972 2.43311 15.0001ZM3.33311 9.1001C2.83605 9.1001 2.43311 9.50304 2.43311 10.0001C2.43311 10.4972 2.83605 10.9001 3.33311 10.9001L16.6664 10.9001C17.1635 10.9001 17.5664 10.4972 17.5664 10.0001C17.5664 9.50304 17.1635 9.1001 16.6664 9.1001L3.33311 9.1001Z" fill=""></path>
</svg>
</span>
<label htmlFor={`taskCheckbox-${task.id}`} className="w-full cursor-pointer">
<div className="relative flex items-start">
<input
type="checkbox"
id={`taskCheckbox-${task.id}`}
className="sr-only taskCheckbox"
checked={checked}
onChange={(e) => onCheckboxChange(e.target.checked)}
/>
<div className={`flex items-center justify-center w-full h-5 mr-3 border border-gray-300 rounded-md box max-w-5 dark:border-gray-700 ${checked ? "bg-brand-500 border-brand-500" : ""}`}>
<span className={checked ? "opacity-100" : "opacity-0"}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6668 3.5L5.25016 9.91667L2.3335 7" stroke="white" strokeWidth="1.94437" strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
</span>
</div>
<p className="-mt-0.5 text-base text-gray-800 dark:text-white/90" onClick={onClick}>
{task.title}
</p>
</div>
</label>
</div>
<div className="flex flex-col-reverse items-start justify-end w-full gap-3 xl:flex-row xl:items-center xl:gap-5">
{task.tags && task.tags.length > 0 && (
<span className={`inline-flex rounded-full px-2 py-0.5 text-theme-xs font-medium ${getTagClass(task.tags[0].color)}`}>
{task.tags[0].label}
</span>
)}
<div className="flex items-center justify-between w-full gap-5 xl:w-auto xl:justify-normal">
<div className="flex items-center gap-3">
{task.dueDate && (
<span className="flex items-center gap-1 text-sm text-gray-500 cursor-pointer dark:text-gray-400">
<svg className="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M5.33329 1.0835C5.74751 1.0835 6.08329 1.41928 6.08329 1.8335V2.25016L9.91663 2.25016V1.8335C9.91663 1.41928 10.2524 1.0835 10.6666 1.0835C11.0808 1.0835 11.4166 1.41928 11.4166 1.8335V2.25016L12.3333 2.25016C13.2998 2.25016 14.0833 3.03366 14.0833 4.00016V6.00016L14.0833 12.6668C14.0833 13.6333 13.2998 14.4168 12.3333 14.4168L3.66663 14.4168C2.70013 14.4168 1.91663 13.6333 1.91663 12.6668L1.91663 6.00016L1.91663 4.00016C1.91663 3.03366 2.70013 2.25016 3.66663 2.25016L4.58329 2.25016V1.8335C4.58329 1.41928 4.91908 1.0835 5.33329 1.0835ZM5.33329 3.75016L3.66663 3.75016C3.52855 3.75016 3.41663 3.86209 3.41663 4.00016V5.25016L12.5833 5.25016V4.00016C12.5833 3.86209 12.4714 3.75016 12.3333 3.75016L10.6666 3.75016L5.33329 3.75016ZM12.5833 6.75016L3.41663 6.75016L3.41663 12.6668C3.41663 12.8049 3.52855 12.9168 3.66663 12.9168L12.3333 12.9168C12.4714 12.9168 12.5833 12.8049 12.5833 12.6668L12.5833 6.75016Z" fill=""></path>
</svg>
{task.dueDate}
</span>
)}
{task.commentsCount !== undefined && (
<span className="flex items-center gap-1 text-sm text-gray-500 cursor-pointer dark:text-gray-400">
<svg className="stroke-current" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 15.6343C12.6244 15.6343 15.5625 12.6961 15.5625 9.07178C15.5625 5.44741 12.6244 2.50928 9 2.50928C5.37563 2.50928 2.4375 5.44741 2.4375 9.07178C2.4375 10.884 3.17203 12.5246 4.35961 13.7122L2.4375 15.6343H9Z" stroke="" strokeWidth="1.5" strokeLinejoin="round"></path>
</svg>
{task.commentsCount}
</span>
)}
{task.attachmentsCount !== undefined && (
<span className="flex items-center gap-1 text-sm text-gray-500 cursor-pointer dark:text-gray-400">
<svg className="fill-current" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M6.88066 3.10905C8.54039 1.44932 11.2313 1.44933 12.8911 3.10906C14.5508 4.76878 14.5508 7.45973 12.8911 9.11946L12.0657 9.94479L11.0051 8.88413L11.8304 8.0588C12.9043 6.98486 12.9043 5.24366 11.8304 4.16972C10.7565 3.09577 9.01526 3.09577 7.94132 4.16971L7.11599 4.99504L6.05533 3.93438L6.88066 3.10905ZM8.88376 11.0055L9.94442 12.0661L9.11983 12.8907C7.4601 14.5504 4.76915 14.5504 3.10942 12.8907C1.44969 11.231 1.44969 8.54002 3.10942 6.88029L3.93401 6.0557L4.99467 7.11636L4.17008 7.94095C3.09614 9.01489 3.09614 10.7561 4.17008 11.83C5.24402 12.904 6.98522 12.904 8.05917 11.83L8.88376 11.0055ZM9.94458 7.11599C10.2375 6.8231 10.2375 6.34823 9.94458 6.05533C9.65169 5.76244 9.17682 5.76244 8.88392 6.05533L6.0555 8.88376C5.7626 9.17665 5.7626 9.65153 6.0555 9.94442C6.34839 10.2373 6.82326 10.2373 7.11616 9.94442L9.94458 7.11599Z" fill=""></path>
</svg>
{task.attachmentsCount}
</span>
)}
</div>
{task.assignee?.avatar && (
<div className="h-6 w-full max-w-6 overflow-hidden rounded-full border-[0.5px] border-gray-200 dark:border-gray-800">
<img src={task.assignee.avatar} alt={task.assignee.name || "user"} />
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default TaskList;

View File

@@ -0,0 +1,4 @@
export { default as KanbanBoard } from "./KanbanBoard";
export { default as TaskList } from "./TaskList";
export type { Task } from "./KanbanBoard";

View File

@@ -0,0 +1,88 @@
import { ReactNode, useState, useRef, useEffect } from "react";
interface EnhancedTooltipProps {
children: ReactNode;
content: ReactNode | string;
placement?: "top" | "bottom" | "left" | "right";
className?: string;
delay?: number;
}
export const EnhancedTooltip: React.FC<EnhancedTooltipProps> = ({
children,
content,
placement = "top",
className = "",
delay = 200,
}) => {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout>();
const placementClasses = {
top: "bottom-full left-1/2 mb-2 -translate-x-1/2 before:top-full before:left-1/2 before:-translate-x-1/2 before:-mt-1 before:border-t-gray-900",
bottom:
"top-full left-1/2 mt-2 -translate-x-1/2 before:bottom-full before:left-1/2 before:-translate-x-1/2 before:-mb-1 before:border-b-gray-900",
left: "right-full top-1/2 mr-2 -translate-y-1/2 before:left-full before:top-1/2 before:-translate-y-1/2 before:-ml-1 before:border-l-gray-900",
right:
"left-full top-1/2 ml-2 -translate-y-1/2 before:right-full before:top-1/2 before:-translate-y-1/2 before:-mr-1 before:border-r-gray-900",
};
const handleMouseEnter = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsVisible(true);
}, delay);
};
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setIsVisible(false);
};
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<div
ref={triggerRef}
className={`relative inline-flex ${className}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
{isVisible && (
<div
ref={tooltipRef}
className={`
absolute z-50 px-3 py-2 text-xs font-medium text-white bg-gray-900 rounded-lg
opacity-100 pointer-events-none transition-opacity duration-200
whitespace-normal max-w-xs shadow-lg
before:absolute before:border-4 before:border-transparent before:content-['']
${placementClasses[placement]}
`}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{typeof content === "string" ? (
<span>{content}</span>
) : (
<div className="text-white">{content}</div>
)}
</div>
)}
</div>
);
};

View File

@@ -4,6 +4,8 @@ import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import WorkflowPipeline, { WorkflowStep } from "../../components/dashboard/WorkflowPipeline";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
@@ -444,174 +446,141 @@ export default function PlannerDashboard() {
</button>
</div>
{/* Top Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<Link
to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Keywords Ready</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.keywords.total.toLocaleString()}
</h4>
{trends.keywords !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.keywords > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.keywords > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.keywords)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} mapped {stats.keywords.unmapped} unmapped
{/* Hero Section - Key Metric */}
<div className="rounded-2xl border border-gray-200 bg-gradient-to-br from-brand-50 to-white dark:from-brand-500/10 dark:to-gray-800/50 dark:border-gray-800 p-6 md:p-8">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Planning Progress</p>
<h3 className="mt-2 text-3xl font-bold text-gray-800 dark:text-white/90">
{stats.ideas.queued > 0 ? (
<>
{stats.ideas.queued} Ideas Ready for Content Generation
</>
) : stats.ideas.total > 0 ? (
<>
{stats.ideas.total} Ideas Generated
</>
) : stats.clusters.total > 0 ? (
<>
{stats.clusters.total} Clusters Built
</>
) : (
<>
{stats.keywords.total} Keywords Ready
</>
)}
</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{stats.keywords.total} keywords {stats.clusters.total} clusters {stats.ideas.total} ideas
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<ListIcon className="text-brand-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/clusters"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Clusters Built</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.clusters.total.toLocaleString()}
</h4>
{trends.clusters !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.clusters > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.clusters > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.clusters)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.clusters.totalVolume.toLocaleString()} total volume {stats.clusters.avgKeywords} avg keywords
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<GroupIcon className="text-success-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/ideas"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Ideas Generated</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.ideas.total.toLocaleString()}
</h4>
{trends.ideas !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.ideas > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.ideas > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.ideas)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.ideas.queued} queued {stats.ideas.notQueued} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoltIcon className="text-warning-500 size-6" />
</div>
</div>
</Link>
<Link
to="/planner/keywords"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Mapping Progress</p>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
{keywordMappingPct}%
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.keywords.mapped} of {stats.keywords.total} keywords mapped
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
<PieChartIcon className="text-purple-500 size-6" />
</div>
</div>
</Link>
</div>
{/* Planner Workflow Steps */}
<ComponentCard title="Planner Workflow Steps" desc="Track your planning progress">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{workflowSteps.map((step) => (
<Link
key={step.number}
to={step.path}
className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group"
>
<div className="flex items-center gap-3 mb-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${
step.status === "completed"
? "bg-success-500 text-white"
: step.status === "in_progress"
? "bg-warning-500 text-white"
: "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}`}>
{step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number}
</div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
<div className="hidden md:flex items-center gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-brand-500">{keywordMappingPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Mapped</div>
</div>
<div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-1.5">
{step.status === "completed" ? (
<>
<CheckCircleIcon className="size-4 text-success-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span>
</>
) : (
<>
<TimeIcon className="size-4 text-amber-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span>
</>
)}
</div>
<div className="text-center">
<div className="text-2xl font-bold text-success-500">{clustersIdeasPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">With Ideas</div>
</div>
{step.count !== null && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{step.count} {step.title.includes("Keywords") ? "keywords" : step.title.includes("Clusters") ? "clusters" : step.title.includes("Ideas") ? "ideas" : "items"}
</p>
)}
{step.status === "pending" && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(step.path);
}}
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform"
>
Start Now <ArrowRightIcon className="size-3" />
</button>
)}
</Link>
))}
<div className="text-center">
<div className="text-2xl font-bold text-warning-500">{ideasQueuedPct}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Queued</div>
</div>
</div>
</div>
</div>
{/* Enhanced Metric Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<EnhancedMetricCard
title="Keywords Ready"
value={stats.keywords.total}
subtitle={`${stats.keywords.mapped} mapped • ${stats.keywords.unmapped} unmapped`}
trend={trends.keywords}
icon={<ListIcon className="size-6" />}
accentColor="blue"
href="/planner/keywords"
details={[
{ label: "Total Keywords", value: stats.keywords.total },
{ label: "Mapped", value: stats.keywords.mapped },
{ label: "Unmapped", value: stats.keywords.unmapped },
{ label: "Active", value: stats.keywords.byStatus.active || 0 },
{ label: "Pending", value: stats.keywords.byStatus.pending || 0 },
]}
/>
<EnhancedMetricCard
title="Clusters Built"
value={stats.clusters.total}
subtitle={`${stats.clusters.totalVolume.toLocaleString()} total volume • ${stats.clusters.avgKeywords} avg keywords`}
trend={trends.clusters}
icon={<GroupIcon className="size-6" />}
accentColor="green"
href="/planner/clusters"
details={[
{ label: "Total Clusters", value: stats.clusters.total },
{ label: "With Ideas", value: stats.clusters.withIdeas },
{ label: "Without Ideas", value: stats.clusters.withoutIdeas },
{ label: "Total Volume", value: stats.clusters.totalVolume.toLocaleString() },
{ label: "Avg Keywords", value: stats.clusters.avgKeywords },
]}
/>
<EnhancedMetricCard
title="Ideas Generated"
value={stats.ideas.total}
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} pending`}
trend={trends.ideas}
icon={<BoltIcon className="size-6" />}
accentColor="orange"
href="/planner/ideas"
details={[
{ label: "Total Ideas", value: stats.ideas.total },
{ label: "Queued", value: stats.ideas.queued },
{ label: "Not Queued", value: stats.ideas.notQueued },
{ label: "New", value: stats.ideas.byStatus.new || 0 },
{ label: "Scheduled", value: stats.ideas.byStatus.scheduled || 0 },
]}
/>
<EnhancedMetricCard
title="Mapping Progress"
value={`${keywordMappingPct}%`}
subtitle={`${stats.keywords.mapped} of ${stats.keywords.total} keywords mapped`}
icon={<PieChartIcon className="size-6" />}
accentColor="purple"
href="/planner/keywords"
details={[
{ label: "Mapping Progress", value: `${keywordMappingPct}%` },
{ label: "Mapped Keywords", value: stats.keywords.mapped },
{ label: "Total Keywords", value: stats.keywords.total },
{ label: "Unmapped", value: stats.keywords.unmapped },
]}
/>
</div>
{/* Interactive Workflow Pipeline */}
<ComponentCard title="Planner Workflow Pipeline" desc="Track your planning progress through each stage">
<WorkflowPipeline
steps={workflowSteps.map(step => ({
number: step.number,
title: step.title,
status: step.status === "completed" ? "completed" : step.status === "in_progress" ? "in_progress" : "pending",
count: step.count || 0,
path: step.path,
description: step.title,
details: step.status === "completed"
? `${step.title} completed with ${step.count} items`
: step.status === "pending"
? `${step.title} pending - ${step.count} items ready`
: `${step.title} in progress`,
}))}
onStepClick={(step) => {
navigate(step.path);
}}
showConnections={true}
/>
</ComponentCard>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">

View File

@@ -4,6 +4,8 @@ import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import WorkflowPipeline, { WorkflowStep } from "../../components/dashboard/WorkflowPipeline";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
@@ -523,174 +525,147 @@ export default function WriterDashboard() {
</button>
</div>
{/* Top Status Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<Link
to="/writer/tasks"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Total Tasks</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.tasks.total.toLocaleString()}
</h4>
{trends.tasks !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.tasks > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.tasks > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.tasks)}</span>
</div>
)}
{/* Hero Section - Key Metric */}
<div className="rounded-2xl border border-gray-200 bg-gradient-to-br from-brand-50 to-white dark:from-brand-500/10 dark:to-gray-800/50 dark:border-gray-800 p-6 md:p-8">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Content Creation Progress</p>
<h3 className="mt-2 text-3xl font-bold text-gray-800 dark:text-white/90">
{stats.content.published > 0 ? (
<>
{stats.content.published} Content Pieces Published
</>
) : stats.content.review > 0 ? (
<>
{stats.content.review} Pieces Ready to Publish
</>
) : stats.content.drafts > 0 ? (
<>
{stats.content.drafts} Drafts Ready for Review
</>
) : stats.tasks.total > 0 ? (
<>
{stats.tasks.total} Tasks Created
</>
) : (
<>
Ready to Create Content
</>
)}
</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{stats.tasks.total} tasks {stats.content.total} content pieces {stats.images.generated} images generated
</p>
</div>
<div className="hidden md:flex items-center gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-brand-500">{completionRate}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Complete</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-success-500">{stats.productivity.publishRate}%</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Published</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-warning-500">
{stats.images.generated > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.tasks.completed} completed {stats.tasks.pending} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-xl dark:bg-blue-500/10 group-hover:bg-blue-100 dark:group-hover:bg-blue-500/20 transition-colors">
<FileTextIcon className="text-brand-500 size-6" />
<div className="text-xs text-gray-500 dark:text-gray-400">Images</div>
</div>
</div>
</Link>
<Link
to="/writer/content"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-success-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Content Pieces</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.content.total.toLocaleString()}
</h4>
{trends.content !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.content > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.content > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.content)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.content.published} published {stats.content.drafts} drafts
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-green-50 rounded-xl dark:bg-green-500/10 group-hover:bg-green-100 dark:group-hover:bg-green-500/20 transition-colors">
<PencilIcon className="text-success-500 size-6" />
</div>
</div>
</Link>
<Link
to="/writer/images"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-warning-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Images Generated</p>
<div className="flex items-center gap-2 mt-2">
<h4 className="font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.images.generated.toLocaleString()}
</h4>
{trends.images !== 0 && (
<div className={`flex items-center gap-1 text-xs ${trends.images > 0 ? 'text-success-500' : 'text-error-500'}`}>
{trends.images > 0 ? <ArrowUpIcon className="size-3" /> : <ArrowDownIcon className="size-3" />}
<span>{Math.abs(trends.images)}</span>
</div>
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.images.total} total {stats.images.pending} pending
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-amber-50 rounded-xl dark:bg-amber-500/10 group-hover:bg-amber-100 dark:group-hover:bg-amber-500/20 transition-colors">
<BoxIcon className="text-warning-500 size-6" />
</div>
</div>
</Link>
<Link
to="/writer/published"
className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6 hover:shadow-lg transition-all cursor-pointer group relative overflow-hidden"
>
<div className="absolute left-0 top-0 bottom-0 w-1 bg-purple-500"></div>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-gray-500 dark:text-gray-400">Publish Rate</p>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
{stats.productivity.publishRate}%
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{stats.content.published} of {stats.content.total} published
</p>
</div>
<div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-xl dark:bg-purple-500/10 group-hover:bg-purple-100 dark:group-hover:bg-purple-500/20 transition-colors">
<BoltIcon className="text-purple-500 size-6" />
</div>
</div>
</Link>
</div>
</div>
{/* Writer Workflow Steps */}
<ComponentCard title="Writer Workflow Steps" desc="Track your content creation progress">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{workflowSteps.map((step) => (
<Link
key={step.number}
to={step.path}
className="rounded-xl border border-gray-200 bg-gradient-to-br from-gray-50 to-white p-4 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-800 hover:border-brand-300 hover:bg-gradient-to-br hover:from-brand-50 hover:to-white dark:hover:from-brand-500/10 dark:hover:to-gray-800/50 transition-all group"
>
<div className="flex items-center gap-3 mb-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold ${
step.status === "completed"
? "bg-success-500 text-white"
: step.status === "in_progress"
? "bg-warning-500 text-white"
: "bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
}`}>
{step.status === "completed" ? <CheckCircleIcon className="size-5" /> : step.number}
</div>
<h4 className="font-medium text-gray-800 dark:text-white/90">{step.title}</h4>
</div>
<div className="flex items-center justify-between text-sm mb-2">
<div className="flex items-center gap-1.5">
{step.status === "completed" ? (
<>
<CheckCircleIcon className="size-4 text-success-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Completed</span>
</>
) : (
<>
<ClockIcon className="size-4 text-amber-500" />
<span className="text-gray-600 dark:text-gray-300 font-medium">Pending</span>
</>
)}
</div>
</div>
{step.count !== null && (
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
{step.count} {step.title.includes("Tasks") ? "tasks" : step.title.includes("Content") ? "pieces" : step.title.includes("Images") ? "images" : "items"}
</p>
)}
{step.status === "pending" && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate(step.path);
}}
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer group-hover:translate-x-1 transition-transform"
>
Start Now <ArrowRightIcon className="size-3" />
</button>
)}
</Link>
))}
</div>
{/* Enhanced Metric Cards */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 md:gap-6">
<EnhancedMetricCard
title="Total Tasks"
value={stats.tasks.total}
subtitle={`${stats.tasks.completed} completed • ${stats.tasks.pending} pending`}
trend={trends.tasks}
icon={<FileTextIcon className="size-6" />}
accentColor="blue"
href="/writer/tasks"
details={[
{ label: "Total Tasks", value: stats.tasks.total },
{ label: "Completed", value: stats.tasks.completed },
{ label: "Pending", value: stats.tasks.pending },
{ label: "In Progress", value: stats.tasks.inProgress },
{ label: "Avg Word Count", value: stats.tasks.avgWordCount },
]}
/>
<EnhancedMetricCard
title="Content Pieces"
value={stats.content.total}
subtitle={`${stats.content.published} published • ${stats.content.drafts} drafts`}
trend={trends.content}
icon={<PencilIcon className="size-6" />}
accentColor="green"
href="/writer/content"
details={[
{ label: "Total Content", value: stats.content.total },
{ label: "Published", value: stats.content.published },
{ label: "In Review", value: stats.content.review },
{ label: "Drafts", value: stats.content.drafts },
{ label: "Avg Word Count", value: stats.content.avgWordCount.toLocaleString() },
]}
/>
<EnhancedMetricCard
title="Images Generated"
value={stats.images.generated}
subtitle={`${stats.images.total} total • ${stats.images.pending} pending`}
trend={trends.images}
icon={<BoxIcon className="size-6" />}
accentColor="orange"
href="/writer/images"
details={[
{ label: "Generated", value: stats.images.generated },
{ label: "Total Images", value: stats.images.total },
{ label: "Pending", value: stats.images.pending },
{ label: "Failed", value: stats.images.failed },
]}
/>
<EnhancedMetricCard
title="Publish Rate"
value={`${stats.productivity.publishRate}%`}
subtitle={`${stats.content.published} of ${stats.content.total} published`}
icon={<BoltIcon className="size-6" />}
accentColor="purple"
href="/writer/published"
details={[
{ label: "Publish Rate", value: `${stats.productivity.publishRate}%` },
{ label: "Published", value: stats.content.published },
{ label: "Total Content", value: stats.content.total },
{ label: "This Week", value: stats.productivity.contentThisWeek },
{ label: "This Month", value: stats.productivity.contentThisMonth },
]}
/>
</div>
{/* Interactive Workflow Pipeline */}
<ComponentCard title="Writer Workflow Pipeline" desc="Track your content creation progress through each stage">
<WorkflowPipeline
steps={workflowSteps.map(step => ({
number: step.number,
title: step.title,
status: step.status === "completed" ? "completed" : step.status === "in_progress" ? "in_progress" : "pending",
count: step.count || 0,
path: step.path,
description: step.title,
details: step.status === "completed"
? `${step.title} completed with ${step.count} items`
: step.status === "pending"
? `${step.title} pending - ${step.count} items ready`
: `${step.title} in progress`,
}))}
onStepClick={(step) => {
navigate(step.path);
}}
showConnections={true}
/>
</ComponentCard>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">

View File

@@ -29,6 +29,8 @@ import { TaskIcon, PlusIcon, DownloadIcon } from '../../icons';
import { createTasksPageConfig } from '../../config/pages/tasks.config';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import ViewToggle, { ViewType } from '../../components/common/ViewToggle';
import { KanbanBoard, TaskList, Task as KanbanTask } from '../../components/tasks';
export default function Tasks() {
const toast = useToast();
@@ -58,6 +60,9 @@ export default function Tasks() {
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [showContent, setShowContent] = useState(false);
// View state
const [currentView, setCurrentView] = useState<ViewType>('table');
// Modal state
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
@@ -183,6 +188,43 @@ export default function Tasks() {
setCurrentPage(1);
}, [pageSize]);
// Map API Task to KanbanBoard Task
const mapTaskToKanbanTask = (task: Task): KanbanTask => {
// Map status from API to Kanban status
const statusMap: Record<string, "todo" | "in_progress" | "completed"> = {
'queued': 'todo',
'in_progress': 'in_progress',
'generating': 'in_progress',
'completed': 'completed',
'published': 'completed',
'draft': 'todo',
'review': 'in_progress',
};
const kanbanStatus = statusMap[task.status] || 'todo';
// Format due date if available (you might need to add this field to Task)
const dueDate = task.created_at ? new Date(task.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
}) : undefined;
return {
id: String(task.id),
title: task.title || 'Untitled Task',
status: kanbanStatus,
dueDate,
commentsCount: 0, // Add if you have this data
attachmentsCount: 0, // Add if you have this data
tags: task.cluster_name ? [{ label: task.cluster_name, color: 'brand' as const }] : undefined,
description: task.description || undefined,
assignee: undefined, // Add if you have assignee data
};
};
const kanbanTasks = useMemo(() => tasks.map(mapTaskToKanbanTask), [tasks]);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
@@ -545,111 +587,233 @@ export default function Tasks() {
}
};
const handleTaskClick = (kanbanTask: KanbanTask) => {
const apiTask = tasks.find(t => String(t.id) === kanbanTask.id);
if (apiTask) {
setEditingTask(apiTask);
setFormData({
title: apiTask.title || '',
description: apiTask.description || '',
keywords: apiTask.keywords || '',
cluster_id: apiTask.cluster_id || null,
content_structure: apiTask.content_structure || 'blog_post',
content_type: apiTask.content_type || 'blog_post',
status: apiTask.status || 'queued',
word_count: apiTask.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}
};
const handleTaskMove = async (taskId: string, newStatus: "todo" | "in_progress" | "completed") => {
const apiTask = tasks.find(t => String(t.id) === taskId);
if (!apiTask) return;
// Map Kanban status back to API status
const statusMap: Record<string, string> = {
'todo': 'queued',
'in_progress': 'in_progress',
'completed': 'completed',
};
try {
await updateTask(apiTask.id, { status: statusMap[newStatus] || 'queued' });
toast.success('Task status updated');
loadTasks();
} catch (error: any) {
toast.error(`Failed to update task: ${error.message}`);
}
};
return (
<>
<TablePageTemplate
title="Tasks"
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
subtitle="Manage content generation queue and tasks"
columns={pageConfig.columns}
data={tasks}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
} else if (key === 'cluster_id') {
setClusterFilter(stringValue);
} else if (key === 'content_structure') {
setStructureFilter(stringValue);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
}
setCurrentPage(1);
}}
onEdit={(row) => {
setEditingTask(row);
setFormData({
title: row.title || '',
description: row.description || '',
keywords: row.keywords || '',
cluster_id: row.cluster_id || null,
content_structure: row.content_structure || 'blog_post',
content_type: row.content_type || 'blog_post',
status: row.status || 'queued',
word_count: row.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}}
onCreate={() => {
resetForm();
setIsModalOpen(true);
}}
createLabel="Add Task"
onCreateIcon={<PlusIcon />}
onDelete={async (id: number) => {
await deleteTask(id);
loadTasks();
}}
onBulkDelete={async (ids: number[]) => {
const result = await bulkDeleteTasks(ids);
// Clear selection first
setSelectedIds([]);
// Reset to page 1 if we deleted all items on current page
if (currentPage > 1 && tasks.length <= ids.length) {
{/* View Toggle - Only show for Kanban/List views */}
{currentView !== 'table' && (
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<TaskIcon className="text-brand-500 size-6" />
Tasks
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 hidden sm:block">
Manage content generation queue and tasks
</p>
</div>
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
</div>
)}
{/* Table View */}
{currentView === 'table' && (
<div className="mb-4 flex justify-end">
<ViewToggle currentView={currentView} onViewChange={setCurrentView} />
</div>
<TablePageTemplate
title="Tasks"
titleIcon={<TaskIcon className="text-brand-500 size-5" />}
subtitle="Manage content generation queue and tasks"
columns={pageConfig.columns}
data={tasks}
loading={loading}
showContent={showContent}
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
status: statusFilter,
cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
} else if (key === 'cluster_id') {
setClusterFilter(stringValue);
} else if (key === 'content_structure') {
setStructureFilter(stringValue);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
}
setCurrentPage(1);
}
// Always reload data to refresh the table
await loadTasks();
return result;
}}
onBulkExport={handleBulkExport}
onBulkUpdateStatus={handleBulkUpdateStatus}
onBulkAction={handleBulkAction}
onRowAction={handleRowAction}
getItemDisplayName={(row: Task) => row.title}
onExport={async () => {
toast.info('Export functionality coming soon');
}}
onExportIcon={<DownloadIcon />}
selectionLabel="task"
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onFilterReset={() => {
setSearchTerm('');
setStatusFilter('');
setClusterFilter('');
setStructureFilter('');
setTypeFilter('');
setCurrentPage(1);
}}
/>
}}
onEdit={(row) => {
setEditingTask(row);
setFormData({
title: row.title || '',
description: row.description || '',
keywords: row.keywords || '',
cluster_id: row.cluster_id || null,
content_structure: row.content_structure || 'blog_post',
content_type: row.content_type || 'blog_post',
status: row.status || 'queued',
word_count: row.word_count || 0,
});
setIsEditMode(true);
setIsModalOpen(true);
}}
onCreate={() => {
resetForm();
setIsModalOpen(true);
}}
createLabel="Add Task"
onCreateIcon={<PlusIcon />}
onDelete={async (id: number) => {
await deleteTask(id);
loadTasks();
}}
onBulkDelete={async (ids: number[]) => {
const result = await bulkDeleteTasks(ids);
// Clear selection first
setSelectedIds([]);
// Reset to page 1 if we deleted all items on current page
if (currentPage > 1 && tasks.length <= ids.length) {
setCurrentPage(1);
}
// Always reload data to refresh the table
await loadTasks();
return result;
}}
onBulkExport={handleBulkExport}
onBulkUpdateStatus={handleBulkUpdateStatus}
onBulkAction={handleBulkAction}
onRowAction={handleRowAction}
getItemDisplayName={(row: Task) => row.title}
onExport={async () => {
toast.info('Export functionality coming soon');
}}
onExportIcon={<DownloadIcon />}
selectionLabel="task"
pagination={{
currentPage,
totalPages,
totalCount,
onPageChange: setCurrentPage,
}}
selection={{
selectedIds,
onSelectionChange: setSelectedIds,
}}
sorting={{
sortBy,
sortDirection,
onSort: handleSort,
}}
headerMetrics={headerMetrics}
onFilterReset={() => {
setSearchTerm('');
setStatusFilter('');
setClusterFilter('');
setStructureFilter('');
setTypeFilter('');
setCurrentPage(1);
}}
/>
)}
{/* Kanban View */}
{currentView === 'kanban' && (
<KanbanBoard
tasks={kanbanTasks}
onTaskClick={handleTaskClick}
onTaskMove={handleTaskMove}
onAddTask={() => {
resetForm();
setIsModalOpen(true);
}}
onFilterChange={(filter) => {
// Map filter to status filter
const statusMap: Record<string, string> = {
'All': '',
'Todo': 'queued',
'InProgress': 'in_progress',
'Completed': 'completed',
};
setStatusFilter(statusMap[filter] || '');
setCurrentPage(1);
loadTasks();
}}
/>
)}
{/* List View */}
{currentView === 'list' && (
<TaskList
tasks={kanbanTasks}
onTaskClick={handleTaskClick}
onTaskToggle={async (taskId, completed) => {
const apiTask = tasks.find(t => String(t.id) === taskId);
if (apiTask) {
try {
await updateTask(apiTask.id, { status: completed ? 'completed' : 'queued' });
toast.success('Task updated');
loadTasks();
} catch (error: any) {
toast.error(`Failed to update task: ${error.message}`);
}
}
}}
onAddTask={() => {
resetForm();
setIsModalOpen(true);
}}
onFilterChange={(filter) => {
// Map filter to status filter
const statusMap: Record<string, string> = {
'All': '',
'Todo': 'queued',
'InProgress': 'in_progress',
'Completed': 'completed',
};
setStatusFilter(statusMap[filter] || '');
setCurrentPage(1);
loadTasks();
}}
/>
)}
{/* Progress Modal for AI Functions */}
<ProgressModal