enhanced ui
This commit is contained in:
169
frontend/src/components/dashboard/EnhancedMetricCard.tsx
Normal file
169
frontend/src/components/dashboard/EnhancedMetricCard.tsx
Normal 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;
|
||||
}
|
||||
|
||||
150
frontend/src/components/dashboard/WorkflowPipeline.tsx
Normal file
150
frontend/src/components/dashboard/WorkflowPipeline.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user