@siteboon/claude-code-ui 1.8.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.
- package/.env.example +12 -0
- package/.nvmrc +1 -0
- package/LICENSE +675 -0
- package/README.md +275 -0
- package/index.html +48 -0
- package/package.json +84 -0
- package/postcss.config.js +6 -0
- package/public/convert-icons.md +53 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +9 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-128x128.svg +12 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-144x144.svg +12 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-152x152.svg +12 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-192x192.svg +12 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-384x384.svg +12 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-512x512.svg +12 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-72x72.svg +12 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/icon-96x96.svg +12 -0
- package/public/icons/icon-template.svg +12 -0
- package/public/logo.svg +9 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/cli-selection.png +0 -0
- package/public/screenshots/desktop-main.png +0 -0
- package/public/screenshots/mobile-chat.png +0 -0
- package/public/screenshots/tools-modal.png +0 -0
- package/public/sw.js +49 -0
- package/server/claude-cli.js +391 -0
- package/server/cursor-cli.js +250 -0
- package/server/database/db.js +86 -0
- package/server/database/init.sql +16 -0
- package/server/index.js +1167 -0
- package/server/middleware/auth.js +80 -0
- package/server/projects.js +1063 -0
- package/server/routes/auth.js +135 -0
- package/server/routes/cursor.js +794 -0
- package/server/routes/git.js +823 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +552 -0
- package/server/routes/taskmaster.js +1971 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/src/App.jsx +751 -0
- package/src/components/ChatInterface.jsx +3485 -0
- package/src/components/ClaudeLogo.jsx +11 -0
- package/src/components/ClaudeStatus.jsx +107 -0
- package/src/components/CodeEditor.jsx +422 -0
- package/src/components/CreateTaskModal.jsx +88 -0
- package/src/components/CursorLogo.jsx +9 -0
- package/src/components/DarkModeToggle.jsx +35 -0
- package/src/components/DiffViewer.jsx +41 -0
- package/src/components/ErrorBoundary.jsx +73 -0
- package/src/components/FileTree.jsx +480 -0
- package/src/components/GitPanel.jsx +1283 -0
- package/src/components/ImageViewer.jsx +54 -0
- package/src/components/LoginForm.jsx +110 -0
- package/src/components/MainContent.jsx +577 -0
- package/src/components/MicButton.jsx +272 -0
- package/src/components/MobileNav.jsx +88 -0
- package/src/components/NextTaskBanner.jsx +695 -0
- package/src/components/PRDEditor.jsx +871 -0
- package/src/components/ProtectedRoute.jsx +44 -0
- package/src/components/QuickSettingsPanel.jsx +262 -0
- package/src/components/Settings.jsx +2023 -0
- package/src/components/SetupForm.jsx +135 -0
- package/src/components/Shell.jsx +663 -0
- package/src/components/Sidebar.jsx +1665 -0
- package/src/components/StandaloneShell.jsx +106 -0
- package/src/components/TaskCard.jsx +210 -0
- package/src/components/TaskDetail.jsx +406 -0
- package/src/components/TaskIndicator.jsx +108 -0
- package/src/components/TaskList.jsx +1054 -0
- package/src/components/TaskMasterSetupWizard.jsx +603 -0
- package/src/components/TaskMasterStatus.jsx +86 -0
- package/src/components/TodoList.jsx +91 -0
- package/src/components/Tooltip.jsx +91 -0
- package/src/components/ui/badge.jsx +31 -0
- package/src/components/ui/button.jsx +46 -0
- package/src/components/ui/input.jsx +19 -0
- package/src/components/ui/scroll-area.jsx +23 -0
- package/src/contexts/AuthContext.jsx +158 -0
- package/src/contexts/TaskMasterContext.jsx +324 -0
- package/src/contexts/TasksSettingsContext.jsx +95 -0
- package/src/contexts/ThemeContext.jsx +94 -0
- package/src/contexts/WebSocketContext.jsx +29 -0
- package/src/hooks/useAudioRecorder.js +109 -0
- package/src/hooks/useVersionCheck.js +39 -0
- package/src/index.css +822 -0
- package/src/lib/utils.js +6 -0
- package/src/main.jsx +10 -0
- package/src/utils/api.js +141 -0
- package/src/utils/websocket.js +109 -0
- package/src/utils/whisper.js +37 -0
- package/tailwind.config.js +63 -0
- package/vite.config.js +29 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { Search, Filter, ArrowUpDown, ArrowUp, ArrowDown, List, Grid, ChevronDown, Columns, Plus, Settings, Terminal, FileText, HelpCircle, X } from 'lucide-react';
|
|
3
|
+
import { cn } from '../lib/utils';
|
|
4
|
+
import TaskCard from './TaskCard';
|
|
5
|
+
import CreateTaskModal from './CreateTaskModal';
|
|
6
|
+
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
|
7
|
+
import Shell from './Shell';
|
|
8
|
+
import { api } from '../utils/api';
|
|
9
|
+
|
|
10
|
+
const TaskList = ({
|
|
11
|
+
tasks = [],
|
|
12
|
+
onTaskClick,
|
|
13
|
+
className = '',
|
|
14
|
+
showParentTasks = false,
|
|
15
|
+
defaultView = 'kanban', // 'list', 'grid', or 'kanban'
|
|
16
|
+
currentProject,
|
|
17
|
+
onTaskCreated,
|
|
18
|
+
onShowPRDEditor,
|
|
19
|
+
existingPRDs = [],
|
|
20
|
+
onRefreshPRDs
|
|
21
|
+
}) => {
|
|
22
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
23
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
24
|
+
const [priorityFilter, setPriorityFilter] = useState('all');
|
|
25
|
+
const [sortBy, setSortBy] = useState('id'); // 'id', 'title', 'status', 'priority', 'updated'
|
|
26
|
+
const [sortOrder, setSortOrder] = useState('asc'); // 'asc' or 'desc'
|
|
27
|
+
const [viewMode, setViewMode] = useState(defaultView);
|
|
28
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
29
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
30
|
+
const [showCLI, setShowCLI] = useState(false);
|
|
31
|
+
const [showHelpGuide, setShowHelpGuide] = useState(false);
|
|
32
|
+
const [isTaskMasterComplete, setIsTaskMasterComplete] = useState(false);
|
|
33
|
+
const [showPRDDropdown, setShowPRDDropdown] = useState(false);
|
|
34
|
+
|
|
35
|
+
const { projectTaskMaster, refreshProjects, refreshTasks, setCurrentProject } = useTaskMaster();
|
|
36
|
+
|
|
37
|
+
// Close PRD dropdown when clicking outside
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const handleClickOutside = (event) => {
|
|
40
|
+
if (showPRDDropdown && !event.target.closest('.relative')) {
|
|
41
|
+
setShowPRDDropdown(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
46
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
47
|
+
}, [showPRDDropdown]);
|
|
48
|
+
|
|
49
|
+
// Get unique status values from tasks
|
|
50
|
+
const statuses = useMemo(() => {
|
|
51
|
+
const statusSet = new Set(tasks.map(task => task.status).filter(Boolean));
|
|
52
|
+
return Array.from(statusSet).sort();
|
|
53
|
+
}, [tasks]);
|
|
54
|
+
|
|
55
|
+
// Get unique priority values from tasks
|
|
56
|
+
const priorities = useMemo(() => {
|
|
57
|
+
const prioritySet = new Set(tasks.map(task => task.priority).filter(Boolean));
|
|
58
|
+
return Array.from(prioritySet).sort();
|
|
59
|
+
}, [tasks]);
|
|
60
|
+
|
|
61
|
+
// Filter and sort tasks
|
|
62
|
+
const filteredAndSortedTasks = useMemo(() => {
|
|
63
|
+
let filtered = tasks.filter(task => {
|
|
64
|
+
// Text search
|
|
65
|
+
const searchLower = searchTerm.toLowerCase();
|
|
66
|
+
const matchesSearch = !searchTerm ||
|
|
67
|
+
task.title.toLowerCase().includes(searchLower) ||
|
|
68
|
+
task.description?.toLowerCase().includes(searchLower) ||
|
|
69
|
+
task.id.toString().includes(searchLower);
|
|
70
|
+
|
|
71
|
+
// Status filter
|
|
72
|
+
const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
|
|
73
|
+
|
|
74
|
+
// Priority filter
|
|
75
|
+
const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
|
|
76
|
+
|
|
77
|
+
return matchesSearch && matchesStatus && matchesPriority;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Sort tasks
|
|
81
|
+
filtered.sort((a, b) => {
|
|
82
|
+
let aVal, bVal;
|
|
83
|
+
|
|
84
|
+
switch (sortBy) {
|
|
85
|
+
case 'title':
|
|
86
|
+
aVal = a.title.toLowerCase();
|
|
87
|
+
bVal = b.title.toLowerCase();
|
|
88
|
+
break;
|
|
89
|
+
case 'status':
|
|
90
|
+
// Custom status ordering: pending, in-progress, done, blocked, deferred, cancelled
|
|
91
|
+
const statusOrder = { pending: 1, 'in-progress': 2, done: 3, blocked: 4, deferred: 5, cancelled: 6 };
|
|
92
|
+
aVal = statusOrder[a.status] || 99;
|
|
93
|
+
bVal = statusOrder[b.status] || 99;
|
|
94
|
+
break;
|
|
95
|
+
case 'priority':
|
|
96
|
+
// Custom priority ordering: high should be sorted first in descending
|
|
97
|
+
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
|
98
|
+
aVal = priorityOrder[a.priority] || 0;
|
|
99
|
+
bVal = priorityOrder[b.priority] || 0;
|
|
100
|
+
break;
|
|
101
|
+
case 'updated':
|
|
102
|
+
aVal = new Date(a.updatedAt || a.createdAt || 0);
|
|
103
|
+
bVal = new Date(b.updatedAt || b.createdAt || 0);
|
|
104
|
+
break;
|
|
105
|
+
case 'id':
|
|
106
|
+
default:
|
|
107
|
+
// Handle numeric and dotted IDs (1, 1.1, 1.2, 2, 2.1, etc.)
|
|
108
|
+
const parseId = (id) => {
|
|
109
|
+
const parts = id.toString().split('.');
|
|
110
|
+
return parts.map(part => parseInt(part, 10));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const aIds = parseId(a.id);
|
|
114
|
+
const bIds = parseId(b.id);
|
|
115
|
+
|
|
116
|
+
// Compare each part
|
|
117
|
+
for (let i = 0; i < Math.max(aIds.length, bIds.length); i++) {
|
|
118
|
+
const aId = aIds[i] || 0;
|
|
119
|
+
const bId = bIds[i] || 0;
|
|
120
|
+
if (aId !== bId) {
|
|
121
|
+
aVal = aId;
|
|
122
|
+
bVal = bId;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (sortBy === 'updated') {
|
|
130
|
+
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof aVal === 'string') {
|
|
134
|
+
return sortOrder === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return filtered;
|
|
141
|
+
}, [tasks, searchTerm, statusFilter, priorityFilter, sortBy, sortOrder]);
|
|
142
|
+
|
|
143
|
+
// Organize tasks by status for Kanban view
|
|
144
|
+
const kanbanColumns = useMemo(() => {
|
|
145
|
+
const allColumns = [
|
|
146
|
+
{
|
|
147
|
+
id: 'pending',
|
|
148
|
+
title: '📋 To Do',
|
|
149
|
+
status: 'pending',
|
|
150
|
+
color: 'bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700',
|
|
151
|
+
headerColor: 'bg-slate-100 dark:bg-slate-800 text-slate-800 dark:text-slate-200'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'in-progress',
|
|
155
|
+
title: '🚀 In Progress',
|
|
156
|
+
status: 'in-progress',
|
|
157
|
+
color: 'bg-blue-50 dark:bg-blue-900/50 border-blue-200 dark:border-blue-700',
|
|
158
|
+
headerColor: 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'done',
|
|
162
|
+
title: '✅ Done',
|
|
163
|
+
status: 'done',
|
|
164
|
+
color: 'bg-emerald-50 dark:bg-emerald-900/50 border-emerald-200 dark:border-emerald-700',
|
|
165
|
+
headerColor: 'bg-emerald-100 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'blocked',
|
|
169
|
+
title: '🚫 Blocked',
|
|
170
|
+
status: 'blocked',
|
|
171
|
+
color: 'bg-red-50 dark:bg-red-900/50 border-red-200 dark:border-red-700',
|
|
172
|
+
headerColor: 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200'
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'deferred',
|
|
176
|
+
title: '⏳ Deferred',
|
|
177
|
+
status: 'deferred',
|
|
178
|
+
color: 'bg-amber-50 dark:bg-amber-900/50 border-amber-200 dark:border-amber-700',
|
|
179
|
+
headerColor: 'bg-amber-100 dark:bg-amber-800 text-amber-800 dark:text-amber-200'
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'cancelled',
|
|
183
|
+
title: '❌ Cancelled',
|
|
184
|
+
status: 'cancelled',
|
|
185
|
+
color: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700',
|
|
186
|
+
headerColor: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200'
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// Only show columns that have tasks or are part of the main workflow
|
|
191
|
+
const mainWorkflowStatuses = ['pending', 'in-progress', 'done'];
|
|
192
|
+
const columnsWithTasks = allColumns.filter(column => {
|
|
193
|
+
const hasTask = filteredAndSortedTasks.some(task => task.status === column.status);
|
|
194
|
+
const isMainWorkflow = mainWorkflowStatuses.includes(column.status);
|
|
195
|
+
return hasTask || isMainWorkflow;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return columnsWithTasks.map(column => ({
|
|
199
|
+
...column,
|
|
200
|
+
tasks: filteredAndSortedTasks.filter(task => task.status === column.status)
|
|
201
|
+
}));
|
|
202
|
+
}, [filteredAndSortedTasks]);
|
|
203
|
+
|
|
204
|
+
const handleSortChange = (newSortBy) => {
|
|
205
|
+
if (sortBy === newSortBy) {
|
|
206
|
+
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
207
|
+
} else {
|
|
208
|
+
setSortBy(newSortBy);
|
|
209
|
+
setSortOrder('asc');
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const clearFilters = () => {
|
|
214
|
+
setSearchTerm('');
|
|
215
|
+
setStatusFilter('all');
|
|
216
|
+
setPriorityFilter('all');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const getSortIcon = (field) => {
|
|
220
|
+
if (sortBy !== field) return <ArrowUpDown className="w-4 h-4" />;
|
|
221
|
+
return sortOrder === 'asc' ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (tasks.length === 0) {
|
|
225
|
+
// Check if TaskMaster is configured by looking for .taskmaster directory
|
|
226
|
+
const hasTaskMasterDirectory = currentProject?.taskMasterConfigured ||
|
|
227
|
+
currentProject?.taskmaster?.hasTaskmaster ||
|
|
228
|
+
projectTaskMaster?.hasTaskmaster;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className={cn('text-center py-12', className)}>
|
|
232
|
+
{!hasTaskMasterDirectory ? (
|
|
233
|
+
// TaskMaster not configured
|
|
234
|
+
<div className="max-w-md mx-auto">
|
|
235
|
+
<div className="text-blue-600 dark:text-blue-400 mb-4">
|
|
236
|
+
<Settings className="w-12 h-12 mx-auto mb-4" />
|
|
237
|
+
</div>
|
|
238
|
+
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
|
239
|
+
TaskMaster AI is not configured
|
|
240
|
+
</h3>
|
|
241
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
|
242
|
+
TaskMaster helps break down complex projects into manageable tasks with AI-powered assistance
|
|
243
|
+
</p>
|
|
244
|
+
|
|
245
|
+
{/* What is TaskMaster section */}
|
|
246
|
+
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-950 rounded-lg text-left">
|
|
247
|
+
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-3">
|
|
248
|
+
🎯 What is TaskMaster?
|
|
249
|
+
</h4>
|
|
250
|
+
<div className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
|
251
|
+
<p>• <strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks</p>
|
|
252
|
+
<p>• <strong>PRD Templates:</strong> Generate tasks from Product Requirements Documents</p>
|
|
253
|
+
<p>• <strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
|
|
254
|
+
<p>• <strong>Progress Visualization:</strong> Kanban boards and detailed task analytics</p>
|
|
255
|
+
<p>• <strong>CLI Integration:</strong> Use taskmaster commands for advanced workflows</p>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<button
|
|
260
|
+
onClick={() => {
|
|
261
|
+
setIsTaskMasterComplete(false); // Reset completion state
|
|
262
|
+
setShowCLI(true);
|
|
263
|
+
}}
|
|
264
|
+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors flex items-center gap-2 mx-auto"
|
|
265
|
+
>
|
|
266
|
+
<Terminal className="w-4 h-4" />
|
|
267
|
+
Initialize TaskMaster AI
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
) : (
|
|
271
|
+
// TaskMaster configured but no tasks - show Getting Started guide
|
|
272
|
+
<div className="max-w-4xl mx-auto">
|
|
273
|
+
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/50 dark:to-indigo-950/50 rounded-xl border border-blue-200 dark:border-blue-800 p-6 mb-6">
|
|
274
|
+
<div className="flex items-center gap-3 mb-4">
|
|
275
|
+
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
|
276
|
+
<FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
277
|
+
</div>
|
|
278
|
+
<div>
|
|
279
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Getting Started with TaskMaster</h2>
|
|
280
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">TaskMaster is initialized! Here's what to do next:</p>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="space-y-4 text-left">
|
|
285
|
+
<div className="grid gap-3">
|
|
286
|
+
{/* Step 1 */}
|
|
287
|
+
<div className="flex gap-3 p-3 bg-white dark:bg-gray-800/50 rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
288
|
+
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white text-xs font-semibold rounded-full flex items-center justify-center">1</div>
|
|
289
|
+
<div>
|
|
290
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Create a Product Requirements Document (PRD)</h4>
|
|
291
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">Discuss your project idea and create a PRD that describes what you want to build.</p>
|
|
292
|
+
<button
|
|
293
|
+
onClick={() => {
|
|
294
|
+
onShowPRDEditor?.();
|
|
295
|
+
}}
|
|
296
|
+
className="inline-flex items-center gap-1 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-2 py-1 rounded hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-colors"
|
|
297
|
+
>
|
|
298
|
+
<FileText className="w-3 h-3" />
|
|
299
|
+
Add PRD
|
|
300
|
+
</button>
|
|
301
|
+
|
|
302
|
+
{/* Show existing PRDs if any */}
|
|
303
|
+
{existingPRDs.length > 0 && (
|
|
304
|
+
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
|
305
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Existing PRDs:</p>
|
|
306
|
+
<div className="flex flex-wrap gap-2">
|
|
307
|
+
{existingPRDs.map((prd) => (
|
|
308
|
+
<button
|
|
309
|
+
key={prd.name}
|
|
310
|
+
onClick={async () => {
|
|
311
|
+
try {
|
|
312
|
+
// Load the PRD content from the API
|
|
313
|
+
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}/${encodeURIComponent(prd.name)}`);
|
|
314
|
+
if (response.ok) {
|
|
315
|
+
const prdData = await response.json();
|
|
316
|
+
onShowPRDEditor?.({
|
|
317
|
+
name: prd.name,
|
|
318
|
+
content: prdData.content,
|
|
319
|
+
isExisting: true
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
console.error('Failed to load PRD:', response.statusText);
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('Error loading PRD:', error);
|
|
326
|
+
}
|
|
327
|
+
}}
|
|
328
|
+
className="inline-flex items-center gap-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-2 py-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
329
|
+
>
|
|
330
|
+
<FileText className="w-3 h-3" />
|
|
331
|
+
{prd.name}
|
|
332
|
+
</button>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Step 2 */}
|
|
341
|
+
<div className="flex gap-3 p-3 bg-white dark:bg-gray-800/50 rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
342
|
+
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white text-xs font-semibold rounded-full flex items-center justify-center">2</div>
|
|
343
|
+
<div>
|
|
344
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Generate Tasks from PRD</h4>
|
|
345
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Once you have a PRD, ask your AI assistant to parse it and TaskMaster will automatically break it down into manageable tasks with implementation details.</p>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
{/* Step 3 */}
|
|
350
|
+
<div className="flex gap-3 p-3 bg-white dark:bg-gray-800/50 rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
351
|
+
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white text-xs font-semibold rounded-full flex items-center justify-center">3</div>
|
|
352
|
+
<div>
|
|
353
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Analyze & Expand Tasks</h4>
|
|
354
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Ask your AI assistant to analyze task complexity and expand them into detailed subtasks for easier implementation.</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Step 4 */}
|
|
359
|
+
<div className="flex gap-3 p-3 bg-white dark:bg-gray-800/50 rounded-lg border border-blue-100 dark:border-blue-800/50">
|
|
360
|
+
<div className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white text-xs font-semibold rounded-full flex items-center justify-center">4</div>
|
|
361
|
+
<div>
|
|
362
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-1">Start Building</h4>
|
|
363
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Ask your AI assistant to begin working on tasks, update their status, and add new tasks as your project evolves.</p>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div className="flex gap-3 pt-4 border-t border-blue-200 dark:border-blue-700">
|
|
369
|
+
<button
|
|
370
|
+
onClick={(e) => {
|
|
371
|
+
e.preventDefault();
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
onShowPRDEditor?.();
|
|
374
|
+
}}
|
|
375
|
+
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition-colors cursor-pointer"
|
|
376
|
+
style={{ zIndex: 10 }}
|
|
377
|
+
>
|
|
378
|
+
<FileText className="w-4 h-4" />
|
|
379
|
+
Add PRD
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<div className="text-center">
|
|
386
|
+
<div className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
|
387
|
+
💡 <strong>Tip:</strong> Start with a PRD to get the most out of TaskMaster's AI-powered task generation
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{/* TaskMaster CLI Setup Modal */}
|
|
394
|
+
{showCLI && (
|
|
395
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-16 bg-black/50 backdrop-blur-sm">
|
|
396
|
+
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-4xl h-[600px] flex flex-col">
|
|
397
|
+
{/* Modal Header */}
|
|
398
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
|
399
|
+
<div className="flex items-center gap-3">
|
|
400
|
+
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
|
401
|
+
<Terminal className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
402
|
+
</div>
|
|
403
|
+
<div>
|
|
404
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">TaskMaster Setup</h2>
|
|
405
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">Interactive CLI for {currentProject?.displayName}</p>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
<button
|
|
409
|
+
onClick={() => {
|
|
410
|
+
setShowCLI(false);
|
|
411
|
+
// Refresh project data after closing CLI to detect TaskMaster initialization
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
refreshProjects();
|
|
414
|
+
// Also refresh the current project's TaskMaster status
|
|
415
|
+
if (currentProject) {
|
|
416
|
+
setCurrentProject(currentProject);
|
|
417
|
+
}
|
|
418
|
+
}, 1000);
|
|
419
|
+
}}
|
|
420
|
+
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
421
|
+
>
|
|
422
|
+
<Plus className="w-5 h-5 rotate-45" />
|
|
423
|
+
</button>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
{/* Terminal Container */}
|
|
427
|
+
<div className="flex-1 p-4">
|
|
428
|
+
<div
|
|
429
|
+
className="h-full bg-black rounded-lg overflow-hidden"
|
|
430
|
+
onClick={(e) => {
|
|
431
|
+
// Focus the terminal when clicked
|
|
432
|
+
const terminalElement = e.currentTarget.querySelector('.xterm-screen');
|
|
433
|
+
if (terminalElement) {
|
|
434
|
+
terminalElement.focus();
|
|
435
|
+
}
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
<Shell
|
|
439
|
+
selectedProject={currentProject}
|
|
440
|
+
selectedSession={null}
|
|
441
|
+
isActive={true}
|
|
442
|
+
initialCommand="npx task-master init"
|
|
443
|
+
isPlainShell={true}
|
|
444
|
+
onProcessComplete={(exitCode) => {
|
|
445
|
+
setIsTaskMasterComplete(true);
|
|
446
|
+
if (exitCode === 0) {
|
|
447
|
+
// Auto-refresh after successful completion
|
|
448
|
+
setTimeout(() => {
|
|
449
|
+
refreshProjects();
|
|
450
|
+
if (currentProject) {
|
|
451
|
+
setCurrentProject(currentProject);
|
|
452
|
+
}
|
|
453
|
+
}, 1000);
|
|
454
|
+
}
|
|
455
|
+
}}
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{/* Modal Footer */}
|
|
461
|
+
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
|
|
462
|
+
<div className="flex items-center justify-between">
|
|
463
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
464
|
+
{isTaskMasterComplete ? (
|
|
465
|
+
<span className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
|
466
|
+
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
467
|
+
TaskMaster setup completed! You can now close this window.
|
|
468
|
+
</span>
|
|
469
|
+
) : (
|
|
470
|
+
"TaskMaster initialization will start automatically"
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
<button
|
|
474
|
+
onClick={() => {
|
|
475
|
+
setShowCLI(false);
|
|
476
|
+
setIsTaskMasterComplete(false); // Reset state
|
|
477
|
+
// Refresh project data after closing CLI to detect TaskMaster initialization
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
refreshProjects();
|
|
480
|
+
// Also refresh the current project's TaskMaster status
|
|
481
|
+
if (currentProject) {
|
|
482
|
+
setCurrentProject(currentProject);
|
|
483
|
+
}
|
|
484
|
+
}, 1000);
|
|
485
|
+
}}
|
|
486
|
+
className={cn(
|
|
487
|
+
"px-4 py-2 text-sm font-medium rounded-md transition-colors",
|
|
488
|
+
isTaskMasterComplete
|
|
489
|
+
? "bg-green-600 hover:bg-green-700 text-white"
|
|
490
|
+
: "text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600"
|
|
491
|
+
)}
|
|
492
|
+
>
|
|
493
|
+
{isTaskMasterComplete ? "Close & Continue" : "Close"}
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<div className={cn('space-y-4', className)}>
|
|
506
|
+
{/* Header Controls */}
|
|
507
|
+
<div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
|
|
508
|
+
{/* Search Bar */}
|
|
509
|
+
<div className="relative flex-1 max-w-md">
|
|
510
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
511
|
+
<input
|
|
512
|
+
type="text"
|
|
513
|
+
placeholder="Search tasks..."
|
|
514
|
+
value={searchTerm}
|
|
515
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
516
|
+
className="pl-10 pr-4 py-2 w-full border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
{/* Controls */}
|
|
521
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
522
|
+
{/* View Toggle */}
|
|
523
|
+
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
|
524
|
+
<button
|
|
525
|
+
onClick={() => setViewMode('kanban')}
|
|
526
|
+
className={cn(
|
|
527
|
+
'p-2 rounded-md transition-colors',
|
|
528
|
+
viewMode === 'kanban'
|
|
529
|
+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
530
|
+
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
531
|
+
)}
|
|
532
|
+
title="Kanban view"
|
|
533
|
+
>
|
|
534
|
+
<Columns className="w-4 h-4" />
|
|
535
|
+
</button>
|
|
536
|
+
<button
|
|
537
|
+
onClick={() => setViewMode('list')}
|
|
538
|
+
className={cn(
|
|
539
|
+
'p-2 rounded-md transition-colors',
|
|
540
|
+
viewMode === 'list'
|
|
541
|
+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
542
|
+
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
543
|
+
)}
|
|
544
|
+
title="List view"
|
|
545
|
+
>
|
|
546
|
+
<List className="w-4 h-4" />
|
|
547
|
+
</button>
|
|
548
|
+
<button
|
|
549
|
+
onClick={() => setViewMode('grid')}
|
|
550
|
+
className={cn(
|
|
551
|
+
'p-2 rounded-md transition-colors',
|
|
552
|
+
viewMode === 'grid'
|
|
553
|
+
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
|
554
|
+
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
555
|
+
)}
|
|
556
|
+
title="Grid view"
|
|
557
|
+
>
|
|
558
|
+
<Grid className="w-4 h-4" />
|
|
559
|
+
</button>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{/* Filters Toggle */}
|
|
563
|
+
<button
|
|
564
|
+
onClick={() => setShowFilters(!showFilters)}
|
|
565
|
+
className={cn(
|
|
566
|
+
'flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors',
|
|
567
|
+
showFilters
|
|
568
|
+
? 'bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700 text-blue-700 dark:text-blue-300'
|
|
569
|
+
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
570
|
+
)}
|
|
571
|
+
>
|
|
572
|
+
<Filter className="w-4 h-4" />
|
|
573
|
+
<span className="hidden sm:inline">Filters</span>
|
|
574
|
+
<ChevronDown className={cn('w-4 h-4 transition-transform', showFilters && 'rotate-180')} />
|
|
575
|
+
</button>
|
|
576
|
+
|
|
577
|
+
{/* Action Buttons */}
|
|
578
|
+
{currentProject && (
|
|
579
|
+
<>
|
|
580
|
+
{/* Help Button */}
|
|
581
|
+
<button
|
|
582
|
+
onClick={() => setShowHelpGuide(true)}
|
|
583
|
+
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors border border-gray-300 dark:border-gray-600"
|
|
584
|
+
title="TaskMaster Getting Started Guide"
|
|
585
|
+
>
|
|
586
|
+
<HelpCircle className="w-4 h-4" />
|
|
587
|
+
</button>
|
|
588
|
+
|
|
589
|
+
{/* PRD Management */}
|
|
590
|
+
<div className="relative">
|
|
591
|
+
{existingPRDs.length > 0 ? (
|
|
592
|
+
// Dropdown when PRDs exist
|
|
593
|
+
<div className="relative">
|
|
594
|
+
<button
|
|
595
|
+
onClick={() => setShowPRDDropdown(!showPRDDropdown)}
|
|
596
|
+
className="flex items-center gap-2 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
|
|
597
|
+
title={`${existingPRDs.length} PRD${existingPRDs.length > 1 ? 's' : ''} available`}
|
|
598
|
+
>
|
|
599
|
+
<FileText className="w-4 h-4" />
|
|
600
|
+
<span className="hidden sm:inline">PRDs</span>
|
|
601
|
+
<span className="px-1.5 py-0.5 text-xs bg-purple-500 rounded-full min-w-[1.25rem] text-center">
|
|
602
|
+
{existingPRDs.length}
|
|
603
|
+
</span>
|
|
604
|
+
<ChevronDown className={cn('w-3 h-3 transition-transform hidden sm:block', showPRDDropdown && 'rotate-180')} />
|
|
605
|
+
</button>
|
|
606
|
+
|
|
607
|
+
{showPRDDropdown && (
|
|
608
|
+
<div className="absolute right-0 top-full mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl z-30">
|
|
609
|
+
<div className="p-2">
|
|
610
|
+
<button
|
|
611
|
+
onClick={() => {
|
|
612
|
+
onShowPRDEditor?.();
|
|
613
|
+
setShowPRDDropdown(false);
|
|
614
|
+
}}
|
|
615
|
+
className="w-full text-left px-3 py-2 text-sm font-medium text-purple-700 dark:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/30 rounded flex items-center gap-2"
|
|
616
|
+
>
|
|
617
|
+
<Plus className="w-4 h-4" />
|
|
618
|
+
Create New PRD
|
|
619
|
+
</button>
|
|
620
|
+
<div className="border-t border-gray-200 dark:border-gray-700 my-1"></div>
|
|
621
|
+
<div className="text-xs text-gray-500 dark:text-gray-400 px-3 py-1 font-medium">Existing PRDs:</div>
|
|
622
|
+
{existingPRDs.map((prd) => (
|
|
623
|
+
<button
|
|
624
|
+
key={prd.name}
|
|
625
|
+
onClick={async () => {
|
|
626
|
+
try {
|
|
627
|
+
const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}/${encodeURIComponent(prd.name)}`);
|
|
628
|
+
if (response.ok) {
|
|
629
|
+
const prdData = await response.json();
|
|
630
|
+
onShowPRDEditor?.({
|
|
631
|
+
name: prd.name,
|
|
632
|
+
content: prdData.content,
|
|
633
|
+
isExisting: true
|
|
634
|
+
});
|
|
635
|
+
setShowPRDDropdown(false);
|
|
636
|
+
}
|
|
637
|
+
} catch (error) {
|
|
638
|
+
console.error('Error loading PRD:', error);
|
|
639
|
+
}
|
|
640
|
+
}}
|
|
641
|
+
className="w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded flex items-center gap-2"
|
|
642
|
+
title={`Modified: ${new Date(prd.modified).toLocaleDateString()}`}
|
|
643
|
+
>
|
|
644
|
+
<FileText className="w-4 h-4" />
|
|
645
|
+
<span className="truncate">{prd.name}</span>
|
|
646
|
+
</button>
|
|
647
|
+
))}
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
) : (
|
|
653
|
+
// Simple button when no PRDs exist
|
|
654
|
+
<button
|
|
655
|
+
onClick={() => {
|
|
656
|
+
onShowPRDEditor?.();
|
|
657
|
+
}}
|
|
658
|
+
className="flex items-center gap-2 px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
|
|
659
|
+
title="Create Product Requirements Document"
|
|
660
|
+
>
|
|
661
|
+
<FileText className="w-4 h-4" />
|
|
662
|
+
<span className="hidden sm:inline">Add PRD</span>
|
|
663
|
+
</button>
|
|
664
|
+
)}
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
{/* Add Task Button */}
|
|
668
|
+
{((currentProject?.taskMasterConfigured || currentProject?.taskmaster?.hasTaskmaster || projectTaskMaster?.hasTaskmaster) || tasks.length > 0) && (
|
|
669
|
+
<button
|
|
670
|
+
onClick={() => setShowCreateModal(true)}
|
|
671
|
+
className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
|
672
|
+
title="Add a new task"
|
|
673
|
+
>
|
|
674
|
+
<Plus className="w-4 h-4" />
|
|
675
|
+
<span className="hidden sm:inline">Add Task</span>
|
|
676
|
+
</button>
|
|
677
|
+
)}
|
|
678
|
+
</>
|
|
679
|
+
)}
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
{/* Expanded Filters */}
|
|
684
|
+
{showFilters && (
|
|
685
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 space-y-4">
|
|
686
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
687
|
+
{/* Status Filter */}
|
|
688
|
+
<div>
|
|
689
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
690
|
+
Status
|
|
691
|
+
</label>
|
|
692
|
+
<select
|
|
693
|
+
value={statusFilter}
|
|
694
|
+
onChange={(e) => setStatusFilter(e.target.value)}
|
|
695
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
696
|
+
>
|
|
697
|
+
<option value="all">All Statuses</option>
|
|
698
|
+
{statuses.map(status => (
|
|
699
|
+
<option key={status} value={status}>
|
|
700
|
+
{status.charAt(0).toUpperCase() + status.slice(1).replace('-', ' ')}
|
|
701
|
+
</option>
|
|
702
|
+
))}
|
|
703
|
+
</select>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
{/* Priority Filter */}
|
|
707
|
+
<div>
|
|
708
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
709
|
+
Priority
|
|
710
|
+
</label>
|
|
711
|
+
<select
|
|
712
|
+
value={priorityFilter}
|
|
713
|
+
onChange={(e) => setPriorityFilter(e.target.value)}
|
|
714
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
715
|
+
>
|
|
716
|
+
<option value="all">All Priorities</option>
|
|
717
|
+
{priorities.map(priority => (
|
|
718
|
+
<option key={priority} value={priority}>
|
|
719
|
+
{priority.charAt(0).toUpperCase() + priority.slice(1)}
|
|
720
|
+
</option>
|
|
721
|
+
))}
|
|
722
|
+
</select>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
{/* Sort By */}
|
|
726
|
+
<div>
|
|
727
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
728
|
+
Sort By
|
|
729
|
+
</label>
|
|
730
|
+
<select
|
|
731
|
+
value={`${sortBy}-${sortOrder}`}
|
|
732
|
+
onChange={(e) => {
|
|
733
|
+
const [field, order] = e.target.value.split('-');
|
|
734
|
+
setSortBy(field);
|
|
735
|
+
setSortOrder(order);
|
|
736
|
+
}}
|
|
737
|
+
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
738
|
+
>
|
|
739
|
+
<option value="id-asc">ID (Ascending)</option>
|
|
740
|
+
<option value="id-desc">ID (Descending)</option>
|
|
741
|
+
<option value="title-asc">Title (A-Z)</option>
|
|
742
|
+
<option value="title-desc">Title (Z-A)</option>
|
|
743
|
+
<option value="status-asc">Status (Pending First)</option>
|
|
744
|
+
<option value="status-desc">Status (Done First)</option>
|
|
745
|
+
<option value="priority-asc">Priority (High First)</option>
|
|
746
|
+
<option value="priority-desc">Priority (Low First)</option>
|
|
747
|
+
</select>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
{/* Filter Actions */}
|
|
752
|
+
<div className="flex items-center justify-between">
|
|
753
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
754
|
+
Showing {filteredAndSortedTasks.length} of {tasks.length} tasks
|
|
755
|
+
</div>
|
|
756
|
+
<button
|
|
757
|
+
onClick={clearFilters}
|
|
758
|
+
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
|
759
|
+
>
|
|
760
|
+
Clear Filters
|
|
761
|
+
</button>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
)}
|
|
765
|
+
|
|
766
|
+
{/* Quick Sort Buttons */}
|
|
767
|
+
<div className="flex flex-wrap gap-2">
|
|
768
|
+
<button
|
|
769
|
+
onClick={() => handleSortChange('id')}
|
|
770
|
+
className={cn(
|
|
771
|
+
'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
|
|
772
|
+
sortBy === 'id'
|
|
773
|
+
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
|
774
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
775
|
+
)}
|
|
776
|
+
>
|
|
777
|
+
ID {getSortIcon('id')}
|
|
778
|
+
</button>
|
|
779
|
+
<button
|
|
780
|
+
onClick={() => handleSortChange('status')}
|
|
781
|
+
className={cn(
|
|
782
|
+
'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
|
|
783
|
+
sortBy === 'status'
|
|
784
|
+
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
|
785
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
786
|
+
)}
|
|
787
|
+
>
|
|
788
|
+
Status {getSortIcon('status')}
|
|
789
|
+
</button>
|
|
790
|
+
<button
|
|
791
|
+
onClick={() => handleSortChange('priority')}
|
|
792
|
+
className={cn(
|
|
793
|
+
'flex items-center gap-1 px-3 py-1.5 rounded-md text-sm transition-colors',
|
|
794
|
+
sortBy === 'priority'
|
|
795
|
+
? 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
|
796
|
+
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
797
|
+
)}
|
|
798
|
+
>
|
|
799
|
+
Priority {getSortIcon('priority')}
|
|
800
|
+
</button>
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
{/* Task Cards */}
|
|
804
|
+
{filteredAndSortedTasks.length === 0 ? (
|
|
805
|
+
<div className="text-center py-12">
|
|
806
|
+
<div className="text-gray-500 dark:text-gray-400">
|
|
807
|
+
<Search className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
808
|
+
<h3 className="text-lg font-medium mb-2">No tasks match your filters</h3>
|
|
809
|
+
<p className="text-sm">Try adjusting your search or filter criteria.</p>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
) : viewMode === 'kanban' ? (
|
|
813
|
+
/* Kanban Board Layout - Dynamic grid based on column count */
|
|
814
|
+
<div className={cn(
|
|
815
|
+
"grid gap-6",
|
|
816
|
+
kanbanColumns.length === 1 && "grid-cols-1 max-w-md mx-auto",
|
|
817
|
+
kanbanColumns.length === 2 && "grid-cols-1 md:grid-cols-2",
|
|
818
|
+
kanbanColumns.length === 3 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
|
819
|
+
kanbanColumns.length === 4 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-4",
|
|
820
|
+
kanbanColumns.length === 5 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5",
|
|
821
|
+
kanbanColumns.length >= 6 && "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6"
|
|
822
|
+
)}>
|
|
823
|
+
{kanbanColumns.map((column) => (
|
|
824
|
+
<div key={column.id} className={cn('rounded-xl border shadow-sm transition-shadow hover:shadow-md', column.color)}>
|
|
825
|
+
{/* Column Header */}
|
|
826
|
+
<div className={cn('px-4 py-3 rounded-t-xl border-b', column.headerColor)}>
|
|
827
|
+
<div className="flex items-center justify-between">
|
|
828
|
+
<h3 className="font-semibold text-sm">
|
|
829
|
+
{column.title}
|
|
830
|
+
</h3>
|
|
831
|
+
<div className="flex items-center gap-2">
|
|
832
|
+
<span className="text-xs font-medium px-2 py-1 bg-white/60 dark:bg-black/20 rounded-full">
|
|
833
|
+
{column.tasks.length}
|
|
834
|
+
</span>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
{/* Column Tasks */}
|
|
840
|
+
<div className="p-3 space-y-3 min-h-[200px] max-h-[calc(100vh-300px)] overflow-y-auto">
|
|
841
|
+
{column.tasks.length === 0 ? (
|
|
842
|
+
<div className="text-center py-8 text-gray-400 dark:text-gray-500">
|
|
843
|
+
<div className="w-8 h-8 mx-auto mb-2 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
|
844
|
+
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600"></div>
|
|
845
|
+
</div>
|
|
846
|
+
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
|
847
|
+
No tasks yet
|
|
848
|
+
</div>
|
|
849
|
+
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
|
850
|
+
{column.status === 'pending' ? 'Tasks will appear here' :
|
|
851
|
+
column.status === 'in-progress' ? 'Move tasks here when started' :
|
|
852
|
+
column.status === 'done' ? 'Completed tasks appear here' :
|
|
853
|
+
'Tasks with this status will appear here'}
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
) : (
|
|
857
|
+
column.tasks.map((task) => (
|
|
858
|
+
<div key={task.id} className="transform transition-transform hover:scale-[1.02]">
|
|
859
|
+
<TaskCard
|
|
860
|
+
task={task}
|
|
861
|
+
onClick={() => onTaskClick?.(task)}
|
|
862
|
+
showParent={showParentTasks}
|
|
863
|
+
className="w-full shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
|
864
|
+
/>
|
|
865
|
+
</div>
|
|
866
|
+
))
|
|
867
|
+
)}
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
))}
|
|
871
|
+
</div>
|
|
872
|
+
) : (
|
|
873
|
+
<div className={cn(
|
|
874
|
+
'gap-4',
|
|
875
|
+
viewMode === 'grid'
|
|
876
|
+
? 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3'
|
|
877
|
+
: 'space-y-4'
|
|
878
|
+
)}>
|
|
879
|
+
{filteredAndSortedTasks.map((task) => (
|
|
880
|
+
<TaskCard
|
|
881
|
+
key={task.id}
|
|
882
|
+
task={task}
|
|
883
|
+
onClick={() => onTaskClick?.(task)}
|
|
884
|
+
showParent={showParentTasks}
|
|
885
|
+
className={viewMode === 'grid' ? 'h-full' : ''}
|
|
886
|
+
/>
|
|
887
|
+
))}
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
|
|
891
|
+
{/* Create Task Modal */}
|
|
892
|
+
{showCreateModal && (
|
|
893
|
+
<CreateTaskModal
|
|
894
|
+
currentProject={currentProject}
|
|
895
|
+
onClose={() => setShowCreateModal(false)}
|
|
896
|
+
onTaskCreated={() => {
|
|
897
|
+
setShowCreateModal(false);
|
|
898
|
+
if (onTaskCreated) onTaskCreated();
|
|
899
|
+
}}
|
|
900
|
+
/>
|
|
901
|
+
)}
|
|
902
|
+
|
|
903
|
+
{/* Help Guide Modal */}
|
|
904
|
+
{showHelpGuide && (
|
|
905
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
906
|
+
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-4xl max-h-[90vh] overflow-hidden">
|
|
907
|
+
{/* Modal Header */}
|
|
908
|
+
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
|
909
|
+
<div className="flex items-center gap-3">
|
|
910
|
+
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center">
|
|
911
|
+
<FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
912
|
+
</div>
|
|
913
|
+
<div>
|
|
914
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Getting Started with TaskMaster</h2>
|
|
915
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Your guide to productive task management</p>
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
<button
|
|
919
|
+
onClick={() => setShowHelpGuide(false)}
|
|
920
|
+
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
921
|
+
>
|
|
922
|
+
<X className="w-5 h-5" />
|
|
923
|
+
</button>
|
|
924
|
+
</div>
|
|
925
|
+
|
|
926
|
+
{/* Modal Content */}
|
|
927
|
+
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
|
|
928
|
+
<div className="space-y-4">
|
|
929
|
+
{/* Step 1 */}
|
|
930
|
+
<div className="flex gap-4 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/50 dark:to-indigo-950/50 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
931
|
+
<div className="flex-shrink-0 w-8 h-8 bg-blue-600 text-white text-sm font-semibold rounded-full flex items-center justify-center">1</div>
|
|
932
|
+
<div>
|
|
933
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Create a Product Requirements Document (PRD)</h4>
|
|
934
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">Discuss your project idea and create a PRD that describes what you want to build.</p>
|
|
935
|
+
<button
|
|
936
|
+
onClick={() => {
|
|
937
|
+
onShowPRDEditor?.();
|
|
938
|
+
setShowHelpGuide(false);
|
|
939
|
+
}}
|
|
940
|
+
className="inline-flex items-center gap-2 text-sm bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-3 py-1.5 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-colors"
|
|
941
|
+
>
|
|
942
|
+
<FileText className="w-4 h-4" />
|
|
943
|
+
Add PRD
|
|
944
|
+
</button>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
|
|
948
|
+
{/* Step 2 */}
|
|
949
|
+
<div className="flex gap-4 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/50 dark:to-emerald-950/50 rounded-lg border border-green-200 dark:border-green-800">
|
|
950
|
+
<div className="flex-shrink-0 w-8 h-8 bg-green-600 text-white text-sm font-semibold rounded-full flex items-center justify-center">2</div>
|
|
951
|
+
<div>
|
|
952
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Generate Tasks from PRD</h4>
|
|
953
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">Once you have a PRD, ask your AI assistant to parse it and TaskMaster will automatically break it down into manageable tasks with implementation details.</p>
|
|
954
|
+
<div className="bg-white dark:bg-gray-800/50 rounded border border-green-200 dark:border-green-700/50 p-3 mb-2">
|
|
955
|
+
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">💬 Example:</p>
|
|
956
|
+
<p className="text-xs text-gray-900 dark:text-white font-mono">
|
|
957
|
+
"I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/prd.txt. Can you help me parse it and set up the initial tasks?"
|
|
958
|
+
</p>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
{/* Step 3 */}
|
|
964
|
+
<div className="flex gap-4 p-4 bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-950/50 dark:to-orange-950/50 rounded-lg border border-amber-200 dark:border-amber-800">
|
|
965
|
+
<div className="flex-shrink-0 w-8 h-8 bg-amber-600 text-white text-sm font-semibold rounded-full flex items-center justify-center">3</div>
|
|
966
|
+
<div>
|
|
967
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Analyze & Expand Tasks</h4>
|
|
968
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">Ask your AI assistant to analyze task complexity and expand them into detailed subtasks for easier implementation.</p>
|
|
969
|
+
<div className="bg-white dark:bg-gray-800/50 rounded border border-amber-200 dark:border-amber-700/50 p-3 mb-2">
|
|
970
|
+
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">💬 Example:</p>
|
|
971
|
+
<p className="text-xs text-gray-900 dark:text-white font-mono">
|
|
972
|
+
"Task 5 seems complex. Can you break it down into subtasks?"
|
|
973
|
+
</p>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
|
|
978
|
+
{/* Step 4 */}
|
|
979
|
+
<div className="flex gap-4 p-4 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/50 dark:to-pink-950/50 rounded-lg border border-purple-200 dark:border-purple-800">
|
|
980
|
+
<div className="flex-shrink-0 w-8 h-8 bg-purple-600 text-white text-sm font-semibold rounded-full flex items-center justify-center">4</div>
|
|
981
|
+
<div>
|
|
982
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Start Building</h4>
|
|
983
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">Ask your AI assistant to begin working on tasks, update their status, and add new tasks as your project evolves.</p>
|
|
984
|
+
<div className="bg-white dark:bg-gray-800/50 rounded border border-purple-200 dark:border-purple-700/50 p-3 mb-3">
|
|
985
|
+
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">💬 Example:</p>
|
|
986
|
+
<p className="text-xs text-gray-900 dark:text-white font-mono">
|
|
987
|
+
"Please add a new task to implement user profile image uploads using Cloudinary, research the best approach."
|
|
988
|
+
</p>
|
|
989
|
+
</div>
|
|
990
|
+
<a
|
|
991
|
+
href="https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md"
|
|
992
|
+
target="_blank"
|
|
993
|
+
rel="noopener noreferrer"
|
|
994
|
+
className="inline-block text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline"
|
|
995
|
+
>
|
|
996
|
+
View more examples and usage patterns →
|
|
997
|
+
</a>
|
|
998
|
+
</div>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
{/* Pro Tips */}
|
|
1002
|
+
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
1003
|
+
<h4 className="font-medium text-gray-900 dark:text-white mb-3">💡 Pro Tips</h4>
|
|
1004
|
+
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
|
1005
|
+
<li className="flex items-start gap-2">
|
|
1006
|
+
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full mt-2 flex-shrink-0"></span>
|
|
1007
|
+
Use the search bar to quickly find specific tasks
|
|
1008
|
+
</li>
|
|
1009
|
+
<li className="flex items-start gap-2">
|
|
1010
|
+
<span className="w-1.5 h-1.5 bg-green-500 rounded-full mt-2 flex-shrink-0"></span>
|
|
1011
|
+
Switch between Kanban, List, and Grid views using the view toggles
|
|
1012
|
+
</li>
|
|
1013
|
+
<li className="flex items-start gap-2">
|
|
1014
|
+
<span className="w-1.5 h-1.5 bg-purple-500 rounded-full mt-2 flex-shrink-0"></span>
|
|
1015
|
+
Use filters to focus on specific task statuses or priorities
|
|
1016
|
+
</li>
|
|
1017
|
+
<li className="flex items-start gap-2">
|
|
1018
|
+
<span className="w-1.5 h-1.5 bg-orange-500 rounded-full mt-2 flex-shrink-0"></span>
|
|
1019
|
+
Click on any task to view detailed information and manage subtasks
|
|
1020
|
+
</li>
|
|
1021
|
+
</ul>
|
|
1022
|
+
</div>
|
|
1023
|
+
|
|
1024
|
+
{/* Learn More Section */}
|
|
1025
|
+
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950/50 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
1026
|
+
<h4 className="font-medium text-blue-900 dark:text-blue-100 mb-3">📚 Learn More</h4>
|
|
1027
|
+
<p className="text-sm text-blue-800 dark:text-blue-200 mb-3">
|
|
1028
|
+
TaskMaster AI is an advanced task management system built for developers. Get documentation, examples, and contribute to the project.
|
|
1029
|
+
</p>
|
|
1030
|
+
<a
|
|
1031
|
+
href="https://github.com/eyaltoledano/claude-task-master"
|
|
1032
|
+
target="_blank"
|
|
1033
|
+
rel="noopener noreferrer"
|
|
1034
|
+
className="inline-flex items-center gap-2 text-sm bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg font-medium transition-colors"
|
|
1035
|
+
>
|
|
1036
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
1037
|
+
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
|
|
1038
|
+
</svg>
|
|
1039
|
+
View on GitHub
|
|
1040
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1041
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
1042
|
+
</svg>
|
|
1043
|
+
</a>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
)}
|
|
1050
|
+
</div>
|
|
1051
|
+
);
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
export default TaskList;
|