Skip to content

Commit 03e79f0

Browse files
author
Marvin Zhang
committed
feat: update project name validation to follow GitHub naming conventions and improve user guidance
1 parent 3ee496b commit 03e79f0

File tree

7 files changed

+106
-41
lines changed

7 files changed

+106
-41
lines changed

packages/core/src/utils/project-name.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ export function generateSlugFromName(name: string): string {
4444
}
4545

4646
/**
47-
* Validate a project display name (more permissive than slugs):
48-
* - Can contain letters, numbers, spaces, hyphens, underscores, dots
49-
* - Cannot start or end with whitespace
47+
* Validate a project display name following GitHub repository naming rules:
48+
* - Can only contain ASCII letters, digits, and the characters -, ., and _
5049
* - Must not be empty
5150
* - Length between 1-100 characters
5251
*/
@@ -55,13 +54,8 @@ export function validateProjectDisplayName(name: string): boolean {
5554
return false;
5655
}
5756

58-
// Check for leading/trailing whitespace
59-
if (name.trim() !== name) {
60-
return false;
61-
}
62-
63-
// Must contain only valid display characters
64-
if (!/^[a-zA-Z0-9\s._-]+$/.test(name)) {
57+
// Must contain only ASCII letters, digits, hyphens, dots, and underscores
58+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
6559
return false;
6660
}
6761

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

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,34 @@ import { Skeleton } from '@/components/ui/skeleton';
2020
import { toast } from 'sonner';
2121

2222
export function NavigationBreadcrumb() {
23-
const router = useRouter();
2423
const pathname = usePathname();
24+
const router = useRouter();
25+
26+
// Parse project name and devlog ID from URL instead of using context hooks
27+
// since this component is rendered at app level, outside of the provider hierarchy
28+
const pathSegments = pathname.split('/').filter(Boolean);
29+
let projectName: string | null = null;
30+
let devlogId: number | null = null;
31+
32+
// Check if we're in a project path: /projects/[name] or /projects/[name]/devlogs/[id]
33+
if (pathSegments[0] === 'projects' && pathSegments[1]) {
34+
projectName = pathSegments[1];
35+
36+
// Check if we're in a devlog path
37+
if (pathSegments[2] === 'devlogs' && pathSegments[3]) {
38+
const parsedDevlogId = parseInt(pathSegments[3], 10);
39+
if (!isNaN(parsedDevlogId)) {
40+
devlogId = parsedDevlogId;
41+
}
42+
}
43+
}
44+
2545
const { currentProjectContext, currentProjectName, projectsContext, fetchProjects } =
2646
useProjectStore();
27-
const { currentDevlogContext, currentDevlogId } = useDevlogStore();
47+
const { currentDevlogContext } = useDevlogStore();
2848

29-
// Don't show breadcrumb on the home or project list page
30-
if (['/', '/projects'].includes(pathname)) {
49+
// If we are not in a project context, do not render the breadcrumb
50+
if (!projectName) {
3151
return null;
3252
}
3353

@@ -50,12 +70,15 @@ export function NavigationBreadcrumb() {
5070
}
5171
};
5272

53-
const handleDropdownOpenChange = (open: boolean) => {
54-
if (open) {
55-
// Load projects when dropdown is opened
56-
fetchProjects();
57-
}
58-
};
73+
const dropdownSkeletons = Array.from({ length: 3 }).map((_, index) => (
74+
<DropdownMenuItem key={index} disabled className="flex items-center gap-3 p-3">
75+
<Skeleton className="w-6 h-6 rounded-full flex-shrink-0" />
76+
<div className="flex-1 min-w-0 space-y-1">
77+
<Skeleton className="h-4 w-full" />
78+
<Skeleton className="h-3 w-8" />
79+
</div>
80+
</DropdownMenuItem>
81+
));
5982

6083
const renderProjectDropdown = () => {
6184
// Show skeleton if current project is loading
@@ -68,29 +91,24 @@ export function NavigationBreadcrumb() {
6891
}
6992

7093
return (
71-
<DropdownMenu onOpenChange={handleDropdownOpenChange}>
94+
<DropdownMenu
95+
onOpenChange={async (open) => {
96+
if (open) await fetchProjects();
97+
}}
98+
>
7299
<DropdownMenuTrigger asChild>
73100
<div className="flex items-center gap-2 cursor-pointer rounded">
74101
<Package size={14} />
75-
<span>{currentProjectContext.data?.name}</span>
102+
<span>{projectName}</span>
76103
<ChevronsUpDown size={14} className="text-muted-foreground" />
77104
</div>
78105
</DropdownMenuTrigger>
79106
<DropdownMenuContent align="start" className="w-64">
80107
{/* Show skeleton items if projects list is loading */}
81108
{projectsContext.loading
82-
? Array.from({ length: 3 }).map((_, index) => (
83-
<DropdownMenuItem key={index} disabled className="flex items-center gap-3 p-3">
84-
<Skeleton className="w-6 h-6 rounded-full flex-shrink-0" />
85-
<div className="flex-1 min-w-0 space-y-1">
86-
<Skeleton className="h-4 w-full" />
87-
<Skeleton className="h-3 w-8" />
88-
</div>
89-
</DropdownMenuItem>
90-
))
109+
? dropdownSkeletons
91110
: projectsContext.data?.map((project) => {
92-
const isCurrentProject = currentProjectName === project.name;
93-
111+
const isCurrentProject = projectName === project.name;
94112
return (
95113
<DropdownMenuItem
96114
key={project.id}
@@ -100,11 +118,11 @@ export function NavigationBreadcrumb() {
100118
>
101119
<div className="flex-1 min-w-0">
102120
<div className="text-sm font-medium truncate">{project.name}</div>
103-
<div className="text-xs text-muted-foreground truncate">{project.description}</div>
121+
<div className="text-xs text-muted-foreground truncate">
122+
{project.description}
123+
</div>
104124
</div>
105-
{isCurrentProject && (
106-
<Check size={14} className="text-primary flex-shrink-0" />
107-
)}
125+
{isCurrentProject && <Check size={14} className="text-primary flex-shrink-0" />}
108126
</DropdownMenuItem>
109127
);
110128
})}
@@ -134,8 +152,8 @@ export function NavigationBreadcrumb() {
134152
return (
135153
<Breadcrumb className="navigation-breadcrumb">
136154
<BreadcrumbList>
137-
{currentProjectName && <BreadcrumbItem>{renderProjectDropdown()}</BreadcrumbItem>}
138-
{currentDevlogId && (
155+
<BreadcrumbItem>{renderProjectDropdown()}</BreadcrumbItem>
156+
{devlogId && (
139157
<>
140158
<BreadcrumbSeparator />
141159
<BreadcrumbItem>{renderDevlogDropdown()}</BreadcrumbItem>

packages/web/app/projects/ProjectListPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,14 @@ export function ProjectListPage() {
201201
<Label htmlFor="name">Project Name</Label>
202202
<Input
203203
id="name"
204-
placeholder="e.g., My Development Project"
204+
placeholder="e.g., My-Dev-Project_2025"
205205
value={formData.name}
206206
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
207207
required
208208
/>
209+
<p className="text-sm text-muted-foreground mt-1">
210+
Can only contain ASCII letters, digits, and the characters -, ., and _
211+
</p>
209212
</div>
210213
<div>
211214
<Label htmlFor="description">Description (Optional)</Label>

packages/web/app/projects/[name]/ProjectProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function ProjectProvider({
2727

2828
export function useProject(): ProjectContextValue {
2929
const context = useContext(ProjectContext);
30+
console.debug('useProject', 'context', context);
3031
if (!context) {
3132
throw new Error('useProject must be used within a ProjectProvider');
3233
}

packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function DevlogProvider({
2727

2828
export function useDevlog(): DevlogContextValue {
2929
const context = useContext(DevlogContext);
30+
console.debug('useDevlog', 'context', context);
3031
if (!context) {
3132
throw new Error('useDevlog must be used within a DevlogProvider');
3233
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
import { DevlogService, ProjectService } from '@codervisor/devlog-core/server';
3+
import { notFound } from 'next/navigation';
4+
import { DevlogProvider } from '../DevlogProvider';
5+
6+
interface DevlogLayoutProps {
7+
children: React.ReactNode;
8+
params: {
9+
name: string; // The project name from the URL
10+
id: string; // The devlog ID from the URL
11+
};
12+
}
13+
14+
/**
15+
* Server layout that resolves devlog data and provides it to all child pages
16+
*/
17+
export default async function DevlogLayout({ children, params }: DevlogLayoutProps) {
18+
const projectName = params.name;
19+
const devlogId = parseInt(params.id, 10);
20+
21+
// Validate devlog ID
22+
if (isNaN(devlogId) || devlogId <= 0) {
23+
notFound();
24+
}
25+
26+
try {
27+
// Get project to ensure it exists and get project ID
28+
const projectService = ProjectService.getInstance();
29+
const project = await projectService.getByName(projectName);
30+
31+
if (!project) {
32+
notFound();
33+
}
34+
35+
// Get devlog service and fetch the devlog
36+
const devlogService = DevlogService.getInstance(project.id);
37+
const devlog = await devlogService.get(devlogId);
38+
39+
if (!devlog) {
40+
notFound();
41+
}
42+
43+
return <DevlogProvider devlog={devlog}>{children}</DevlogProvider>;
44+
} catch (error) {
45+
console.error('Error resolving devlog:', error);
46+
notFound();
47+
}
48+
}

packages/web/app/schemas/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const CreateProjectBodySchema = z.object({
2727
name: z
2828
.string()
2929
.min(1, 'Project name is required')
30-
.refine(validateProjectDisplayName, 'Project name can contain letters, numbers, spaces, hyphens, underscores, and dots. Cannot start or end with whitespace.'),
30+
.refine(validateProjectDisplayName, 'The repository name can only contain ASCII letters, digits, and the characters -, ., and _.'),
3131
description: z.string().optional(),
3232
repositoryUrl: z.string().optional(),
3333
settings: z

0 commit comments

Comments
 (0)