@locusai/web 0.1.0

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/next.config.js +7 -0
  3. package/package.json +37 -0
  4. package/postcss.config.mjs +5 -0
  5. package/src/app/backlog/page.tsx +19 -0
  6. package/src/app/docs/page.tsx +7 -0
  7. package/src/app/globals.css +603 -0
  8. package/src/app/layout.tsx +43 -0
  9. package/src/app/page.tsx +16 -0
  10. package/src/app/providers.tsx +16 -0
  11. package/src/app/settings/page.tsx +194 -0
  12. package/src/components/BoardFilter.tsx +98 -0
  13. package/src/components/Header.tsx +21 -0
  14. package/src/components/PropertyItem.tsx +98 -0
  15. package/src/components/Sidebar.tsx +109 -0
  16. package/src/components/TaskCard.tsx +138 -0
  17. package/src/components/TaskCreateModal.tsx +243 -0
  18. package/src/components/TaskPanel.tsx +765 -0
  19. package/src/components/index.ts +7 -0
  20. package/src/components/ui/Badge.tsx +77 -0
  21. package/src/components/ui/Button.tsx +47 -0
  22. package/src/components/ui/Checkbox.tsx +52 -0
  23. package/src/components/ui/Dropdown.tsx +107 -0
  24. package/src/components/ui/Input.tsx +36 -0
  25. package/src/components/ui/Modal.tsx +79 -0
  26. package/src/components/ui/Textarea.tsx +21 -0
  27. package/src/components/ui/index.ts +7 -0
  28. package/src/hooks/useTasks.ts +119 -0
  29. package/src/lib/api-client.ts +24 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/services/doc.service.ts +27 -0
  32. package/src/services/index.ts +3 -0
  33. package/src/services/sprint.service.ts +26 -0
  34. package/src/services/task.service.ts +75 -0
  35. package/src/views/Backlog.tsx +691 -0
  36. package/src/views/Board.tsx +306 -0
  37. package/src/views/Docs.tsx +625 -0
  38. package/tsconfig.json +21 -0
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ import { SprintStatus, TaskStatus } from "@locusai/shared";
4
+ import { useQuery } from "@tanstack/react-query";
5
+ import { AnimatePresence } from "framer-motion";
6
+ import { MoreHorizontal, Plus } from "lucide-react";
7
+ import { useRouter } from "next/navigation";
8
+ import { useCallback, useEffect, useState } from "react";
9
+ import { BoardFilter } from "@/components/BoardFilter";
10
+ import { TaskCard } from "@/components/TaskCard";
11
+ import { TaskCreateModal } from "@/components/TaskCreateModal";
12
+ import { TaskPanel } from "@/components/TaskPanel";
13
+ import { Button } from "@/components/ui/Button";
14
+ import { useTasks } from "@/hooks/useTasks";
15
+ import { cn } from "@/lib/utils";
16
+ import { sprintService } from "@/services";
17
+
18
+ const COLUMNS = [
19
+ TaskStatus.BACKLOG,
20
+ TaskStatus.IN_PROGRESS,
21
+ TaskStatus.REVIEW,
22
+ TaskStatus.VERIFICATION,
23
+ TaskStatus.DONE,
24
+ TaskStatus.BLOCKED,
25
+ ];
26
+
27
+ const STATUS_CONFIG: Record<
28
+ string,
29
+ { label: string; color: string; indicator: string }
30
+ > = {
31
+ [TaskStatus.BACKLOG]: {
32
+ label: "Backlog",
33
+ color: "var(--status-backlog)",
34
+ indicator: "bg-slate-500",
35
+ },
36
+ [TaskStatus.IN_PROGRESS]: {
37
+ label: "In Progress",
38
+ color: "var(--status-progress)",
39
+ indicator: "bg-amber-500",
40
+ },
41
+ [TaskStatus.REVIEW]: {
42
+ label: "Review",
43
+ color: "var(--status-review)",
44
+ indicator: "bg-purple-500",
45
+ },
46
+ [TaskStatus.VERIFICATION]: {
47
+ label: "Verification",
48
+ color: "var(--accent)",
49
+ indicator: "bg-cyan-500",
50
+ },
51
+ [TaskStatus.DONE]: {
52
+ label: "Done",
53
+ color: "var(--status-done)",
54
+ indicator: "bg-emerald-500",
55
+ },
56
+ [TaskStatus.BLOCKED]: {
57
+ label: "Blocked",
58
+ color: "var(--status-blocked)",
59
+ indicator: "bg-rose-500",
60
+ },
61
+ };
62
+
63
+ export function Board() {
64
+ const router = useRouter();
65
+ const {
66
+ searchQuery,
67
+ setSearchQuery,
68
+ priorityFilter,
69
+ setPriorityFilter,
70
+ assigneeFilter,
71
+ setAssigneeFilter,
72
+ hasActiveFilters,
73
+ clearFilters,
74
+ getTasksByStatus,
75
+ updateTaskStatus,
76
+ deleteTask,
77
+ refreshTasks,
78
+ } = useTasks();
79
+
80
+ const { data: sprints = [] } = useQuery({
81
+ queryKey: ["sprints"],
82
+ queryFn: sprintService.getAll,
83
+ });
84
+
85
+ const activeSprint = sprints.find((s) => s.status === SprintStatus.ACTIVE);
86
+
87
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
88
+ const [createModalStatus, setCreateModalStatus] = useState<TaskStatus>(
89
+ TaskStatus.BACKLOG
90
+ );
91
+ const [selectedTaskId, setSelectedTaskId] = useState<number | null>(null);
92
+ const [dragOverColumn, setDragOverColumn] = useState<TaskStatus | null>(null);
93
+
94
+ const handleOpenCreateModal = useCallback((status: TaskStatus) => {
95
+ setCreateModalStatus(status);
96
+ setIsCreateModalOpen(true);
97
+ }, []);
98
+
99
+ // Keyboard shortcuts
100
+ useEffect(() => {
101
+ const handleKeyDown = (e: KeyboardEvent) => {
102
+ if (
103
+ e.target instanceof HTMLInputElement ||
104
+ e.target instanceof HTMLTextAreaElement
105
+ ) {
106
+ return;
107
+ }
108
+ if (e.key === "n" || e.key === "N") {
109
+ e.preventDefault();
110
+ handleOpenCreateModal(TaskStatus.BACKLOG);
111
+ }
112
+ if (e.key === "/" && !e.metaKey && !e.ctrlKey) {
113
+ e.preventDefault();
114
+ const searchInput = document.querySelector(
115
+ ".search-input"
116
+ ) as HTMLInputElement;
117
+ searchInput?.focus();
118
+ }
119
+ if (e.key === "Escape" && selectedTaskId) {
120
+ setSelectedTaskId(null);
121
+ }
122
+ };
123
+
124
+ document.addEventListener("keydown", handleKeyDown);
125
+ return () => document.removeEventListener("keydown", handleKeyDown);
126
+ }, [selectedTaskId, handleOpenCreateModal]);
127
+
128
+ const handleDrop = async (status: TaskStatus, e: React.DragEvent) => {
129
+ e.preventDefault();
130
+ setDragOverColumn(null);
131
+ const taskId = e.dataTransfer.getData("taskId");
132
+ if (!taskId) return;
133
+ await updateTaskStatus(Number(taskId), status);
134
+ };
135
+
136
+ const handleDragOver = (status: TaskStatus, e: React.DragEvent) => {
137
+ e.preventDefault();
138
+ e.dataTransfer.dropEffect = "move";
139
+ setDragOverColumn(status);
140
+ };
141
+
142
+ const handleDragLeave = () => {
143
+ setDragOverColumn(null);
144
+ };
145
+
146
+ if (!activeSprint) {
147
+ return (
148
+ <div className="flex-1 flex flex-col items-center justify-center p-8 text-center space-y-4">
149
+ <div className="bg-primary/5 p-4 rounded-full">
150
+ <svg
151
+ className="w-12 h-12 text-primary/40"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ viewBox="0 0 24 24"
155
+ >
156
+ <title>Icon</title>
157
+ <path
158
+ strokeLinecap="round"
159
+ strokeLinejoin="round"
160
+ strokeWidth={2}
161
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
162
+ />
163
+ </svg>
164
+ </div>
165
+ <div>
166
+ <h2 className="text-xl font-bold">No Active Sprint</h2>
167
+ <p className="text-muted-foreground mt-2 max-w-sm mx-auto">
168
+ You need to start a sprint from the Backlog to see tasks on the
169
+ board.
170
+ </p>
171
+ </div>
172
+ <Button onClick={() => router.push("/backlog")} variant="secondary">
173
+ Go to Backlog
174
+ </Button>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ return (
180
+ <div className="flex-1 overflow-auto bg-background">
181
+ <div className="mb-6">
182
+ <div className="flex justify-between items-center">
183
+ <div>
184
+ <div className="flex items-center gap-3">
185
+ <h1 className="text-2xl font-bold tracking-tight text-foreground">
186
+ {activeSprint.name}
187
+ </h1>
188
+ <span className="px-2 py-0.5 rounded-full bg-primary/10 text-primary text-[10px] font-black uppercase tracking-wider">
189
+ Active
190
+ </span>
191
+ </div>
192
+ <p className="text-muted-foreground text-sm mt-1 flex items-center gap-3">
193
+ Track and manage engineering tasks
194
+ <span className="hidden md:inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-secondary/60 text-[10px] font-semibold text-muted-foreground">
195
+ <kbd className="bg-background/80 px-1.5 py-0.5 rounded text-[9px] font-bold border">
196
+ N
197
+ </kbd>
198
+ New task
199
+ </span>
200
+ </p>
201
+ </div>
202
+ <Button
203
+ onClick={() => handleOpenCreateModal(TaskStatus.BACKLOG)}
204
+ className="shadow-lg shadow-primary/20"
205
+ >
206
+ <Plus size={16} className="mr-1.5" />
207
+ New Task
208
+ </Button>
209
+ </div>
210
+ </div>
211
+ <BoardFilter
212
+ searchQuery={searchQuery}
213
+ onSearchChange={setSearchQuery}
214
+ priorityFilter={priorityFilter}
215
+ onPriorityChange={setPriorityFilter}
216
+ assigneeFilter={assigneeFilter}
217
+ onAssigneeChange={setAssigneeFilter}
218
+ onClearFilters={clearFilters}
219
+ hasActiveFilters={hasActiveFilters}
220
+ />
221
+ <div className="flex gap-4 min-h-[600px] pb-4">
222
+ {COLUMNS.map((status) => {
223
+ // Filter tasks by active sprint ID
224
+ const columnTasks = getTasksByStatus(status).filter(
225
+ (t) => t.sprintId === activeSprint.id
226
+ );
227
+ const isDragOver = dragOverColumn === status;
228
+
229
+ return (
230
+ <div
231
+ key={status}
232
+ className={cn(
233
+ "flex flex-col w-[280px] shrink-0 rounded-xl transition-all border",
234
+ isDragOver
235
+ ? "bg-primary/5 border-primary/40 ring-1 ring-primary/20"
236
+ : "bg-card/50 border-border/50"
237
+ )}
238
+ onDrop={(e) => handleDrop(status, e)}
239
+ onDragOver={(e) => handleDragOver(status, e)}
240
+ onDragLeave={handleDragLeave}
241
+ >
242
+ <div className="flex items-center justify-between p-3 border-b border-border/50">
243
+ <div className="flex items-center gap-2.5">
244
+ <div
245
+ className={cn(
246
+ "h-2.5 w-2.5 rounded-full",
247
+ STATUS_CONFIG[status].indicator
248
+ )}
249
+ />
250
+ <span className="text-xs font-semibold text-foreground">
251
+ {STATUS_CONFIG[status].label}
252
+ </span>
253
+ <span className="flex items-center justify-center h-5 min-w-[20px] px-1.5 bg-secondary/80 text-[10px] font-bold rounded-md text-muted-foreground">
254
+ {columnTasks.length}
255
+ </span>
256
+ </div>
257
+ <button className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground transition-colors">
258
+ <MoreHorizontal size={14} />
259
+ </button>
260
+ </div>
261
+
262
+ <div className="flex flex-col gap-2.5 p-2.5 flex-1 overflow-y-auto">
263
+ {columnTasks.map((task) => (
264
+ <TaskCard
265
+ key={task.id}
266
+ task={task}
267
+ onClick={() => setSelectedTaskId(task.id)}
268
+ onDelete={deleteTask}
269
+ />
270
+ ))}
271
+
272
+ <Button
273
+ variant="ghost"
274
+ className="w-full justify-center text-xs font-medium text-muted-foreground/60 hover:text-foreground hover:bg-secondary/40 h-8 border border-dashed border-border/50 hover:border-border rounded-lg"
275
+ onClick={() => handleOpenCreateModal(status)}
276
+ >
277
+ <Plus size={14} className="mr-1.5" />
278
+ Add
279
+ </Button>
280
+ </div>
281
+ </div>
282
+ );
283
+ })}
284
+ </div>
285
+ <TaskCreateModal
286
+ isOpen={isCreateModalOpen}
287
+ onClose={() => setIsCreateModalOpen(false)}
288
+ onCreated={refreshTasks}
289
+ initialStatus={createModalStatus}
290
+ sprintId={activeSprint?.id}
291
+ />
292
+
293
+ <AnimatePresence>
294
+ {selectedTaskId && (
295
+ <TaskPanel
296
+ key="task-panel"
297
+ taskId={selectedTaskId}
298
+ onClose={() => setSelectedTaskId(null)}
299
+ onDeleted={refreshTasks}
300
+ onUpdated={refreshTasks}
301
+ />
302
+ )}
303
+ </AnimatePresence>
304
+ </div>
305
+ );
306
+ }