Skip to content

Commit 0eda40e

Browse files
author
Marvin Zhang
committed
feat(api): add notes management for devlog entries with POST and PUT routes
refactor: change projectId type from string to number across relevant components
1 parent aa2562e commit 0eda40e

File tree

9 files changed

+188
-34
lines changed

9 files changed

+188
-34
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { DevlogService, ProjectService } from '@codervisor/devlog-core';
3+
import { RouteParams, ApiErrors } from '@/lib/api-utils';
4+
import { z } from 'zod';
5+
import type { NoteCategory } from '@codervisor/devlog-core';
6+
7+
// Mark this route as dynamic to prevent static generation
8+
export const dynamic = 'force-dynamic';
9+
10+
// Schema for adding notes
11+
const AddNoteBodySchema = z.object({
12+
note: z.string().min(1, 'Note is required'),
13+
category: z.string().optional().default('progress'),
14+
files: z.array(z.string()).optional(),
15+
codeChanges: z.string().optional(),
16+
});
17+
18+
// Schema for updating devlog with note
19+
const UpdateWithNoteBodySchema = z.object({
20+
note: z.string().min(1, 'Note is required'),
21+
category: z.string().optional().default('progress'),
22+
files: z.array(z.string()).optional(),
23+
codeChanges: z.string().optional(),
24+
// Optional update fields
25+
status: z
26+
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
27+
.optional(),
28+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
29+
assignee: z.string().nullable().optional(),
30+
businessContext: z.string().nullable().optional(),
31+
technicalContext: z.string().nullable().optional(),
32+
acceptanceCriteria: z.array(z.string()).optional(),
33+
});
34+
35+
// POST /api/projects/[id]/devlogs/[devlogId]/notes - Add note to devlog entry
36+
export async function POST(
37+
request: NextRequest,
38+
{ params }: { params: { id: string; devlogId: string } },
39+
) {
40+
try {
41+
// Parse and validate parameters
42+
const paramResult = RouteParams.parseProjectAndDevlogId(params);
43+
if (!paramResult.success) {
44+
return paramResult.response;
45+
}
46+
47+
const { projectId, devlogId } = paramResult.data;
48+
49+
// Validate request body
50+
const data = await request.json();
51+
const validationResult = AddNoteBodySchema.safeParse(data);
52+
if (!validationResult.success) {
53+
return ApiErrors.invalidRequest(validationResult.error.errors[0].message);
54+
}
55+
56+
const { note, category, files, codeChanges } = validationResult.data;
57+
58+
// Ensure project exists
59+
const projectService = ProjectService.getInstance();
60+
const project = await projectService.get(projectId);
61+
if (!project) {
62+
return ApiErrors.projectNotFound();
63+
}
64+
65+
// Create project-aware devlog service
66+
const devlogService = DevlogService.getInstance(projectId);
67+
68+
// Get the existing devlog entry
69+
const existingEntry = await devlogService.get(devlogId);
70+
if (!existingEntry) {
71+
return ApiErrors.devlogNotFound();
72+
}
73+
74+
// Create the new note
75+
const newNote = {
76+
id: `note-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
77+
content: note,
78+
category: (category || 'progress') as NoteCategory,
79+
timestamp: new Date().toISOString(),
80+
files: files || [],
81+
codeChanges: codeChanges || undefined,
82+
};
83+
84+
// Add the note to the entry's notes array and save
85+
const updatedEntry = {
86+
...existingEntry,
87+
notes: [...(existingEntry.notes || []), newNote],
88+
updatedAt: new Date().toISOString(),
89+
};
90+
91+
await devlogService.save(updatedEntry);
92+
93+
return NextResponse.json(updatedEntry);
94+
} catch (error) {
95+
console.error('Error adding devlog note:', error);
96+
return ApiErrors.internalError('Failed to add note to devlog entry');
97+
}
98+
}
99+
100+
// PUT /api/projects/[id]/devlogs/[devlogId]/notes - Update devlog and add note in one operation
101+
export async function PUT(
102+
request: NextRequest,
103+
{ params }: { params: { id: string; devlogId: string } },
104+
) {
105+
try {
106+
// Parse and validate parameters
107+
const paramResult = RouteParams.parseProjectAndDevlogId(params);
108+
if (!paramResult.success) {
109+
return paramResult.response;
110+
}
111+
112+
const { projectId, devlogId } = paramResult.data;
113+
114+
// Validate request body
115+
const data = await request.json();
116+
const validationResult = UpdateWithNoteBodySchema.safeParse(data);
117+
if (!validationResult.success) {
118+
return ApiErrors.invalidRequest(validationResult.error.errors[0].message);
119+
}
120+
121+
const { note, category, files, codeChanges, ...updateFields } = validationResult.data;
122+
123+
// Ensure project exists
124+
const projectService = ProjectService.getInstance();
125+
const project = await projectService.get(projectId);
126+
if (!project) {
127+
return ApiErrors.projectNotFound();
128+
}
129+
130+
// Create project-aware devlog service
131+
const devlogService = DevlogService.getInstance(projectId);
132+
133+
// Get the existing devlog entry
134+
const existingEntry = await devlogService.get(devlogId);
135+
if (!existingEntry) {
136+
return ApiErrors.devlogNotFound();
137+
}
138+
139+
// Create the new note
140+
const newNote = {
141+
id: `note-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
142+
content: note,
143+
category: (category || 'progress') as NoteCategory,
144+
timestamp: new Date().toISOString(),
145+
files: files || [],
146+
codeChanges: codeChanges || undefined,
147+
};
148+
149+
// Prepare the update data with both the note and any field updates
150+
const updatedEntry = {
151+
...existingEntry,
152+
...updateFields,
153+
notes: [...(existingEntry.notes || []), newNote],
154+
updatedAt: new Date().toISOString(),
155+
};
156+
157+
// Save the updated entry
158+
await devlogService.save(updatedEntry);
159+
160+
return NextResponse.json(updatedEntry);
161+
} catch (error) {
162+
console.error('Error updating devlog with note:', error);
163+
return ApiErrors.internalError('Failed to update devlog entry with note');
164+
}
165+
}

packages/web/app/components/layout/NavigationBreadcrumb.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ export function NavigationBreadcrumb() {
7777
setCurrentProject({
7878
projectId,
7979
project: targetProject,
80-
isDefault: projectId === 'default',
8180
});
8281

8382
toast.success(`Switched to project: ${targetProject.name}`);

packages/web/app/contexts/ProjectContext.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface ProjectMetadata {
1414
export interface ProjectContext {
1515
projectId: number; // Changed from string to number to match API
1616
project: ProjectMetadata;
17-
isDefault: boolean;
1817
}
1918

2019
interface ProjectContextValue {
@@ -60,7 +59,6 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
6059
setCurrentProject({
6160
projectId: firstProject.id,
6261
project: firstProject,
63-
isDefault: firstProject.id === 1, // Assume project with ID 1 is the default
6462
});
6563
}
6664
} catch (err) {
@@ -87,7 +85,6 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
8785
setCurrentProject({
8886
projectId: savedProject.id,
8987
project: savedProject,
90-
isDefault: savedProject.id === 1, // Assume project with ID 1 is the default
9188
});
9289
}
9390
}

packages/web/app/projects/[id]/ProjectDetailsPage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ export function ProjectDetailsPage({ projectId }: ProjectDetailsPageProps) {
2222

2323
// Set the current project based on the route parameter
2424
useEffect(() => {
25-
const project = projects.find(p => p.id === projectId);
26-
if (project && (!currentProject || currentProject.projectId !== projectId)) {
25+
const numericProjectId = parseInt(projectId, 10);
26+
const project = projects.find((p) => p.id === numericProjectId);
27+
if (project && (!currentProject || currentProject.projectId !== numericProjectId)) {
2728
setCurrentProject({
2829
projectId: project.id,
2930
project,
30-
isDefault: project.id === 'default',
3131
});
3232
}
3333
}, [projectId, projects, currentProject, setCurrentProject]);
@@ -37,7 +37,8 @@ export function ProjectDetailsPage({ projectId }: ProjectDetailsPageProps) {
3737
};
3838

3939
// Don't render until we have the correct project context
40-
if (!currentProject || currentProject.projectId !== projectId) {
40+
const numericProjectId = parseInt(projectId, 10);
41+
if (!currentProject || currentProject.projectId !== numericProjectId) {
4142
return (
4243
<PageLayout>
4344
<div>Loading project...</div>

packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
1111
import { PlusIcon } from 'lucide-react';
1212

1313
interface ProjectDevlogListPageProps {
14-
projectId: string;
14+
projectId: number;
1515
}
1616

1717
export function ProjectDevlogListPage({ projectId }: ProjectDevlogListPageProps) {
@@ -20,12 +20,11 @@ export function ProjectDevlogListPage({ projectId }: ProjectDevlogListPageProps)
2020

2121
// Set the current project based on the route parameter
2222
useEffect(() => {
23-
const project = projects.find(p => p.id === projectId);
23+
const project = projects.find((p) => p.id === projectId);
2424
if (project && (!currentProject || currentProject.projectId !== projectId)) {
2525
setCurrentProject({
2626
projectId: project.id,
2727
project,
28-
isDefault: project.id === 'default',
2928
});
3029
}
3130
}, [projectId, projects, currentProject, setCurrentProject]);

packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,14 @@ import { useProject } from '@/contexts/ProjectContext';
88
import { useRouter } from 'next/navigation';
99
import { Button } from '@/components/ui/button';
1010
import { Alert, AlertDescription } from '@/components/ui/alert';
11+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
1112
import {
12-
Popover,
13-
PopoverContent,
14-
PopoverTrigger,
15-
} from '@/components/ui/popover';
16-
import {
17-
ArrowLeftIcon,
18-
TrashIcon,
19-
SaveIcon,
20-
UndoIcon,
13+
ArrowLeftIcon,
14+
TrashIcon,
15+
SaveIcon,
16+
UndoIcon,
2117
AlertTriangleIcon,
22-
InfoIcon
18+
InfoIcon,
2319
} from 'lucide-react';
2420
import { toast } from 'sonner';
2521

@@ -34,12 +30,12 @@ export function ProjectDevlogDetailsPage({ projectId, devlogId }: ProjectDevlogD
3430

3531
// Set the current project based on the route parameter
3632
useEffect(() => {
37-
const project = projects.find(p => p.id === projectId);
38-
if (project && (!currentProject || currentProject.projectId !== projectId)) {
33+
const numericProjectId = parseInt(projectId, 10);
34+
const project = projects.find((p) => p.id === numericProjectId);
35+
if (project && (!currentProject || currentProject.projectId !== numericProjectId)) {
3936
setCurrentProject({
4037
projectId: project.id,
4138
project,
42-
isDefault: project.id === 'default',
4339
});
4440
}
4541
}, [projectId, projects, currentProject, setCurrentProject]);
@@ -111,7 +107,8 @@ export function ProjectDevlogDetailsPage({ projectId, devlogId }: ProjectDevlogD
111107
};
112108

113109
// Don't render until we have the correct project context
114-
if (!currentProject || currentProject.projectId !== projectId) {
110+
const numericProjectId = parseInt(projectId, 10);
111+
if (!currentProject || currentProject.projectId !== numericProjectId) {
115112
return (
116113
<PageLayout>
117114
<div>Loading project...</div>

packages/web/app/projects/[id]/devlogs/create/ProjectDevlogCreatePage.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button';
99
import { ArrowLeftIcon } from 'lucide-react';
1010

1111
interface ProjectDevlogCreatePageProps {
12-
projectId: string;
12+
projectId: number;
1313
}
1414

1515
export function ProjectDevlogCreatePage({ projectId }: ProjectDevlogCreatePageProps) {
@@ -19,12 +19,11 @@ export function ProjectDevlogCreatePage({ projectId }: ProjectDevlogCreatePagePr
1919

2020
// Set the current project based on the route parameter
2121
useEffect(() => {
22-
const project = projects.find(p => p.id === projectId);
22+
const project = projects.find((p) => p.id === projectId);
2323
if (project && (!currentProject || currentProject.projectId !== projectId)) {
2424
setCurrentProject({
2525
projectId: project.id,
2626
project,
27-
isDefault: project.id === 'default',
2827
});
2928
}
3029
}, [projectId, projects, currentProject, setCurrentProject]);
@@ -62,10 +61,7 @@ export function ProjectDevlogCreatePage({ projectId }: ProjectDevlogCreatePagePr
6261

6362
return (
6463
<PageLayout actions={actions}>
65-
<DevlogForm
66-
onSubmit={handleSubmit}
67-
onCancel={handleCancel}
68-
/>
64+
<DevlogForm onSubmit={handleSubmit} onCancel={handleCancel} />
6965
</PageLayout>
7066
);
7167
}

packages/web/app/projects/[id]/devlogs/create/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ interface ProjectCreateDevlogPageProps {
1010
}
1111

1212
export default function ProjectCreateDevlogPage({ params }: ProjectCreateDevlogPageProps) {
13-
return <ProjectDevlogCreatePage projectId={params.id} />;
13+
return <ProjectDevlogCreatePage projectId={parseInt(params.id, 10)} />;
1414
}

packages/web/app/projects/[id]/devlogs/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ interface ProjectDevlogsPageProps {
1010
}
1111

1212
export default function ProjectDevlogsPage({ params }: ProjectDevlogsPageProps) {
13-
return <ProjectDevlogListPage projectId={params.id} />;
13+
return <ProjectDevlogListPage projectId={parseInt(params.id, 10)} />;
1414
}

0 commit comments

Comments
 (0)