@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,7 @@
1
+ export * from "./BoardFilter";
2
+ export * from "./Header";
3
+ export * from "./PropertyItem";
4
+ export * from "./Sidebar";
5
+ export * from "./TaskCard";
6
+ export * from "./TaskCreateModal";
7
+ export * from "./TaskPanel";
@@ -0,0 +1,77 @@
1
+ interface BadgeProps {
2
+ children: React.ReactNode;
3
+ variant?: "default" | "success" | "warning" | "error" | "info" | "purple";
4
+ size?: "sm" | "md";
5
+ }
6
+
7
+ export function Badge({
8
+ children,
9
+ variant = "default",
10
+ size = "sm",
11
+ }: BadgeProps) {
12
+ const variants = {
13
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/80",
14
+ success: "bg-status-done/15 text-status-done border-status-done/20",
15
+ warning:
16
+ "bg-status-progress/15 text-status-progress border-status-progress/20",
17
+ error: "bg-status-blocked/15 text-status-blocked border-status-blocked/20",
18
+ info: "bg-status-todo/15 text-status-todo border-status-todo/20",
19
+ purple: "bg-status-review/15 text-status-review border-status-review/20",
20
+ };
21
+
22
+ const sizes = {
23
+ sm: "px-2.5 py-0.5 text-xs font-semibold",
24
+ md: "px-3 py-1 text-sm font-semibold",
25
+ };
26
+
27
+ return (
28
+ <span
29
+ className={`inline-flex items-center rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${variants[variant]} ${sizes[size]}`}
30
+ >
31
+ {children}
32
+ </span>
33
+ );
34
+ }
35
+
36
+ interface PriorityBadgeProps {
37
+ priority: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
38
+ }
39
+
40
+ const PRIORITY_CONFIG = {
41
+ LOW: { label: "Low", variant: "default" as const },
42
+ MEDIUM: { label: "Medium", variant: "info" as const },
43
+ HIGH: { label: "High", variant: "warning" as const },
44
+ CRITICAL: { label: "Critical", variant: "error" as const },
45
+ };
46
+
47
+ export function PriorityBadge({ priority }: PriorityBadgeProps) {
48
+ const config = PRIORITY_CONFIG[priority];
49
+ return <Badge variant={config.variant}>{config.label}</Badge>;
50
+ }
51
+
52
+ interface StatusBadgeProps {
53
+ status: string;
54
+ }
55
+
56
+ const STATUS_CONFIG: Record<
57
+ string,
58
+ {
59
+ label: string;
60
+ variant: "default" | "success" | "warning" | "error" | "info" | "purple";
61
+ }
62
+ > = {
63
+ BACKLOG: { label: "Backlog", variant: "default" },
64
+ IN_PROGRESS: { label: "In Progress", variant: "warning" },
65
+ REVIEW: { label: "Review", variant: "purple" },
66
+ VERIFICATION: { label: "Verification", variant: "info" },
67
+ DONE: { label: "Done", variant: "success" },
68
+ BLOCKED: { label: "Blocked", variant: "error" },
69
+ };
70
+
71
+ export function StatusBadge({ status }: StatusBadgeProps) {
72
+ const config = STATUS_CONFIG[status] || {
73
+ label: status,
74
+ variant: "default" as const,
75
+ };
76
+ return <Badge variant={config.variant}>{config.label}</Badge>;
77
+ }
@@ -0,0 +1,47 @@
1
+ import { type ButtonHTMLAttributes, forwardRef } from "react";
2
+
3
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?:
5
+ | "primary"
6
+ | "secondary"
7
+ | "outline"
8
+ | "ghost"
9
+ | "danger"
10
+ | "success";
11
+ size?: "sm" | "md" | "lg" | "icon";
12
+ }
13
+
14
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
15
+ ({ className, variant = "primary", size = "md", ...props }, ref) => {
16
+ const variants = {
17
+ primary: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
+ outline:
21
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
22
+ ghost: "hover:bg-accent hover:text-accent-foreground",
23
+ danger:
24
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
25
+ success: "bg-emerald-600 text-white shadow-sm hover:bg-emerald-700",
26
+ };
27
+
28
+ const sizes = {
29
+ sm: "h-8 px-3 text-xs",
30
+ md: "h-9 px-4 py-2 text-sm",
31
+ lg: "h-10 px-8 text-base",
32
+ icon: "h-9 w-9",
33
+ };
34
+
35
+ return (
36
+ <button
37
+ ref={ref}
38
+ className={`inline-flex items-center justify-center gap-2 font-semibold rounded-lg transition-all duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none ${variants[variant]} ${sizes[size]} ${className || ""}`}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+ );
44
+
45
+ Button.displayName = "Button";
46
+
47
+ export { Button };
@@ -0,0 +1,52 @@
1
+ import { Check } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ interface CheckboxProps {
5
+ checked: boolean;
6
+ onChange: (checked: boolean) => void;
7
+ label?: string;
8
+ disabled?: boolean;
9
+ className?: string;
10
+ }
11
+
12
+ export function Checkbox({
13
+ checked,
14
+ onChange,
15
+ label,
16
+ disabled = false,
17
+ className,
18
+ }: CheckboxProps) {
19
+ return (
20
+ <label
21
+ className={cn(
22
+ `flex items-center gap-2.5 select-none transition-opacity duration-200`,
23
+ disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
24
+ className || ""
25
+ )}
26
+ >
27
+ <input
28
+ type="checkbox"
29
+ checked={checked}
30
+ onChange={(e) => onChange(e.target.checked)}
31
+ disabled={disabled}
32
+ className="hidden"
33
+ />
34
+ <span
35
+ className={`h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-150 flex items-center justify-center ${
36
+ checked ? "bg-primary text-primary-foreground" : "bg-transparent"
37
+ }`}
38
+ >
39
+ {checked && <Check size={12} strokeWidth={3} />}
40
+ </span>
41
+ {label && (
42
+ <span
43
+ className={`text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 transition-all duration-150 ${
44
+ checked ? "line-through text-muted-foreground" : "text-foreground"
45
+ }`}
46
+ >
47
+ {label}
48
+ </span>
49
+ )}
50
+ </label>
51
+ );
52
+ }
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import { ChevronDown } from "lucide-react";
4
+ import { type ReactNode, useEffect, useRef, useState } from "react";
5
+
6
+ interface DropdownOption<T extends string> {
7
+ value: T;
8
+ label: string;
9
+ color?: string;
10
+ }
11
+
12
+ interface DropdownProps<T extends string> {
13
+ value: T | undefined;
14
+ onChange: (value: T) => void;
15
+ options: DropdownOption<T>[];
16
+ placeholder?: string;
17
+ label?: string;
18
+ disabled?: boolean;
19
+ renderOption?: (option: DropdownOption<T>) => ReactNode;
20
+ }
21
+
22
+ export function Dropdown<T extends string>({
23
+ value,
24
+ onChange,
25
+ options,
26
+ placeholder = "Select...",
27
+ label,
28
+ disabled = false,
29
+ renderOption,
30
+ }: DropdownProps<T>) {
31
+ const [isOpen, setIsOpen] = useState(false);
32
+ const containerRef = useRef<HTMLDivElement>(null);
33
+
34
+ useEffect(() => {
35
+ const handleClickOutside = (e: MouseEvent) => {
36
+ if (
37
+ containerRef.current &&
38
+ !containerRef.current.contains(e.target as Node)
39
+ ) {
40
+ setIsOpen(false);
41
+ }
42
+ };
43
+
44
+ document.addEventListener("mousedown", handleClickOutside);
45
+ return () => document.removeEventListener("mousedown", handleClickOutside);
46
+ }, []);
47
+
48
+ const selectedOption = options.find((o) => o.value === value);
49
+
50
+ return (
51
+ <div className="relative w-full" ref={containerRef}>
52
+ {label && (
53
+ <label className="block text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wider">
54
+ {label}
55
+ </label>
56
+ )}
57
+ <button
58
+ type="button"
59
+ className="w-full flex items-center justify-between gap-2 px-3.5 py-2.5 bg-background border border-input rounded-md text-foreground text-sm cursor-pointer transition-all hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
60
+ onClick={() => !disabled && setIsOpen(!isOpen)}
61
+ disabled={disabled}
62
+ >
63
+ <span className={selectedOption ? "" : "text-muted-foreground"}>
64
+ {selectedOption ? selectedOption.label : placeholder}
65
+ </span>
66
+ <ChevronDown
67
+ size={16}
68
+ className={`transition-transform duration-200 text-muted-foreground ${isOpen ? "rotate-180" : "rotate-0"}`}
69
+ />
70
+ </button>
71
+
72
+ {isOpen && (
73
+ <div className="absolute top-full left-0 right-0 mt-2 bg-popover border border-border rounded-md p-1 z-50 max-h-[240px] overflow-y-auto animate-in fade-in slide-in-from-top-2 duration-200 shadow-md">
74
+ {options.map((option) => (
75
+ <button
76
+ key={option.value}
77
+ type="button"
78
+ className={`w-full flex items-center gap-2 px-3 py-2 bg-transparent border-none rounded-sm text-popover-foreground text-sm cursor-pointer transition-colors text-left hover:bg-accent hover:text-accent-foreground ${
79
+ option.value === value
80
+ ? "bg-accent/10 text-accent-foreground"
81
+ : ""
82
+ }`}
83
+ onClick={() => {
84
+ onChange(option.value);
85
+ setIsOpen(false);
86
+ }}
87
+ >
88
+ {renderOption ? (
89
+ renderOption(option)
90
+ ) : (
91
+ <>
92
+ {option.color && (
93
+ <span
94
+ className="w-2 h-2 rounded-full shrink-0"
95
+ style={{ background: option.color }}
96
+ />
97
+ )}
98
+ {option.label}
99
+ </>
100
+ )}
101
+ </button>
102
+ ))}
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,36 @@
1
+ import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
2
+
3
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
4
+ icon?: ReactNode;
5
+ rightElement?: ReactNode;
6
+ }
7
+
8
+ const Input = forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, icon, rightElement, ...props }, ref) => {
10
+ return (
11
+ <div className="relative w-full group">
12
+ {icon && (
13
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted group-focus-within:text-accent transition-colors">
14
+ {icon}
15
+ </div>
16
+ )}
17
+ <input
18
+ ref={ref}
19
+ className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
20
+ icon ? "pl-10" : "pl-3"
21
+ } ${rightElement ? "pr-10" : "pr-3"} ${className || ""}`}
22
+ {...props}
23
+ />
24
+ {rightElement && (
25
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
26
+ {rightElement}
27
+ </div>
28
+ )}
29
+ </div>
30
+ );
31
+ }
32
+ );
33
+
34
+ Input.displayName = "Input";
35
+
36
+ export { Input };
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import { X } from "lucide-react";
4
+ import { type ReactNode, useEffect, useRef } from "react";
5
+ import { createPortal } from "react-dom";
6
+
7
+ interface ModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ children: ReactNode;
11
+ title?: string;
12
+ size?: "sm" | "md" | "lg";
13
+ }
14
+
15
+ export function Modal({
16
+ isOpen,
17
+ onClose,
18
+ children,
19
+ title,
20
+ size = "md",
21
+ }: ModalProps) {
22
+ const overlayRef = useRef<HTMLDivElement>(null);
23
+
24
+ useEffect(() => {
25
+ const handleEscape = (e: KeyboardEvent) => {
26
+ if (e.key === "Escape") onClose();
27
+ };
28
+
29
+ if (isOpen) {
30
+ document.addEventListener("keydown", handleEscape);
31
+ document.body.style.overflow = "hidden";
32
+ }
33
+
34
+ return () => {
35
+ document.removeEventListener("keydown", handleEscape);
36
+ document.body.style.overflow = "";
37
+ };
38
+ }, [isOpen, onClose]);
39
+
40
+ if (!isOpen) return null;
41
+
42
+ const widths = {
43
+ sm: "w-[400px]",
44
+ md: "w-[520px]",
45
+ lg: "w-[680px]",
46
+ };
47
+
48
+ return createPortal(
49
+ <div
50
+ ref={overlayRef}
51
+ className="fixed inset-0 bg-black/40 backdrop-blur-md flex items-center justify-center z-1000 animate-in fade-in duration-300"
52
+ onClick={(e) => {
53
+ if (e.target === overlayRef.current) onClose();
54
+ }}
55
+ >
56
+ <div
57
+ className={`bg-background border border-border rounded-xl shadow-lg animate-in zoom-in-95 duration-200 max-h-[90vh] overflow-hidden flex flex-col ${
58
+ widths[size]
59
+ } max-w-[90vw]`}
60
+ >
61
+ {title && (
62
+ <div className="flex justify-between items-center p-5 border-b border-border">
63
+ <h3 className="text-lg font-semibold m-0 text-foreground">
64
+ {title}
65
+ </h3>
66
+ <button
67
+ className="bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer p-0 transition-colors duration-200"
68
+ onClick={onClose}
69
+ >
70
+ <X size={20} />
71
+ </button>
72
+ </div>
73
+ )}
74
+ <div className="p-6 overflow-y-auto">{children}</div>
75
+ </div>
76
+ </div>,
77
+ document.body
78
+ );
79
+ }
@@ -0,0 +1,21 @@
1
+ import { forwardRef, type TextareaHTMLAttributes } from "react";
2
+
3
+ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {}
4
+
5
+ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
6
+ ({ className, ...props }, ref) => {
7
+ return (
8
+ <textarea
9
+ ref={ref}
10
+ className={`flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-none ${
11
+ className || ""
12
+ }`}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+ );
18
+
19
+ Textarea.displayName = "Textarea";
20
+
21
+ export { Textarea };
@@ -0,0 +1,7 @@
1
+ export * from "./Badge";
2
+ export * from "./Button";
3
+ export * from "./Checkbox";
4
+ export * from "./Dropdown";
5
+ export * from "./Input";
6
+ export * from "./Modal";
7
+ export * from "./Textarea";
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { AssigneeRole, TaskPriority, TaskStatus } from "@locusai/shared";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useCallback, useMemo, useState } from "react";
6
+
7
+ import { taskService } from "@/services";
8
+
9
+ export function useTasks() {
10
+ const queryClient = useQueryClient();
11
+
12
+ const {
13
+ data: tasks = [],
14
+ isLoading: loading,
15
+ error: queryError,
16
+ refetch: refreshTasks,
17
+ } = useQuery({
18
+ queryKey: ["tasks"],
19
+ queryFn: () => taskService.getAll(),
20
+ refetchInterval: 15000,
21
+ });
22
+
23
+ const error = queryError ? (queryError as Error).message : null;
24
+
25
+ // Filter state
26
+ const [searchQuery, setSearchQuery] = useState("");
27
+ const [priorityFilter, setPriorityFilter] = useState<TaskPriority | "ALL">(
28
+ "ALL"
29
+ );
30
+ const [assigneeFilter, setAssigneeFilter] = useState<AssigneeRole | "ALL">(
31
+ "ALL"
32
+ );
33
+
34
+ const hasActiveFilters = useMemo(
35
+ () =>
36
+ searchQuery !== "" ||
37
+ priorityFilter !== "ALL" ||
38
+ assigneeFilter !== "ALL",
39
+ [searchQuery, priorityFilter, assigneeFilter]
40
+ );
41
+
42
+ const updateMutation = useMutation({
43
+ mutationFn: ({ taskId, status }: { taskId: number; status: TaskStatus }) =>
44
+ taskService.update(taskId, { status }),
45
+ onSuccess: () => {
46
+ queryClient.invalidateQueries({ queryKey: ["tasks"] });
47
+ },
48
+ });
49
+
50
+ const deleteMutation = useMutation({
51
+ mutationFn: (taskId: number) => taskService.delete(taskId),
52
+ onSuccess: () => {
53
+ queryClient.invalidateQueries({ queryKey: ["tasks"] });
54
+ },
55
+ });
56
+
57
+ const clearFilters = useCallback(() => {
58
+ setSearchQuery("");
59
+ setPriorityFilter("ALL");
60
+ setAssigneeFilter("ALL");
61
+ }, []);
62
+
63
+ const filteredTasks = useMemo(() => {
64
+ return tasks.filter((task) => {
65
+ if (searchQuery) {
66
+ const query = searchQuery.toLowerCase();
67
+ if (!task.title.toLowerCase().includes(query)) {
68
+ return false;
69
+ }
70
+ }
71
+ if (priorityFilter !== "ALL" && task.priority !== priorityFilter) {
72
+ return false;
73
+ }
74
+ if (assigneeFilter !== "ALL" && task.assigneeRole !== assigneeFilter) {
75
+ return false;
76
+ }
77
+ return true;
78
+ });
79
+ }, [tasks, searchQuery, priorityFilter, assigneeFilter]);
80
+
81
+ const getTasksByStatus = useCallback(
82
+ (status: TaskStatus) => {
83
+ return filteredTasks.filter((t) => t.status === status);
84
+ },
85
+ [filteredTasks]
86
+ );
87
+
88
+ const updateTaskStatus = useCallback(
89
+ async (taskId: number, status: TaskStatus) => {
90
+ await updateMutation.mutateAsync({ taskId, status });
91
+ },
92
+ [updateMutation]
93
+ );
94
+
95
+ const deleteTask = useCallback(
96
+ async (taskId: number) => {
97
+ await deleteMutation.mutateAsync(taskId);
98
+ },
99
+ [deleteMutation]
100
+ );
101
+
102
+ return {
103
+ tasks,
104
+ loading,
105
+ error,
106
+ searchQuery,
107
+ setSearchQuery,
108
+ priorityFilter,
109
+ setPriorityFilter,
110
+ assigneeFilter,
111
+ setAssigneeFilter,
112
+ hasActiveFilters,
113
+ clearFilters,
114
+ getTasksByStatus,
115
+ updateTaskStatus,
116
+ deleteTask,
117
+ refreshTasks,
118
+ };
119
+ }
@@ -0,0 +1,24 @@
1
+ import axios from "axios";
2
+
3
+ const apiClient = axios.create({
4
+ baseURL: "/api",
5
+ headers: {
6
+ "Content-Type": "application/json",
7
+ },
8
+ });
9
+
10
+ // Response interceptor for consistent error handling
11
+ apiClient.interceptors.response.use(
12
+ (response) => response,
13
+ (error) => {
14
+ // We can add global error handling here (e.g., logging, toast notifications)
15
+ const message =
16
+ error.response?.data?.message ||
17
+ error.message ||
18
+ "An unexpected error occurred";
19
+ console.error(`[API Error] ${message}`, error);
20
+ return Promise.reject(error);
21
+ }
22
+ );
23
+
24
+ export default apiClient;
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,27 @@
1
+ import apiClient from "@/lib/api-client";
2
+
3
+ export interface DocNode {
4
+ type: "file" | "directory";
5
+ name: string;
6
+ path: string;
7
+ children?: DocNode[];
8
+ }
9
+
10
+ export const docService = {
11
+ getTree: async () => {
12
+ const response = await apiClient.get<DocNode[]>("/docs/tree");
13
+ return response.data;
14
+ },
15
+
16
+ read: async (path: string) => {
17
+ const response = await apiClient.get<{ content: string }>(
18
+ `/docs/read?path=${encodeURIComponent(path)}`
19
+ );
20
+ return response.data;
21
+ },
22
+
23
+ write: async (path: string, content: string) => {
24
+ const response = await apiClient.post("/docs/write", { path, content });
25
+ return response.data;
26
+ },
27
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./doc.service";
2
+ export * from "./sprint.service";
3
+ export * from "./task.service";
@@ -0,0 +1,26 @@
1
+ import { type Sprint } from "@locusai/shared";
2
+ import axios from "axios";
3
+
4
+ const API_URL = "http://localhost:3080/api/sprints";
5
+
6
+ export const sprintService = {
7
+ getAll: async () => {
8
+ const res = await axios.get<Sprint[]>(API_URL);
9
+ return res.data;
10
+ },
11
+
12
+ create: async (data: Partial<Sprint>) => {
13
+ const res = await axios.post<{ id: number }>(API_URL, data);
14
+ return res.data;
15
+ },
16
+
17
+ update: async (id: number, data: Partial<Sprint>) => {
18
+ const res = await axios.patch(`${API_URL}/${id}`, data);
19
+ return res.data;
20
+ },
21
+
22
+ delete: async (id: number) => {
23
+ const res = await axios.delete(`${API_URL}/${id}`);
24
+ return res.data;
25
+ },
26
+ };