Skip to content

Commit 1585da5

Browse files
committed
feat: implement ProjectNotFound component and refactor project resolution logic to enhance error handling and user experience
1 parent 1e6ab69 commit 1585da5

File tree

13 files changed

+184
-187
lines changed

13 files changed

+184
-187
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Link from 'next/link';
2+
3+
export function ProjectNotFound() {
4+
return (
5+
<div className="flex items-center justify-center min-h-screen">
6+
<div className="text-center">
7+
<h1 className="text-2xl font-bold text-gray-900 mb-2">Project Not Found</h1>
8+
<p className="text-gray-600 mb-4">
9+
The requested project could not be found.
10+
</p>
11+
<Link
12+
href="/projects"
13+
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 inline-block"
14+
>
15+
Back to Projects
16+
</Link>
17+
</div>
18+
</div>
19+
);
20+
}
Lines changed: 37 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,56 @@
1-
'use client';
2-
3-
import React, { useEffect, useState } from 'react';
4-
import { useRouter } from 'next/navigation';
5-
import { apiClient, ApiError, debounce } from '@/lib';
6-
7-
// Local type definition to avoid importing from core package in client component
8-
interface Project {
9-
id: number;
10-
name: string;
11-
description?: string;
12-
createdAt: string;
13-
updatedAt: string;
14-
}
15-
16-
// Local utility function to avoid importing from core package
17-
function generateSlugFromName(name: string): string {
18-
return name
19-
.toLowerCase()
20-
.replace(/[^a-z0-9]+/g, '-')
21-
.replace(/^-+|-+$/g, '');
22-
}
1+
import React from 'react';
2+
import { redirect } from 'next/navigation';
3+
import { ProjectService } from '@codervisor/devlog-core/server';
4+
import { generateSlugFromName } from '@codervisor/devlog-core';
5+
import type { Project } from '@codervisor/devlog-core';
6+
import { ProjectNotFound } from './ProjectNotFound';
237

248
interface ProjectResolverProps {
259
identifier: string;
2610
identifierType: 'id' | 'name';
27-
children: (projectName: string, project?: Project) => React.ReactNode;
28-
onNotFound?: () => void;
11+
children: (projectName: string, project: Project) => React.ReactNode;
2912
}
3013

3114
/**
32-
* Resolves a project identifier (ID or name) to a project ID and project data
15+
* Server component that resolves a project identifier to project data
3316
* Handles URL redirects when using name-based routing
3417
*/
35-
export function ProjectResolver({
18+
export async function ProjectResolver({
3619
identifier,
3720
identifierType,
3821
children,
39-
onNotFound,
4022
}: ProjectResolverProps) {
41-
const [project, setProject] = useState<Project | null>(null);
42-
const [loading, setLoading] = useState(true);
43-
const [error, setError] = useState<string | null>(null);
44-
const router = useRouter();
45-
46-
useEffect(() => {
47-
const resolveProject = debounce(async () => {
48-
try {
49-
setLoading(true);
50-
setError(null);
51-
52-
// For numeric IDs, we can direct fetch, but for names we need to resolve
53-
const projectData = await apiClient.get<Project>(`/api/projects/${identifier}`);
54-
setProject(projectData);
55-
56-
// If we're using name-based routing but the URL doesn't match the canonical slug,
57-
// redirect to the canonical URL
58-
if (identifierType === 'name') {
59-
const canonicalSlug = generateSlugFromName(projectData.name);
60-
if (identifier !== canonicalSlug) {
61-
// Redirect to canonical URL
62-
const currentPath = window.location.pathname;
63-
const newPath = currentPath.replace(
64-
`/projects/${identifier}`,
65-
`/projects/${canonicalSlug}`,
66-
);
67-
router.replace(newPath);
68-
return;
69-
}
70-
}
71-
} catch (error) {
72-
console.error('Error resolving project:', error);
73-
74-
// Handle specific API errors
75-
if (error instanceof ApiError && error.status === 404) {
76-
onNotFound?.();
77-
setError('Project not found');
78-
} else {
79-
setError('Failed to load project');
23+
try {
24+
const projectService = ProjectService.getInstance();
25+
26+
let project: Project | null = null;
27+
28+
if (identifierType === 'name') {
29+
project = await projectService.getByName(identifier);
30+
31+
// If project exists but identifier doesn't match canonical slug, redirect
32+
if (project) {
33+
const canonicalSlug = generateSlugFromName(project.name);
34+
if (identifier !== canonicalSlug) {
35+
// Redirect to canonical URL
36+
redirect(`/projects/${canonicalSlug}`);
8037
}
81-
} finally {
82-
setLoading(false);
8338
}
84-
});
85-
86-
resolveProject();
87-
}, [identifier, identifierType, router, onNotFound]);
39+
} else {
40+
// For ID-based routing (fallback/legacy support)
41+
const projectId = parseInt(identifier, 10);
42+
if (!isNaN(projectId)) {
43+
project = await projectService.get(projectId);
44+
}
45+
}
8846

89-
if (loading) {
90-
return (
91-
<div className="flex items-center justify-center min-h-screen">
92-
<div className="text-center">
93-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
94-
<p className="text-sm text-gray-600">Loading project...</p>
95-
</div>
96-
</div>
97-
);
98-
}
47+
if (!project) {
48+
return <ProjectNotFound />;
49+
}
9950

100-
if (error || !project) {
101-
return (
102-
<div className="flex items-center justify-center min-h-screen">
103-
<div className="text-center">
104-
<h1 className="text-2xl font-bold text-gray-900 mb-2">Project Not Found</h1>
105-
<p className="text-gray-600 mb-4">
106-
{error || 'The requested project could not be found.'}
107-
</p>
108-
<button
109-
onClick={() => router.push('/projects')}
110-
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
111-
>
112-
Back to Projects
113-
</button>
114-
</div>
115-
</div>
116-
);
51+
return <>{children(project.name, project)}</>;
52+
} catch (error) {
53+
console.error('Error resolving project:', error);
54+
return <ProjectNotFound />;
11755
}
118-
119-
return <>{children(project.name, project)}</>;
12056
}

packages/web/app/components/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export * from './features/dashboard';
1818
export * from './features/devlogs';
1919

2020
// Project Components
21-
export { ProjectResolver } from './ProjectResolver';
21+
// Note: ProjectResolver is not exported as it's only used server-side in layout.tsx
22+
export { ProjectNotFound } from './ProjectNotFound';
2223
export * from './project';

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import { useDevlogStore, useProjectStore } from '@/stores';
66
import { useDevlogEvents } from '@/hooks/use-realtime';
77
import { DevlogEntry, Project } from '@codervisor/devlog-core';
88
import { useRouter } from 'next/navigation';
9+
import { useProjectName } from './ProjectProvider';
910

10-
interface ProjectDetailsPageProps {
11-
projectName: string;
12-
}
13-
14-
export function ProjectDetailsPage({ projectName }: ProjectDetailsPageProps) {
11+
export function ProjectDetailsPage() {
12+
const projectName = useProjectName();
1513
const router = useRouter();
1614

1715
const { currentProjectName, setCurrentProjectName } = useProjectStore();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import React, { createContext, useContext } from 'react';
4+
import type { Project } from '@codervisor/devlog-core';
5+
6+
interface ProjectContextValue {
7+
project: Project;
8+
projectName: string;
9+
}
10+
11+
const ProjectContext = createContext<ProjectContextValue | null>(null);
12+
13+
export function ProjectProvider({
14+
children,
15+
project
16+
}: {
17+
children: React.ReactNode;
18+
project: Project;
19+
}) {
20+
const value: ProjectContextValue = {
21+
project,
22+
projectName: project.name,
23+
};
24+
25+
return (
26+
<ProjectContext.Provider value={value}>
27+
{children}
28+
</ProjectContext.Provider>
29+
);
30+
}
31+
32+
export function useProject(): ProjectContextValue {
33+
const context = useContext(ProjectContext);
34+
if (!context) {
35+
throw new Error('useProject must be used within a ProjectProvider');
36+
}
37+
return context;
38+
}
39+
40+
export function useProjectName(): string {
41+
const { projectName } = useProject();
42+
return projectName;
43+
}

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import { useDevlogStore, useProjectStore } from '@/stores';
66
import { useDevlogEvents } from '@/hooks/use-realtime';
77
import { DevlogEntry, DevlogId } from '@codervisor/devlog-core';
88
import { useRouter } from 'next/navigation';
9+
import { useProjectName } from '../ProjectProvider';
910

10-
interface ProjectDevlogListPageProps {
11-
projectName: string;
12-
}
13-
14-
export function ProjectDevlogListPage({ projectName }: ProjectDevlogListPageProps) {
11+
export function ProjectDevlogListPage() {
12+
const projectName = useProjectName();
1513
const router = useRouter();
1614

1715
const { currentProjectName, setCurrentProjectName } = useProjectStore();

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { ArrowLeftIcon, SaveIcon, TrashIcon, UndoIcon } from 'lucide-react';
99
import { toast } from 'sonner';
1010
import { DevlogEntry } from '@codervisor/devlog-core';
1111
import { RealtimeEventType } from '@/lib/realtime';
12+
import { useProjectName } from '../../ProjectProvider';
1213

1314
interface ProjectDevlogDetailsPageProps {
14-
projectName: string;
1515
devlogId: number;
1616
}
1717

18-
export function ProjectDevlogDetailsPage({ projectName, devlogId }: ProjectDevlogDetailsPageProps) {
18+
export function ProjectDevlogDetailsPage({ devlogId }: ProjectDevlogDetailsPageProps) {
19+
const projectName = useProjectName();
1920
const router = useRouter();
2021

2122
const { setCurrentProjectName } = useProjectStore();
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
'use client';
2-
31
import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage';
4-
import { ProjectResolver } from '@/components/ProjectResolver';
52
import { RouteParamParsers } from '@/lib';
63

74
interface ProjectDevlogPageProps {
@@ -12,16 +9,7 @@ interface ProjectDevlogPageProps {
129
}
1310

1411
export default function ProjectDevlogPage({ params }: ProjectDevlogPageProps) {
15-
const { projectIdentifier, identifierType, devlogId } = RouteParamParsers.parseDevlogParams(params);
12+
const { devlogId } = RouteParamParsers.parseDevlogParams(params);
1613

17-
return (
18-
<ProjectResolver
19-
identifier={projectIdentifier}
20-
identifierType={identifierType}
21-
>
22-
{(projectName) => (
23-
<ProjectDevlogDetailsPage projectName={projectName} devlogId={devlogId} />
24-
)}
25-
</ProjectResolver>
26-
);
14+
return <ProjectDevlogDetailsPage devlogId={devlogId} />;
2715
}
Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
1-
'use client';
2-
31
import { ProjectDevlogListPage } from './ProjectDevlogListPage';
4-
import { ProjectResolver } from '@/components/ProjectResolver';
5-
import { RouteParamParsers } from '@/lib';
6-
7-
interface ProjectDevlogsPageProps {
8-
params: {
9-
id: string;
10-
};
11-
}
122

13-
export default function ProjectDevlogsPage({ params }: ProjectDevlogsPageProps) {
14-
const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params);
15-
16-
return (
17-
<ProjectResolver
18-
identifier={projectIdentifier}
19-
identifierType={identifierType}
20-
>
21-
{(projectName) => <ProjectDevlogListPage projectName={projectName} />}
22-
</ProjectResolver>
23-
);
3+
export default function ProjectDevlogsPage() {
4+
return <ProjectDevlogListPage />;
245
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { ProjectService } from '@codervisor/devlog-core/server';
3+
import { generateSlugFromName } from '@codervisor/devlog-core';
4+
import type { Project } from '@codervisor/devlog-core';
5+
import { RouteParamParsers } from '@/lib';
6+
import { ProjectNotFound } from '@/components/ProjectNotFound';
7+
import { redirect } from 'next/navigation';
8+
import { ProjectProvider } from './ProjectProvider';
9+
10+
interface ProjectLayoutProps {
11+
children: React.ReactNode;
12+
params: {
13+
id: string;
14+
};
15+
}
16+
17+
/**
18+
* Server layout that resolves project data and provides it to all child pages
19+
*/
20+
export default async function ProjectLayout({ children, params }: ProjectLayoutProps) {
21+
const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params);
22+
23+
try {
24+
const projectService = ProjectService.getInstance();
25+
26+
let project: Project | null = null;
27+
28+
if (identifierType === 'name') {
29+
project = await projectService.getByName(projectIdentifier);
30+
31+
// If project exists but identifier doesn't match canonical slug, redirect
32+
if (project) {
33+
const canonicalSlug = generateSlugFromName(project.name);
34+
if (projectIdentifier !== canonicalSlug) {
35+
// Redirect to canonical URL
36+
const currentPath = `/projects/${projectIdentifier}`;
37+
const newPath = `/projects/${canonicalSlug}`;
38+
redirect(newPath);
39+
}
40+
}
41+
} else {
42+
// For ID-based routing (fallback/legacy support)
43+
const projectId = parseInt(projectIdentifier, 10);
44+
if (!isNaN(projectId)) {
45+
project = await projectService.get(projectId);
46+
}
47+
}
48+
49+
if (!project) {
50+
return <ProjectNotFound />;
51+
}
52+
53+
return (
54+
<ProjectProvider project={project}>
55+
{children}
56+
</ProjectProvider>
57+
);
58+
} catch (error) {
59+
console.error('Error resolving project:', error);
60+
return <ProjectNotFound />;
61+
}
62+
}

0 commit comments

Comments
 (0)