create-varity-app 2.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/create.js +141 -0
- package/dist/index.js +45 -0
- package/dist/utils.js +29 -0
- package/package.json +61 -0
- package/template/.env.example +17 -0
- package/template/KNOWN_ISSUES.md +69 -0
- package/template/LICENSE +21 -0
- package/template/README.md +241 -0
- package/template/gitignore +42 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.js +21 -0
- package/template/package.json +39 -0
- package/template/postcss.config.js +6 -0
- package/template/public/logo.svg +4 -0
- package/template/public/robots.txt +4 -0
- package/template/public/sitemap.xml +4 -0
- package/template/src/app/dashboard/layout.tsx +298 -0
- package/template/src/app/dashboard/page.tsx +209 -0
- package/template/src/app/dashboard/projects/page.tsx +638 -0
- package/template/src/app/dashboard/settings/page.tsx +749 -0
- package/template/src/app/dashboard/tasks/page.tsx +301 -0
- package/template/src/app/dashboard/team/page.tsx +295 -0
- package/template/src/app/globals.css +177 -0
- package/template/src/app/icon.svg +4 -0
- package/template/src/app/layout.tsx +33 -0
- package/template/src/app/login/page.tsx +98 -0
- package/template/src/app/not-found.tsx +20 -0
- package/template/src/app/page.tsx +23 -0
- package/template/src/components/dashboard/DashboardStats.tsx +137 -0
- package/template/src/components/dashboard/RecentActivity.tsx +63 -0
- package/template/src/components/landing/CTA.tsx +42 -0
- package/template/src/components/landing/Features.tsx +116 -0
- package/template/src/components/landing/Hero.tsx +146 -0
- package/template/src/components/landing/HowItWorks.tsx +80 -0
- package/template/src/components/landing/Pricing.tsx +124 -0
- package/template/src/components/landing/Testimonials.tsx +78 -0
- package/template/src/components/providers.tsx +11 -0
- package/template/src/components/shared/Footer.tsx +71 -0
- package/template/src/components/shared/Navbar.tsx +87 -0
- package/template/src/lib/constants.ts +35 -0
- package/template/src/lib/database.ts +7 -0
- package/template/src/lib/hooks.ts +331 -0
- package/template/src/lib/utils.ts +68 -0
- package/template/src/lib/varity.ts +1 -0
- package/template/src/services/dashboardService.ts +589 -0
- package/template/src/types/index.ts +52 -0
- package/template/tailwind.config.js +27 -0
- package/template/tsconfig.json +23 -0
- package/template/varity.config.json +14 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { KPICard, DataTable, EmptyState } from '@varity-labs/ui-kit';
|
|
5
|
+
import { useProjects, useTasks, useCurrentUser } from '@/lib/hooks';
|
|
6
|
+
import { Plus, FolderKanban, ArrowLeft, ListTodo, CheckCircle, Clock, Users, Pencil, Trash2, Download } from 'lucide-react';
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
Input,
|
|
10
|
+
Textarea,
|
|
11
|
+
Select,
|
|
12
|
+
Dialog,
|
|
13
|
+
ConfirmDialog,
|
|
14
|
+
useToast,
|
|
15
|
+
ProjectStatusBadge,
|
|
16
|
+
TaskStatusBadge,
|
|
17
|
+
PriorityBadge
|
|
18
|
+
} from '@varity-labs/ui-kit';
|
|
19
|
+
import { formatDate, downloadCSV } from '@/lib/utils';
|
|
20
|
+
import { PRIORITY_OPTIONS, PROJECT_STATUS_OPTIONS, TASK_STATUS_OPTIONS } from '@/lib/constants';
|
|
21
|
+
import type { Project, Task } from '@/types';
|
|
22
|
+
|
|
23
|
+
const EMPTY_PROJECT = { name: '', description: '', dueDate: '', status: 'active' as Project['status'] };
|
|
24
|
+
const EMPTY_TASK = { title: '', description: '', priority: 'medium' as Task['priority'], status: 'todo' as Task['status'] };
|
|
25
|
+
|
|
26
|
+
export default function ProjectsPage() {
|
|
27
|
+
const toast = useToast();
|
|
28
|
+
const { email } = useCurrentUser();
|
|
29
|
+
const { data: projects, loading, error, create, update, remove, refresh } = useProjects();
|
|
30
|
+
|
|
31
|
+
// Selected project for detail view
|
|
32
|
+
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
|
33
|
+
|
|
34
|
+
// Create project dialog
|
|
35
|
+
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
36
|
+
const [projectForm, setProjectForm] = useState(EMPTY_PROJECT);
|
|
37
|
+
const [nameError, setNameError] = useState('');
|
|
38
|
+
const [projectSubmitting, setProjectSubmitting] = useState(false);
|
|
39
|
+
|
|
40
|
+
// Edit project dialog
|
|
41
|
+
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
42
|
+
const [editForm, setEditForm] = useState(EMPTY_PROJECT);
|
|
43
|
+
const [editNameError, setEditNameError] = useState('');
|
|
44
|
+
const [editSubmitting, setEditSubmitting] = useState(false);
|
|
45
|
+
|
|
46
|
+
// Delete project confirmation
|
|
47
|
+
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(null);
|
|
48
|
+
const [deleteSubmitting, setDeleteSubmitting] = useState(false);
|
|
49
|
+
|
|
50
|
+
// Create task dialog (for detail view)
|
|
51
|
+
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
|
|
52
|
+
const [taskForm, setTaskForm] = useState(EMPTY_TASK);
|
|
53
|
+
const [titleError, setTitleError] = useState('');
|
|
54
|
+
const [taskSubmitting, setTaskSubmitting] = useState(false);
|
|
55
|
+
|
|
56
|
+
// Edit task dialog (for detail view)
|
|
57
|
+
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
|
58
|
+
const [editTaskForm, setEditTaskForm] = useState(EMPTY_TASK);
|
|
59
|
+
const [editTitleError, setEditTitleError] = useState('');
|
|
60
|
+
const [editTaskSubmitting, setEditTaskSubmitting] = useState(false);
|
|
61
|
+
|
|
62
|
+
// Delete task confirmation (for detail view)
|
|
63
|
+
const [deletingTaskId, setDeletingTaskId] = useState<string | null>(null);
|
|
64
|
+
const [deleteTaskSubmitting, setDeleteTaskSubmitting] = useState(false);
|
|
65
|
+
|
|
66
|
+
const selectedProject = selectedProjectId
|
|
67
|
+
? projects.find((p) => p.id === selectedProjectId)
|
|
68
|
+
: null;
|
|
69
|
+
|
|
70
|
+
// Tasks for the selected project
|
|
71
|
+
const {
|
|
72
|
+
data: tasks,
|
|
73
|
+
loading: tasksLoading,
|
|
74
|
+
create: createTask,
|
|
75
|
+
update: updateTask,
|
|
76
|
+
remove: removeTask,
|
|
77
|
+
} = useTasks(selectedProjectId || undefined);
|
|
78
|
+
|
|
79
|
+
const deletingProject = deletingProjectId
|
|
80
|
+
? projects.find((p) => p.id === deletingProjectId)
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
// --- Project CRUD handlers ---
|
|
84
|
+
|
|
85
|
+
function resetCreateDialog() {
|
|
86
|
+
setProjectForm(EMPTY_PROJECT);
|
|
87
|
+
setNameError('');
|
|
88
|
+
setCreateDialogOpen(false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function startEditProject(project: Project) {
|
|
92
|
+
setEditingProject(project);
|
|
93
|
+
setEditForm({
|
|
94
|
+
name: project.name,
|
|
95
|
+
description: project.description,
|
|
96
|
+
dueDate: project.dueDate ? project.dueDate.split('T')[0] : '',
|
|
97
|
+
status: project.status,
|
|
98
|
+
});
|
|
99
|
+
setEditNameError('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resetEditDialog() {
|
|
103
|
+
setEditingProject(null);
|
|
104
|
+
setEditForm(EMPTY_PROJECT);
|
|
105
|
+
setEditNameError('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleCreateProject() {
|
|
109
|
+
if (!projectForm.name.trim()) {
|
|
110
|
+
setNameError('Project name is required');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setProjectSubmitting(true);
|
|
115
|
+
try {
|
|
116
|
+
await create({
|
|
117
|
+
name: projectForm.name.trim(),
|
|
118
|
+
description: projectForm.description,
|
|
119
|
+
status: 'active',
|
|
120
|
+
owner: email,
|
|
121
|
+
members: [email],
|
|
122
|
+
dueDate: projectForm.dueDate || new Date(Date.now() + 30 * 86400000).toISOString(),
|
|
123
|
+
});
|
|
124
|
+
toast.success('Project created successfully');
|
|
125
|
+
resetCreateDialog();
|
|
126
|
+
} catch {
|
|
127
|
+
toast.error('Failed to create project. Please try again.');
|
|
128
|
+
} finally {
|
|
129
|
+
setProjectSubmitting(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function handleEditProject() {
|
|
134
|
+
if (!editingProject?.id) return;
|
|
135
|
+
if (!editForm.name.trim()) {
|
|
136
|
+
setEditNameError('Project name is required');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setEditSubmitting(true);
|
|
141
|
+
try {
|
|
142
|
+
await update(editingProject.id, {
|
|
143
|
+
name: editForm.name.trim(),
|
|
144
|
+
description: editForm.description,
|
|
145
|
+
status: editForm.status,
|
|
146
|
+
dueDate: editForm.dueDate || editingProject.dueDate,
|
|
147
|
+
});
|
|
148
|
+
toast.success('Project updated successfully');
|
|
149
|
+
resetEditDialog();
|
|
150
|
+
} catch {
|
|
151
|
+
toast.error('Failed to update project. Please try again.');
|
|
152
|
+
} finally {
|
|
153
|
+
setEditSubmitting(false);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleDeleteProject() {
|
|
158
|
+
if (!deletingProjectId) return;
|
|
159
|
+
|
|
160
|
+
setDeleteSubmitting(true);
|
|
161
|
+
try {
|
|
162
|
+
await remove(deletingProjectId);
|
|
163
|
+
toast.success('Project deleted');
|
|
164
|
+
setDeletingProjectId(null);
|
|
165
|
+
if (selectedProjectId === deletingProjectId) {
|
|
166
|
+
setSelectedProjectId(null);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
toast.error('Failed to delete project. Please try again.');
|
|
170
|
+
} finally {
|
|
171
|
+
setDeleteSubmitting(false);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Task CRUD handlers ---
|
|
176
|
+
|
|
177
|
+
function resetTaskDialog() {
|
|
178
|
+
setTaskForm(EMPTY_TASK);
|
|
179
|
+
setTitleError('');
|
|
180
|
+
setTaskDialogOpen(false);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function startEditTask(task: Task) {
|
|
184
|
+
setEditingTask(task);
|
|
185
|
+
setEditTaskForm({
|
|
186
|
+
title: task.title,
|
|
187
|
+
description: task.description || '',
|
|
188
|
+
priority: task.priority,
|
|
189
|
+
status: task.status,
|
|
190
|
+
});
|
|
191
|
+
setEditTitleError('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resetEditTaskDialog() {
|
|
195
|
+
setEditingTask(null);
|
|
196
|
+
setEditTaskForm(EMPTY_TASK);
|
|
197
|
+
setEditTitleError('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function handleCreateTask() {
|
|
201
|
+
if (!taskForm.title.trim() || !selectedProjectId) {
|
|
202
|
+
setTitleError('Task title is required');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setTaskSubmitting(true);
|
|
207
|
+
try {
|
|
208
|
+
await createTask({
|
|
209
|
+
projectId: selectedProjectId,
|
|
210
|
+
title: taskForm.title.trim(),
|
|
211
|
+
description: taskForm.description,
|
|
212
|
+
status: 'todo',
|
|
213
|
+
priority: taskForm.priority,
|
|
214
|
+
});
|
|
215
|
+
toast.success('Task added successfully');
|
|
216
|
+
resetTaskDialog();
|
|
217
|
+
} catch {
|
|
218
|
+
toast.error('Failed to add task. Please try again.');
|
|
219
|
+
} finally {
|
|
220
|
+
setTaskSubmitting(false);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function handleEditTask() {
|
|
225
|
+
if (!editingTask?.id) return;
|
|
226
|
+
if (!editTaskForm.title.trim()) {
|
|
227
|
+
setEditTitleError('Task title is required');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setEditTaskSubmitting(true);
|
|
232
|
+
try {
|
|
233
|
+
await updateTask(editingTask.id, {
|
|
234
|
+
title: editTaskForm.title.trim(),
|
|
235
|
+
description: editTaskForm.description,
|
|
236
|
+
priority: editTaskForm.priority,
|
|
237
|
+
status: editTaskForm.status,
|
|
238
|
+
});
|
|
239
|
+
toast.success('Task updated successfully');
|
|
240
|
+
resetEditTaskDialog();
|
|
241
|
+
} catch {
|
|
242
|
+
toast.error('Failed to update task. Please try again.');
|
|
243
|
+
} finally {
|
|
244
|
+
setEditTaskSubmitting(false);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handleDeleteTask() {
|
|
249
|
+
if (!deletingTaskId) return;
|
|
250
|
+
|
|
251
|
+
setDeleteTaskSubmitting(true);
|
|
252
|
+
try {
|
|
253
|
+
await removeTask(deletingTaskId);
|
|
254
|
+
toast.success('Task deleted');
|
|
255
|
+
setDeletingTaskId(null);
|
|
256
|
+
} catch {
|
|
257
|
+
toast.error('Failed to delete task. Please try again.');
|
|
258
|
+
} finally {
|
|
259
|
+
setDeleteTaskSubmitting(false);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Table columns ---
|
|
264
|
+
|
|
265
|
+
const projectColumns = [
|
|
266
|
+
{ key: 'name', header: 'Project Name', sortable: true },
|
|
267
|
+
{
|
|
268
|
+
key: 'status',
|
|
269
|
+
header: 'Status',
|
|
270
|
+
render: (value: string) => <ProjectStatusBadge status={value} />,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
key: 'members',
|
|
274
|
+
header: 'Members',
|
|
275
|
+
render: (value: string[]) => {
|
|
276
|
+
const count = value?.filter(Boolean).length || 0;
|
|
277
|
+
return <span>{count} {count === 1 ? 'member' : 'members'}</span>;
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: 'dueDate',
|
|
282
|
+
header: 'Due Date',
|
|
283
|
+
sortable: true,
|
|
284
|
+
render: (value: string) => formatDate(value),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
key: 'id',
|
|
288
|
+
header: '',
|
|
289
|
+
render: (_: string, row: Project) => (
|
|
290
|
+
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
|
|
291
|
+
<button
|
|
292
|
+
onClick={() => startEditProject(row)}
|
|
293
|
+
className="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 transition-colors"
|
|
294
|
+
title="Edit project"
|
|
295
|
+
>
|
|
296
|
+
<Pencil className="h-4 w-4" />
|
|
297
|
+
</button>
|
|
298
|
+
<button
|
|
299
|
+
onClick={() => setDeletingProjectId(row.id!)}
|
|
300
|
+
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-600 transition-colors"
|
|
301
|
+
title="Delete project"
|
|
302
|
+
>
|
|
303
|
+
<Trash2 className="h-4 w-4" />
|
|
304
|
+
</button>
|
|
305
|
+
</div>
|
|
306
|
+
),
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const taskColumns = [
|
|
311
|
+
{ key: 'title', header: 'Task', sortable: true },
|
|
312
|
+
{
|
|
313
|
+
key: 'status',
|
|
314
|
+
header: 'Status',
|
|
315
|
+
render: (value: string) => <TaskStatusBadge status={value} />,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
key: 'priority',
|
|
319
|
+
header: 'Priority',
|
|
320
|
+
render: (value: string) => <PriorityBadge priority={value} />,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
key: 'assignee',
|
|
324
|
+
header: 'Assignee',
|
|
325
|
+
render: (value: string) => value || 'Unassigned',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: 'id',
|
|
329
|
+
header: '',
|
|
330
|
+
render: (_: string, row: Task) => (
|
|
331
|
+
<div className="flex gap-1">
|
|
332
|
+
<button
|
|
333
|
+
onClick={() => startEditTask(row)}
|
|
334
|
+
className="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 transition-colors"
|
|
335
|
+
title="Edit task"
|
|
336
|
+
>
|
|
337
|
+
<Pencil className="h-4 w-4" />
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => setDeletingTaskId(row.id!)}
|
|
341
|
+
className="rounded p-1 text-gray-400 hover:bg-red-50 hover:text-red-600 transition-colors"
|
|
342
|
+
title="Delete task"
|
|
343
|
+
>
|
|
344
|
+
<Trash2 className="h-4 w-4" />
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
// --- Shared dialogs ---
|
|
352
|
+
|
|
353
|
+
const editProjectDialog = (
|
|
354
|
+
<Dialog
|
|
355
|
+
open={!!editingProject}
|
|
356
|
+
onClose={resetEditDialog}
|
|
357
|
+
title="Edit Project"
|
|
358
|
+
description="Update your project details."
|
|
359
|
+
>
|
|
360
|
+
<div className="space-y-4">
|
|
361
|
+
<Input
|
|
362
|
+
label="Project Name"
|
|
363
|
+
required
|
|
364
|
+
value={editForm.name}
|
|
365
|
+
onChange={(e) => { setEditForm({ ...editForm, name: e.target.value }); if (editNameError) setEditNameError(''); }}
|
|
366
|
+
error={editNameError}
|
|
367
|
+
placeholder="Enter project name"
|
|
368
|
+
/>
|
|
369
|
+
<Textarea
|
|
370
|
+
label="Description"
|
|
371
|
+
value={editForm.description}
|
|
372
|
+
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
|
|
373
|
+
rows={3}
|
|
374
|
+
placeholder="What is this project about?"
|
|
375
|
+
/>
|
|
376
|
+
<Select
|
|
377
|
+
label="Status"
|
|
378
|
+
value={editForm.status}
|
|
379
|
+
onChange={(e) => setEditForm({ ...editForm, status: e.target.value as Project['status'] })}
|
|
380
|
+
options={[...PROJECT_STATUS_OPTIONS]}
|
|
381
|
+
/>
|
|
382
|
+
<Input
|
|
383
|
+
label="Due Date"
|
|
384
|
+
type="date"
|
|
385
|
+
value={editForm.dueDate}
|
|
386
|
+
onChange={(e) => setEditForm({ ...editForm, dueDate: e.target.value })}
|
|
387
|
+
/>
|
|
388
|
+
<div className="flex gap-2 pt-2">
|
|
389
|
+
<Button onClick={handleEditProject} loading={editSubmitting}>Save Changes</Button>
|
|
390
|
+
<Button variant="secondary" onClick={resetEditDialog} disabled={editSubmitting}>Cancel</Button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</Dialog>
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const deleteProjectDialog = (
|
|
397
|
+
<ConfirmDialog
|
|
398
|
+
open={!!deletingProjectId}
|
|
399
|
+
onClose={() => setDeletingProjectId(null)}
|
|
400
|
+
onConfirm={handleDeleteProject}
|
|
401
|
+
title="Delete Project"
|
|
402
|
+
description={`Are you sure you want to delete "${deletingProject?.name || ''}"? This will remove the project and all its tasks. This action cannot be undone.`}
|
|
403
|
+
confirmLabel="Delete Project"
|
|
404
|
+
loading={deleteSubmitting}
|
|
405
|
+
/>
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const editTaskDialog = (
|
|
409
|
+
<Dialog
|
|
410
|
+
open={!!editingTask}
|
|
411
|
+
onClose={resetEditTaskDialog}
|
|
412
|
+
title="Edit Task"
|
|
413
|
+
description="Update task details."
|
|
414
|
+
>
|
|
415
|
+
<div className="space-y-4">
|
|
416
|
+
<Input
|
|
417
|
+
label="Task Title"
|
|
418
|
+
required
|
|
419
|
+
value={editTaskForm.title}
|
|
420
|
+
onChange={(e) => { setEditTaskForm({ ...editTaskForm, title: e.target.value }); if (editTitleError) setEditTitleError(''); }}
|
|
421
|
+
error={editTitleError}
|
|
422
|
+
placeholder="What needs to be done?"
|
|
423
|
+
/>
|
|
424
|
+
<Textarea
|
|
425
|
+
label="Description"
|
|
426
|
+
value={editTaskForm.description}
|
|
427
|
+
onChange={(e) => setEditTaskForm({ ...editTaskForm, description: e.target.value })}
|
|
428
|
+
rows={2}
|
|
429
|
+
placeholder="Add details..."
|
|
430
|
+
/>
|
|
431
|
+
<Select
|
|
432
|
+
label="Status"
|
|
433
|
+
value={editTaskForm.status}
|
|
434
|
+
onChange={(e) => setEditTaskForm({ ...editTaskForm, status: e.target.value as Task['status'] })}
|
|
435
|
+
options={[...TASK_STATUS_OPTIONS]}
|
|
436
|
+
/>
|
|
437
|
+
<Select
|
|
438
|
+
label="Priority"
|
|
439
|
+
value={editTaskForm.priority}
|
|
440
|
+
onChange={(e) => setEditTaskForm({ ...editTaskForm, priority: e.target.value as Task['priority'] })}
|
|
441
|
+
options={[...PRIORITY_OPTIONS]}
|
|
442
|
+
/>
|
|
443
|
+
<div className="flex gap-2 pt-2">
|
|
444
|
+
<Button onClick={handleEditTask} loading={editTaskSubmitting}>Save Changes</Button>
|
|
445
|
+
<Button variant="secondary" onClick={resetEditTaskDialog} disabled={editTaskSubmitting}>Cancel</Button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</Dialog>
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const deleteTaskDialog = (
|
|
452
|
+
<ConfirmDialog
|
|
453
|
+
open={!!deletingTaskId}
|
|
454
|
+
onClose={() => setDeletingTaskId(null)}
|
|
455
|
+
onConfirm={handleDeleteTask}
|
|
456
|
+
title="Delete Task"
|
|
457
|
+
description="Are you sure you want to delete this task? This action cannot be undone."
|
|
458
|
+
confirmLabel="Delete Task"
|
|
459
|
+
loading={deleteTaskSubmitting}
|
|
460
|
+
/>
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// ---------- PROJECT DETAIL VIEW ----------
|
|
464
|
+
if (selectedProject) {
|
|
465
|
+
const todoCount = tasks.filter((t) => t.status === 'todo').length;
|
|
466
|
+
const inProgressCount = tasks.filter((t) => t.status === 'in_progress').length;
|
|
467
|
+
const doneCount = tasks.filter((t) => t.status === 'done').length;
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<div className="space-y-6">
|
|
471
|
+
{editProjectDialog}
|
|
472
|
+
{deleteProjectDialog}
|
|
473
|
+
{editTaskDialog}
|
|
474
|
+
{deleteTaskDialog}
|
|
475
|
+
|
|
476
|
+
<div>
|
|
477
|
+
<Button
|
|
478
|
+
variant="ghost"
|
|
479
|
+
size="sm"
|
|
480
|
+
onClick={() => setSelectedProjectId(null)}
|
|
481
|
+
icon={<ArrowLeft className="h-4 w-4" />}
|
|
482
|
+
className="mb-2"
|
|
483
|
+
>
|
|
484
|
+
Back to Projects
|
|
485
|
+
</Button>
|
|
486
|
+
<div className="flex items-center justify-between">
|
|
487
|
+
<div>
|
|
488
|
+
<h1 className="text-2xl font-bold text-gray-900">{selectedProject.name}</h1>
|
|
489
|
+
<p className="mt-1 text-sm text-gray-600">{selectedProject.description}</p>
|
|
490
|
+
</div>
|
|
491
|
+
<div className="flex items-center gap-2">
|
|
492
|
+
<ProjectStatusBadge status={selectedProject.status} />
|
|
493
|
+
<Button variant="ghost" size="sm" onClick={() => startEditProject(selectedProject)} icon={<Pencil className="h-4 w-4" />}>
|
|
494
|
+
Edit
|
|
495
|
+
</Button>
|
|
496
|
+
<Button variant="ghost" size="sm" onClick={() => setDeletingProjectId(selectedProject.id!)} className="text-red-600 hover:text-red-700 hover:bg-red-50" icon={<Trash2 className="h-4 w-4" />}>
|
|
497
|
+
Delete
|
|
498
|
+
</Button>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
504
|
+
<KPICard title="To Do" value={todoCount} icon={<ListTodo className="h-5 w-5" />} />
|
|
505
|
+
<KPICard title="In Progress" value={inProgressCount} icon={<Clock className="h-5 w-5" />} />
|
|
506
|
+
<KPICard title="Completed" value={doneCount} icon={<CheckCircle className="h-5 w-5" />} trend={doneCount > 0 ? 'up' : undefined} />
|
|
507
|
+
<KPICard title="Team Members" value={selectedProject.members?.length || 0} icon={<Users className="h-5 w-5" />} />
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="flex items-center justify-between">
|
|
511
|
+
<h2 className="text-lg font-semibold text-gray-900">Tasks</h2>
|
|
512
|
+
<Button size="sm" onClick={() => setTaskDialogOpen(true)} icon={<Plus className="h-4 w-4" />}>
|
|
513
|
+
Add Task
|
|
514
|
+
</Button>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<Dialog open={taskDialogOpen} onClose={resetTaskDialog} title="Add Task" description="Create a new task for this project.">
|
|
518
|
+
<div className="space-y-4">
|
|
519
|
+
<Input
|
|
520
|
+
label="Task Title"
|
|
521
|
+
required
|
|
522
|
+
value={taskForm.title}
|
|
523
|
+
onChange={(e) => { setTaskForm({ ...taskForm, title: e.target.value }); if (titleError) setTitleError(''); }}
|
|
524
|
+
error={titleError}
|
|
525
|
+
placeholder="What needs to be done?"
|
|
526
|
+
/>
|
|
527
|
+
<Select
|
|
528
|
+
label="Priority"
|
|
529
|
+
value={taskForm.priority}
|
|
530
|
+
onChange={(e) => setTaskForm({ ...taskForm, priority: e.target.value as Task['priority'] })}
|
|
531
|
+
options={[...PRIORITY_OPTIONS]}
|
|
532
|
+
/>
|
|
533
|
+
<div className="flex gap-2 pt-2">
|
|
534
|
+
<Button onClick={handleCreateTask} loading={taskSubmitting}>Add Task</Button>
|
|
535
|
+
<Button variant="secondary" onClick={resetTaskDialog} disabled={taskSubmitting}>Cancel</Button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</Dialog>
|
|
539
|
+
|
|
540
|
+
<div className="rounded-xl border border-gray-200 bg-white shadow-sm">
|
|
541
|
+
<DataTable columns={taskColumns} data={tasks} loading={tasksLoading} emptyMessage="No tasks yet. Add a task to get started." />
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ---------- PROJECTS LIST VIEW ----------
|
|
548
|
+
return (
|
|
549
|
+
<div className="space-y-6">
|
|
550
|
+
{editProjectDialog}
|
|
551
|
+
{deleteProjectDialog}
|
|
552
|
+
|
|
553
|
+
<div className="flex items-center justify-between">
|
|
554
|
+
<div>
|
|
555
|
+
<h1 className="text-2xl font-bold text-gray-900">Projects</h1>
|
|
556
|
+
<p className="mt-1 text-sm text-gray-600">Manage your projects and track progress.</p>
|
|
557
|
+
</div>
|
|
558
|
+
<div className="flex items-center gap-2">
|
|
559
|
+
{projects.length > 0 && (
|
|
560
|
+
<Button
|
|
561
|
+
variant="secondary"
|
|
562
|
+
size="sm"
|
|
563
|
+
onClick={() => downloadCSV(
|
|
564
|
+
projects.map((p) => ({ name: p.name, status: p.status, description: p.description || '', dueDate: p.dueDate || '', createdAt: p.createdAt })),
|
|
565
|
+
'projects.csv'
|
|
566
|
+
)}
|
|
567
|
+
icon={<Download className="h-4 w-4" />}
|
|
568
|
+
>
|
|
569
|
+
Export
|
|
570
|
+
</Button>
|
|
571
|
+
)}
|
|
572
|
+
<Button onClick={() => setCreateDialogOpen(true)} icon={<Plus className="h-4 w-4" />}>
|
|
573
|
+
New Project
|
|
574
|
+
</Button>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<Dialog open={createDialogOpen} onClose={resetCreateDialog} title="Create New Project" description="Add a new project to organize your team's work.">
|
|
579
|
+
<div className="space-y-4">
|
|
580
|
+
<Input
|
|
581
|
+
label="Project Name"
|
|
582
|
+
required
|
|
583
|
+
value={projectForm.name}
|
|
584
|
+
onChange={(e) => { setProjectForm({ ...projectForm, name: e.target.value }); if (nameError) setNameError(''); }}
|
|
585
|
+
error={nameError}
|
|
586
|
+
placeholder="Enter project name"
|
|
587
|
+
/>
|
|
588
|
+
<Textarea
|
|
589
|
+
label="Description"
|
|
590
|
+
value={projectForm.description}
|
|
591
|
+
onChange={(e) => setProjectForm({ ...projectForm, description: e.target.value })}
|
|
592
|
+
rows={3}
|
|
593
|
+
placeholder="What is this project about?"
|
|
594
|
+
/>
|
|
595
|
+
<Input
|
|
596
|
+
label="Due Date"
|
|
597
|
+
type="date"
|
|
598
|
+
value={projectForm.dueDate}
|
|
599
|
+
onChange={(e) => setProjectForm({ ...projectForm, dueDate: e.target.value })}
|
|
600
|
+
hint="Defaults to 30 days from today if left blank"
|
|
601
|
+
/>
|
|
602
|
+
<div className="flex gap-2 pt-2">
|
|
603
|
+
<Button onClick={handleCreateProject} loading={projectSubmitting}>Create Project</Button>
|
|
604
|
+
<Button variant="secondary" onClick={resetCreateDialog} disabled={projectSubmitting}>Cancel</Button>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
</Dialog>
|
|
608
|
+
|
|
609
|
+
{error && (
|
|
610
|
+
<div className="flex items-center justify-between rounded-lg border border-red-200 bg-red-50 px-4 py-3">
|
|
611
|
+
<p className="text-sm text-red-700">Failed to load projects. Please check your connection and try again.</p>
|
|
612
|
+
<button onClick={refresh} className="text-sm font-medium text-red-700 hover:text-red-800 underline">Retry</button>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{!loading && projects.length === 0 && !createDialogOpen ? (
|
|
617
|
+
<EmptyState
|
|
618
|
+
title="No projects yet"
|
|
619
|
+
description="Create your first project to get started."
|
|
620
|
+
icon={<FolderKanban className="h-12 w-12 text-gray-400" />}
|
|
621
|
+
action={{ label: 'Create Project', onClick: () => setCreateDialogOpen(true) }}
|
|
622
|
+
/>
|
|
623
|
+
) : (
|
|
624
|
+
<div className="rounded-xl border border-gray-200 bg-white shadow-sm">
|
|
625
|
+
<DataTable
|
|
626
|
+
columns={projectColumns}
|
|
627
|
+
data={projects}
|
|
628
|
+
loading={loading}
|
|
629
|
+
pagination
|
|
630
|
+
pageSize={10}
|
|
631
|
+
hoverable
|
|
632
|
+
onRowClick={(row) => setSelectedProjectId(row.id)}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
)}
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
}
|