@@ -21,11 +21,21 @@ import {
2121 DropdownMenuItem ,
2222 DropdownMenuTrigger ,
2323} from '@/features/shared/components/ui/dropdown-menu' ;
24+ import { Input } from '@/features/shared/components/ui/input' ;
2425import type { FSInterface } from '@jaculus/project/fs' ;
26+ import path from 'path' ;
2527import { createFileRoute , Link , useNavigate } from '@tanstack/react-router' ;
2628import { useLiveQuery } from 'dexie-react-hooks' ;
27- import { Blocks , Code , Download , MoreVertical , Trash } from 'lucide-react' ;
29+ import {
30+ Blocks ,
31+ Code ,
32+ Download ,
33+ MoreVertical ,
34+ Pencil ,
35+ Trash ,
36+ } from 'lucide-react' ;
2837import { useState } from 'react' ;
38+ import { loadPackageJson , savePackageJson } from '@jaculus/project/package' ;
2939
3040export const Route = createFileRoute ( '/project/' ) ( {
3141 component : EditorList ,
@@ -36,6 +46,13 @@ function EditorList() {
3646 Route . useRouteContext ( ) ;
3747 const [ deleteDialogOpen , setDeleteDialogOpen ] = useState ( false ) ;
3848 const [ projectToDelete , setProjectToDelete ] = useState < string | null > ( null ) ;
49+ const [ renameDialogOpen , setRenameDialogOpen ] = useState ( false ) ;
50+ const [ projectToRename , setProjectToRename ] = useState < {
51+ id : string ;
52+ name : string ;
53+ } | null > ( null ) ;
54+ const [ renameValue , setRenameValue ] = useState ( '' ) ;
55+ const [ renameError , setRenameError ] = useState < string | null > ( null ) ;
3956
4057 const projects = useLiveQuery (
4158 ( ) => runtimeService . listProjects ( ) ,
@@ -51,6 +68,66 @@ function EditorList() {
5168 setProjectToDelete ( null ) ;
5269 }
5370
71+ const projectNamePattern = / ^ [ a - z A - Z 0 - 9 - _ ] + $ / ;
72+ const projectNamePatternJson = / ^ [ a - z 0 - 9 - _ ] + $ / ;
73+
74+ function startRename ( projectId : string , projectName : string ) {
75+ setProjectToRename ( { id : projectId , name : projectName } ) ;
76+ setRenameValue ( projectName ) ;
77+ setRenameError ( null ) ;
78+ setRenameDialogOpen ( true ) ;
79+ }
80+
81+ async function confirmRename ( ) {
82+ if ( ! projectToRename ) return ;
83+ const nextName = renameValue . trim ( ) ;
84+
85+ if ( ! nextName ) {
86+ setRenameError ( m . project_rename_required ( ) ) ;
87+ return ;
88+ }
89+
90+ if ( ! projectNamePattern . test ( nextName ) ) {
91+ setRenameError ( m . project_rename_invalid ( ) ) ;
92+ return ;
93+ }
94+
95+ setRenameError ( null ) ;
96+
97+ try {
98+ const projectId = projectToRename . id ;
99+
100+ if ( nextName === projectToRename . name ) {
101+ setRenameDialogOpen ( false ) ;
102+ setProjectToRename ( null ) ;
103+ return ;
104+ }
105+ const nextNamePackage = nextName . replace ( / [ ^ a - z A - Z 0 - 9 - _ ] / g, '-' ) ;
106+
107+ if ( projectNamePatternJson . test ( nextNamePackage ) ) {
108+ await projectFsService . withMount (
109+ projectId ,
110+ async ( { fs, projectPath } ) => {
111+ const packageJsonPath = path . join ( projectPath , 'package.json' ) ;
112+ const pkgJson = await loadPackageJson ( fs , packageJsonPath ) ;
113+ await savePackageJson ( fs , packageJsonPath , {
114+ ...pkgJson ,
115+ name : nextName ,
116+ } ) ;
117+ }
118+ ) ;
119+ }
120+
121+ await runtimeService . renameProject ( projectId , nextName ) ;
122+
123+ setRenameDialogOpen ( false ) ;
124+ setProjectToRename ( null ) ;
125+ } catch ( error ) {
126+ console . error ( 'Failed to rename project:' , error ) ;
127+ setRenameError ( m . project_rename_failed ( ) ) ;
128+ }
129+ }
130+
54131 return (
55132 < div >
56133 < h1 className = "text-2xl font-bold mb-4 text-center" >
@@ -119,6 +196,15 @@ function EditorList() {
119196 < Trash className = "w-4 h-4 mr-2" />
120197 { m . project_delete ( ) }
121198 </ DropdownMenuItem >
199+ < DropdownMenuItem
200+ onClick = { e => {
201+ e . stopPropagation ( ) ;
202+ startRename ( project . id , project . name ) ;
203+ } }
204+ >
205+ < Pencil className = "w-4 h-4 mr-2" />
206+ { m . project_rename ( ) }
207+ </ DropdownMenuItem >
122208 < DropdownMenuItem
123209 onClick = { async e => {
124210 e . stopPropagation ( ) ;
@@ -180,6 +266,47 @@ function EditorList() {
180266 </ DialogFooter >
181267 </ DialogContent >
182268 </ Dialog >
269+
270+ < Dialog
271+ open = { renameDialogOpen }
272+ onOpenChange = { open => {
273+ setRenameDialogOpen ( open ) ;
274+ if ( ! open ) {
275+ setRenameError ( null ) ;
276+ setProjectToRename ( null ) ;
277+ }
278+ } }
279+ >
280+ < DialogContent >
281+ < DialogHeader >
282+ < DialogTitle > { m . project_rename_title ( ) } </ DialogTitle >
283+ < DialogDescription >
284+ { m . project_rename_description ( ) }
285+ </ DialogDescription >
286+ </ DialogHeader >
287+ < div className = "space-y-2" >
288+ < Input
289+ value = { renameValue }
290+ onChange = { e => setRenameValue ( e . target . value ) }
291+ placeholder = { m . project_rename_placeholder ( ) }
292+ />
293+ { renameError && (
294+ < p className = "text-sm text-destructive" > { renameError } </ p >
295+ ) }
296+ </ div >
297+ < DialogFooter >
298+ < Button
299+ variant = "outline"
300+ onClick = { ( ) => setRenameDialogOpen ( false ) }
301+ >
302+ { m . project_rename_cancel ( ) }
303+ </ Button >
304+ < Button onClick = { confirmRename } >
305+ { m . project_rename_confirm ( ) }
306+ </ Button >
307+ </ DialogFooter >
308+ </ DialogContent >
309+ </ Dialog >
183310 </ >
184311 ) : (
185312 < p className = "text-center text-muted-foreground" > { m . project_empty ( ) } </ p >
0 commit comments