|
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'; |
23 | 7 |
|
24 | 8 | interface ProjectResolverProps { |
25 | 9 | identifier: string; |
26 | 10 | identifierType: 'id' | 'name'; |
27 | | - children: (projectName: string, project?: Project) => React.ReactNode; |
28 | | - onNotFound?: () => void; |
| 11 | + children: (projectName: string, project: Project) => React.ReactNode; |
29 | 12 | } |
30 | 13 |
|
31 | 14 | /** |
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 |
33 | 16 | * Handles URL redirects when using name-based routing |
34 | 17 | */ |
35 | | -export function ProjectResolver({ |
| 18 | +export async function ProjectResolver({ |
36 | 19 | identifier, |
37 | 20 | identifierType, |
38 | 21 | children, |
39 | | - onNotFound, |
40 | 22 | }: 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}`); |
80 | 37 | } |
81 | | - } finally { |
82 | | - setLoading(false); |
83 | 38 | } |
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 | + } |
88 | 46 |
|
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 | + } |
99 | 50 |
|
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 />; |
117 | 55 | } |
118 | | - |
119 | | - return <>{children(project.name, project)}</>; |
120 | 56 | } |
0 commit comments