claude-team-dashboard 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-team-dashboard might be problematic. Click here for more details.
- package/CHANGELOG.md +76 -0
- package/LICENSE +21 -0
- package/README.md +722 -0
- package/cleanup.js +73 -0
- package/config.js +50 -0
- package/dist/assets/icons-Ijf8rQIc.js +1 -0
- package/dist/assets/index-Cqc1m1x_.css +1 -0
- package/dist/assets/index-jGy3ms0W.js +9 -0
- package/dist/assets/react-vendor-DbmSkCAF.js +1 -0
- package/dist/index.html +16 -0
- package/index.html +13 -0
- package/package.json +93 -0
- package/server.js +953 -0
- package/src/App.jsx +372 -0
- package/src/animations-enhanced.css +929 -0
- package/src/animations.css +783 -0
- package/src/components/ActivityFeed.jsx +289 -0
- package/src/components/AgentActivity.jsx +104 -0
- package/src/components/AgentCard.jsx +163 -0
- package/src/components/AgentOutputViewer.jsx +334 -0
- package/src/components/ArchiveViewer.jsx +283 -0
- package/src/components/ConnectionStatus.jsx +124 -0
- package/src/components/DetailedTaskProgress.jsx +126 -0
- package/src/components/ErrorBoundary.jsx +132 -0
- package/src/components/Header.jsx +154 -0
- package/src/components/LiveAgentStream.jsx +176 -0
- package/src/components/LiveCommunication.jsx +326 -0
- package/src/components/LiveMetrics.jsx +100 -0
- package/src/components/RealTimeMessages.jsx +298 -0
- package/src/components/SkeletonLoader.jsx +384 -0
- package/src/components/StatsOverview.jsx +209 -0
- package/src/components/SystemStatus.jsx +57 -0
- package/src/components/TaskList.jsx +306 -0
- package/src/components/TeamCard.jsx +126 -0
- package/src/components/TeamHistory.jsx +204 -0
- package/src/components/__tests__/ConnectionStatus.test.jsx +54 -0
- package/src/components/__tests__/StatsOverview.test.jsx +66 -0
- package/src/config/constants.js +59 -0
- package/src/hooks/useCounterAnimation.js +219 -0
- package/src/hooks/useWebSocket.js +76 -0
- package/src/index.css +1818 -0
- package/src/main.jsx +17 -0
- package/src/polish-enhancements.css +303 -0
- package/src/premium-visual-polish.css +830 -0
- package/src/responsive-enhancements.css +666 -0
- package/src/styles/theme.css +395 -0
- package/src/test/setup.js +19 -0
- package/start.js +36 -0
- package/vite.config.js +37 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Users, ListTodo, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
|
4
|
+
import { useCounterAnimation } from '../hooks/useCounterAnimation';
|
|
5
|
+
|
|
6
|
+
export function StatsOverview({ stats }) {
|
|
7
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setIsVisible(true);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
// Use optimized counter animation hook for each stat
|
|
14
|
+
// Hook returns a number value, not a function (false positive)
|
|
15
|
+
const animatedTotalTeams = useCounterAnimation(stats?.totalTeams ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
16
|
+
const animatedTotalAgents = useCounterAnimation(stats?.totalAgents ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
17
|
+
const animatedTotalTasks = useCounterAnimation(stats?.totalTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
18
|
+
const animatedPendingTasks = useCounterAnimation(stats?.pendingTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
19
|
+
const animatedInProgress = useCounterAnimation(stats?.inProgressTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
20
|
+
const animatedCompleted = useCounterAnimation(stats?.completedTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
21
|
+
const animatedBlocked = useCounterAnimation(stats?.blockedTasks ?? 0, 1000); // lgtm[js/invocation-of-non-function]
|
|
22
|
+
|
|
23
|
+
if (!stats) return null;
|
|
24
|
+
|
|
25
|
+
// Map stat keys to their animated values
|
|
26
|
+
const animatedValuesMap = {
|
|
27
|
+
totalTeams: animatedTotalTeams,
|
|
28
|
+
totalAgents: animatedTotalAgents,
|
|
29
|
+
totalTasks: animatedTotalTasks,
|
|
30
|
+
pendingTasks: animatedPendingTasks,
|
|
31
|
+
inProgressTasks: animatedInProgress,
|
|
32
|
+
completedTasks: animatedCompleted,
|
|
33
|
+
blockedTasks: animatedBlocked
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const statCards = [
|
|
37
|
+
{
|
|
38
|
+
label: 'Active Teams',
|
|
39
|
+
key: 'totalTeams',
|
|
40
|
+
icon: Users,
|
|
41
|
+
gradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(37, 99, 235, 0.15) 100%)',
|
|
42
|
+
iconColor: '#60a5fa',
|
|
43
|
+
glowColor: 'rgba(59, 130, 246, 0.4)',
|
|
44
|
+
borderColor: 'rgba(59, 130, 246, 0.3)'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'Total Agents',
|
|
48
|
+
key: 'totalAgents',
|
|
49
|
+
icon: Users,
|
|
50
|
+
gradient: 'linear-gradient(135deg, rgba(168, 85, 247, 0.25) 0%, rgba(147, 51, 234, 0.15) 100%)',
|
|
51
|
+
iconColor: '#c084fc',
|
|
52
|
+
glowColor: 'rgba(168, 85, 247, 0.4)',
|
|
53
|
+
borderColor: 'rgba(168, 85, 247, 0.3)'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Total Tasks',
|
|
57
|
+
key: 'totalTasks',
|
|
58
|
+
icon: ListTodo,
|
|
59
|
+
gradient: 'linear-gradient(135deg, rgba(6, 182, 212, 0.25) 0%, rgba(14, 165, 233, 0.15) 100%)',
|
|
60
|
+
iconColor: '#22d3ee',
|
|
61
|
+
glowColor: 'rgba(6, 182, 212, 0.4)',
|
|
62
|
+
borderColor: 'rgba(6, 182, 212, 0.3)'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: 'In Progress',
|
|
66
|
+
key: 'inProgressTasks',
|
|
67
|
+
icon: Clock,
|
|
68
|
+
gradient: 'linear-gradient(135deg, rgba(249, 115, 22, 0.25) 0%, rgba(251, 146, 60, 0.15) 100%)',
|
|
69
|
+
iconColor: '#fb923c',
|
|
70
|
+
glowColor: 'rgba(249, 115, 22, 0.4)',
|
|
71
|
+
borderColor: 'rgba(249, 115, 22, 0.3)'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: 'Completed',
|
|
75
|
+
key: 'completedTasks',
|
|
76
|
+
icon: CheckCircle,
|
|
77
|
+
gradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.25) 0%, rgba(21, 128, 61, 0.15) 100%)',
|
|
78
|
+
iconColor: '#4ade80',
|
|
79
|
+
glowColor: 'rgba(34, 197, 94, 0.4)',
|
|
80
|
+
borderColor: 'rgba(34, 197, 94, 0.3)'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: 'Blocked',
|
|
84
|
+
key: 'blockedTasks',
|
|
85
|
+
icon: AlertCircle,
|
|
86
|
+
gradient: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(220, 38, 38, 0.15) 100%)',
|
|
87
|
+
iconColor: '#f87171',
|
|
88
|
+
glowColor: 'rgba(239, 68, 68, 0.4)',
|
|
89
|
+
borderColor: 'rgba(239, 68, 68, 0.3)'
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className="rounded-2xl p-6 mb-6"
|
|
96
|
+
style={{
|
|
97
|
+
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.9) 100%)',
|
|
98
|
+
border: '1px solid rgba(249, 115, 22, 0.15)',
|
|
99
|
+
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
|
|
100
|
+
backdropFilter: 'blur(16px)'
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
|
104
|
+
{statCards.map((stat, index) => {
|
|
105
|
+
const Icon = stat.icon;
|
|
106
|
+
const value = animatedValuesMap[stat.key];
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
key={stat.key}
|
|
111
|
+
className="relative group rounded-xl p-4 transition-all duration-300"
|
|
112
|
+
style={{
|
|
113
|
+
background: stat.gradient,
|
|
114
|
+
border: `1px solid ${stat.borderColor}`,
|
|
115
|
+
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08)`,
|
|
116
|
+
opacity: isVisible ? 1 : 0,
|
|
117
|
+
transform: isVisible ? 'translateY(0)' : 'translateY(10px)',
|
|
118
|
+
transitionDelay: `${index * 80}ms`
|
|
119
|
+
}}
|
|
120
|
+
onMouseEnter={(e) => {
|
|
121
|
+
e.currentTarget.style.transform = 'translateY(-4px) scale(1.02)';
|
|
122
|
+
e.currentTarget.style.boxShadow = `0 8px 24px ${stat.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.12)`;
|
|
123
|
+
e.currentTarget.style.borderColor = stat.borderColor;
|
|
124
|
+
}}
|
|
125
|
+
onMouseLeave={(e) => {
|
|
126
|
+
e.currentTarget.style.transform = 'translateY(0) scale(1)';
|
|
127
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08)';
|
|
128
|
+
e.currentTarget.style.borderColor = stat.borderColor;
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{/* Icon */}
|
|
132
|
+
<div
|
|
133
|
+
className="inline-flex p-2.5 rounded-lg mb-3 transition-transform duration-300 group-hover:scale-110"
|
|
134
|
+
style={{
|
|
135
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
136
|
+
boxShadow: `0 2px 8px ${stat.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.1)`,
|
|
137
|
+
border: `1px solid ${stat.borderColor}`
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<Icon
|
|
141
|
+
className="h-5 w-5"
|
|
142
|
+
style={{
|
|
143
|
+
color: stat.iconColor,
|
|
144
|
+
filter: `drop-shadow(0 0 8px ${stat.glowColor})`
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Value */}
|
|
150
|
+
<div className="mb-1">
|
|
151
|
+
<p
|
|
152
|
+
className="text-3xl font-extrabold tabular-nums"
|
|
153
|
+
style={{
|
|
154
|
+
color: '#ffffff',
|
|
155
|
+
letterSpacing: '-0.03em',
|
|
156
|
+
textShadow: `0 2px 4px rgba(0, 0, 0, 0.3), 0 0 12px ${stat.glowColor}`,
|
|
157
|
+
lineHeight: 1
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{value}
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Label */}
|
|
165
|
+
<p
|
|
166
|
+
className="text-xs font-semibold uppercase tracking-wider"
|
|
167
|
+
style={{
|
|
168
|
+
color: 'rgba(209, 213, 219, 0.7)',
|
|
169
|
+
letterSpacing: '0.05em'
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{stat.label}
|
|
173
|
+
</p>
|
|
174
|
+
|
|
175
|
+
{/* Hover Glow Effect */}
|
|
176
|
+
<div
|
|
177
|
+
className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
|
|
178
|
+
style={{
|
|
179
|
+
background: `radial-gradient(circle at center, ${stat.glowColor}, transparent 70%)`
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
|
|
183
|
+
{/* Top Border Accent */}
|
|
184
|
+
<div
|
|
185
|
+
className="absolute top-0 left-0 right-0 h-1 rounded-t-xl"
|
|
186
|
+
style={{
|
|
187
|
+
background: `linear-gradient(90deg, transparent, ${stat.iconColor}, transparent)`,
|
|
188
|
+
opacity: 0.6
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
StatsOverview.propTypes = {
|
|
200
|
+
stats: PropTypes.shape({
|
|
201
|
+
totalTeams: PropTypes.number,
|
|
202
|
+
totalAgents: PropTypes.number,
|
|
203
|
+
totalTasks: PropTypes.number,
|
|
204
|
+
pendingTasks: PropTypes.number,
|
|
205
|
+
inProgressTasks: PropTypes.number,
|
|
206
|
+
completedTasks: PropTypes.number,
|
|
207
|
+
blockedTasks: PropTypes.number
|
|
208
|
+
})
|
|
209
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Server, Database, Wifi, Clock } from 'lucide-react';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
5
|
+
dayjs.extend(relativeTime);
|
|
6
|
+
|
|
7
|
+
export function SystemStatus({ isConnected, lastUpdate }) {
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="card">
|
|
11
|
+
<div className="flex items-center gap-2 mb-4">
|
|
12
|
+
<Server className="h-5 w-5 text-claude-orange" />
|
|
13
|
+
<h3 className="text-lg font-semibold text-white">System Status</h3>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="space-y-3">
|
|
17
|
+
<div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
|
|
18
|
+
<div className="flex items-center gap-2">
|
|
19
|
+
<Wifi className={`h-4 w-4 ${isConnected ? 'text-green-400' : 'text-red-400'}`} />
|
|
20
|
+
<span className="text-sm text-gray-300">WebSocket</span>
|
|
21
|
+
</div>
|
|
22
|
+
<span className={`text-xs font-semibold ${isConnected ? 'text-green-400' : 'text-red-400'}`}>
|
|
23
|
+
{isConnected ? 'Connected' : 'Disconnected'}
|
|
24
|
+
</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
|
|
28
|
+
<div className="flex items-center gap-2">
|
|
29
|
+
<Server className="h-4 w-4 text-blue-400" />
|
|
30
|
+
<span className="text-sm text-gray-300">Backend API</span>
|
|
31
|
+
</div>
|
|
32
|
+
<span className="text-xs font-semibold text-green-400">Running</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
|
|
36
|
+
<div className="flex items-center gap-2">
|
|
37
|
+
<Database className="h-4 w-4 text-purple-400" />
|
|
38
|
+
<span className="text-sm text-gray-300">File Watchers</span>
|
|
39
|
+
</div>
|
|
40
|
+
<span className="text-xs font-semibold text-green-400">Active</span>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{lastUpdate && (
|
|
44
|
+
<div className="flex items-center justify-between p-3 rounded-lg" style={{ background: '#1e293b' }}>
|
|
45
|
+
<div className="flex items-center gap-2">
|
|
46
|
+
<Clock className="h-4 w-4 text-cyan-400" />
|
|
47
|
+
<span className="text-sm text-gray-300">Last Update</span>
|
|
48
|
+
</div>
|
|
49
|
+
<span className="text-xs font-semibold text-cyan-400">
|
|
50
|
+
{dayjs(new Date(lastUpdate.timestamp || Date.now())).fromNow()}
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { Circle, Clock, CheckCircle, AlertCircle, User, ChevronDown, ChevronUp } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export function TaskList({ tasks }) {
|
|
6
|
+
const [expandedTasks, setExpandedTasks] = useState(new Set());
|
|
7
|
+
|
|
8
|
+
if (!tasks || tasks.length === 0) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className="text-center py-12 rounded-2xl"
|
|
12
|
+
style={{
|
|
13
|
+
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(15, 23, 42, 0.5) 100%)',
|
|
14
|
+
border: '1px dashed rgba(156, 163, 175, 0.3)'
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<Circle className="h-12 w-12 text-gray-600 mx-auto mb-3 opacity-50" />
|
|
18
|
+
<p className="text-gray-400 text-sm">No tasks yet</p>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const toggleTaskExpanded = (taskId) => {
|
|
24
|
+
setExpandedTasks(prev => {
|
|
25
|
+
const newSet = new Set(prev);
|
|
26
|
+
if (newSet.has(taskId)) {
|
|
27
|
+
newSet.delete(taskId);
|
|
28
|
+
} else {
|
|
29
|
+
newSet.add(taskId);
|
|
30
|
+
}
|
|
31
|
+
return newSet;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getStatusIcon = (status) => {
|
|
36
|
+
switch (status) {
|
|
37
|
+
case 'pending':
|
|
38
|
+
return (
|
|
39
|
+
<Circle
|
|
40
|
+
className="h-6 w-6"
|
|
41
|
+
style={{
|
|
42
|
+
color: '#facc15',
|
|
43
|
+
filter: 'drop-shadow(0 0 6px rgba(250, 204, 21, 0.5))'
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
case 'in_progress':
|
|
48
|
+
return (
|
|
49
|
+
<Clock
|
|
50
|
+
className="h-6 w-6 animate-spin"
|
|
51
|
+
style={{
|
|
52
|
+
color: '#60a5fa',
|
|
53
|
+
filter: 'drop-shadow(0 0 8px rgba(96, 165, 250, 0.5))',
|
|
54
|
+
animationDuration: '3s'
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
case 'completed':
|
|
59
|
+
return (
|
|
60
|
+
<CheckCircle
|
|
61
|
+
className="h-6 w-6"
|
|
62
|
+
style={{
|
|
63
|
+
color: '#4ade80',
|
|
64
|
+
filter: 'drop-shadow(0 0 8px rgba(74, 222, 128, 0.5))'
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
default:
|
|
69
|
+
return <Circle className="h-6 w-6 text-gray-400" />;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getStatusConfig = (status, blockedBy) => {
|
|
74
|
+
if (blockedBy && blockedBy.length > 0) {
|
|
75
|
+
return {
|
|
76
|
+
label: 'Blocked',
|
|
77
|
+
bgGradient: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(220, 38, 38, 0.15) 100%)',
|
|
78
|
+
textColor: '#f87171',
|
|
79
|
+
borderColor: 'rgba(239, 68, 68, 0.4)',
|
|
80
|
+
glowColor: 'rgba(239, 68, 68, 0.3)'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'pending':
|
|
86
|
+
return {
|
|
87
|
+
label: 'Pending',
|
|
88
|
+
bgGradient: 'linear-gradient(135deg, rgba(234, 179, 8, 0.2) 0%, rgba(202, 138, 4, 0.12) 100%)',
|
|
89
|
+
textColor: '#facc15',
|
|
90
|
+
borderColor: 'rgba(234, 179, 8, 0.4)',
|
|
91
|
+
glowColor: 'rgba(234, 179, 8, 0.3)'
|
|
92
|
+
};
|
|
93
|
+
case 'in_progress':
|
|
94
|
+
return {
|
|
95
|
+
label: 'In Progress',
|
|
96
|
+
bgGradient: 'linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(37, 99, 235, 0.15) 100%)',
|
|
97
|
+
textColor: '#60a5fa',
|
|
98
|
+
borderColor: 'rgba(59, 130, 246, 0.5)',
|
|
99
|
+
glowColor: 'rgba(59, 130, 246, 0.35)'
|
|
100
|
+
};
|
|
101
|
+
case 'completed':
|
|
102
|
+
return {
|
|
103
|
+
label: 'Completed',
|
|
104
|
+
bgGradient: 'linear-gradient(135deg, rgba(34, 197, 94, 0.25) 0%, rgba(21, 128, 61, 0.15) 100%)',
|
|
105
|
+
textColor: '#4ade80',
|
|
106
|
+
borderColor: 'rgba(34, 197, 94, 0.5)',
|
|
107
|
+
glowColor: 'rgba(34, 197, 94, 0.3)'
|
|
108
|
+
};
|
|
109
|
+
default:
|
|
110
|
+
return {
|
|
111
|
+
label: 'Unknown',
|
|
112
|
+
bgGradient: 'rgba(55, 65, 81, 0.3)',
|
|
113
|
+
textColor: '#9ca3af',
|
|
114
|
+
borderColor: 'rgba(75, 85, 99, 0.4)',
|
|
115
|
+
glowColor: 'rgba(75, 85, 99, 0.2)'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="space-y-3">
|
|
122
|
+
{tasks.map((task, index) => {
|
|
123
|
+
const statusConfig = getStatusConfig(task.status, task.blockedBy);
|
|
124
|
+
const isExpanded = expandedTasks.has(task.id || index);
|
|
125
|
+
const hasDescription = task.description && task.description.length > 100;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key={task.id || index}
|
|
130
|
+
className="group relative rounded-2xl p-5 transition-all duration-300"
|
|
131
|
+
style={{
|
|
132
|
+
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.6) 0%, rgba(15, 23, 42, 0.5) 100%)',
|
|
133
|
+
border: '1px solid rgba(75, 85, 99, 0.3)',
|
|
134
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)',
|
|
135
|
+
animationDelay: `${index * 60}ms`
|
|
136
|
+
}}
|
|
137
|
+
onMouseEnter={(e) => {
|
|
138
|
+
e.currentTarget.style.transform = 'translateX(6px)';
|
|
139
|
+
e.currentTarget.style.borderColor = statusConfig.borderColor;
|
|
140
|
+
e.currentTarget.style.boxShadow = `0 6px 20px ${statusConfig.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.08)`;
|
|
141
|
+
}}
|
|
142
|
+
onMouseLeave={(e) => {
|
|
143
|
+
e.currentTarget.style.transform = 'translateX(0)';
|
|
144
|
+
e.currentTarget.style.borderColor = 'rgba(75, 85, 99, 0.3)';
|
|
145
|
+
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.05)';
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{/* Status Bar */}
|
|
149
|
+
<div
|
|
150
|
+
className="absolute left-0 top-0 bottom-0 w-1 rounded-l-2xl"
|
|
151
|
+
style={{
|
|
152
|
+
background: statusConfig.bgGradient,
|
|
153
|
+
boxShadow: `0 0 12px ${statusConfig.glowColor}`
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
<div className="flex items-start gap-4">
|
|
158
|
+
{/* Status Icon */}
|
|
159
|
+
<div className="mt-0.5 flex-shrink-0">
|
|
160
|
+
{getStatusIcon(task.status)}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Task Content */}
|
|
164
|
+
<div className="flex-1 min-w-0">
|
|
165
|
+
{/* Header */}
|
|
166
|
+
<div className="flex items-start justify-between gap-3 mb-3">
|
|
167
|
+
<h5
|
|
168
|
+
className="text-white font-bold text-base leading-tight"
|
|
169
|
+
style={{
|
|
170
|
+
letterSpacing: '-0.01em',
|
|
171
|
+
textShadow: '0 1px 2px rgba(0, 0, 0, 0.3)'
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{task.subject}
|
|
175
|
+
</h5>
|
|
176
|
+
|
|
177
|
+
{/* Status Badge */}
|
|
178
|
+
<span
|
|
179
|
+
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold uppercase tracking-wider flex-shrink-0"
|
|
180
|
+
style={{
|
|
181
|
+
background: statusConfig.bgGradient,
|
|
182
|
+
color: statusConfig.textColor,
|
|
183
|
+
border: `1px solid ${statusConfig.borderColor}`,
|
|
184
|
+
boxShadow: `0 2px 8px ${statusConfig.glowColor}, inset 0 1px 0 rgba(255, 255, 255, 0.15)`,
|
|
185
|
+
textShadow: `0 0 10px ${statusConfig.glowColor}`
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{statusConfig.label}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Description */}
|
|
193
|
+
{task.description && (
|
|
194
|
+
<div className="mb-3">
|
|
195
|
+
<p
|
|
196
|
+
className={`text-sm leading-relaxed ${!isExpanded && hasDescription ? 'line-clamp-2' : ''}`}
|
|
197
|
+
style={{
|
|
198
|
+
color: 'rgba(209, 213, 219, 0.85)',
|
|
199
|
+
letterSpacing: '-0.01em'
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{task.description}
|
|
203
|
+
</p>
|
|
204
|
+
{hasDescription && (
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => toggleTaskExpanded(task.id || index)}
|
|
207
|
+
className="flex items-center gap-1 text-xs font-medium mt-2 transition-colors duration-200"
|
|
208
|
+
style={{
|
|
209
|
+
color: statusConfig.textColor
|
|
210
|
+
}}
|
|
211
|
+
aria-label={isExpanded ? "Show less description" : "Show full description"}
|
|
212
|
+
aria-expanded={isExpanded}
|
|
213
|
+
>
|
|
214
|
+
{isExpanded ? (
|
|
215
|
+
<>
|
|
216
|
+
<ChevronUp className="h-3 w-3" />
|
|
217
|
+
Show less
|
|
218
|
+
</>
|
|
219
|
+
) : (
|
|
220
|
+
<>
|
|
221
|
+
<ChevronDown className="h-3 w-3" />
|
|
222
|
+
Read more
|
|
223
|
+
</>
|
|
224
|
+
)}
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Tags and Metadata */}
|
|
231
|
+
<div className="flex flex-wrap gap-2">
|
|
232
|
+
{task.owner && (
|
|
233
|
+
<div
|
|
234
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
|
|
235
|
+
style={{
|
|
236
|
+
background: 'rgba(59, 130, 246, 0.15)',
|
|
237
|
+
color: '#93c5fd',
|
|
238
|
+
border: '1px solid rgba(59, 130, 246, 0.3)'
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<User className="h-3.5 w-3.5" />
|
|
242
|
+
<span>{task.owner}</span>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{task.blockedBy && task.blockedBy.length > 0 && (
|
|
247
|
+
<div
|
|
248
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
|
|
249
|
+
style={{
|
|
250
|
+
background: 'rgba(239, 68, 68, 0.15)',
|
|
251
|
+
color: '#fca5a5',
|
|
252
|
+
border: '1px solid rgba(239, 68, 68, 0.35)',
|
|
253
|
+
boxShadow: '0 0 12px rgba(239, 68, 68, 0.2)'
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
257
|
+
<span>Blocked by {task.blockedBy.length}</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{task.blocks && task.blocks.length > 0 && (
|
|
262
|
+
<div
|
|
263
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
|
|
264
|
+
style={{
|
|
265
|
+
background: 'rgba(249, 115, 22, 0.15)',
|
|
266
|
+
color: '#fdba74',
|
|
267
|
+
border: '1px solid rgba(249, 115, 22, 0.3)'
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
271
|
+
<span>Blocks {task.blocks.length}</span>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Hover Shine Effect */}
|
|
279
|
+
<div
|
|
280
|
+
className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
|
|
281
|
+
style={{
|
|
282
|
+
background: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.03) 50%, transparent 100%)'
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
TaskList.propTypes = {
|
|
293
|
+
tasks: PropTypes.arrayOf(
|
|
294
|
+
PropTypes.shape({
|
|
295
|
+
id: PropTypes.string,
|
|
296
|
+
subject: PropTypes.string.isRequired,
|
|
297
|
+
description: PropTypes.string,
|
|
298
|
+
status: PropTypes.oneOf(['pending', 'in_progress', 'completed']).isRequired,
|
|
299
|
+
owner: PropTypes.string,
|
|
300
|
+
blockedBy: PropTypes.array,
|
|
301
|
+
blocks: PropTypes.array,
|
|
302
|
+
createdAt: PropTypes.number,
|
|
303
|
+
updatedAt: PropTypes.number
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
};
|