feat: implement project management features with routing, including project creation, deletion, renaming, and graph persistence

This commit is contained in:
2026-03-09 22:10:32 +01:00
parent c73aaca45a
commit e441d36cf4
12 changed files with 740 additions and 142 deletions

View File

@@ -1,9 +1,9 @@
/**
* Platform: main entry layout with sidebar (sidebar-07). Projects in sidebar;
* when a project is selected, the canvas is shown in the main area.
* Platform: main layout with sidebar and header. Renders child routes (projects list or canvas) via Outlet.
*/
import React, { useCallback, useState } from 'react'
import React from 'react'
import { Link, Outlet, useParams, useLocation } from 'react-router-dom'
import {
Breadcrumb,
BreadcrumbItem,
@@ -14,79 +14,19 @@ import {
} from '@/components/ui/breadcrumb'
import { Separator } from '@/components/ui/separator'
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { Button } from '@/components/ui/button'
import { Plus } from 'lucide-react'
import { PlatformProvider, usePlatform } from './platformContext'
import { AppSidebar } from './AppSidebar'
import { CanvasPage } from '../canvas/CanvasPage'
import type { Project } from './types'
import { NewProjectDialog } from './NewProjectDialog'
const STORAGE_KEY = 'zui_platform_projects'
function loadProjects(): Project[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) return []
return parsed.filter(
(p): p is Project =>
p &&
typeof p === 'object' &&
typeof (p as Project).id === 'string' &&
typeof (p as Project).name === 'string' &&
typeof (p as Project).iconId === 'string' &&
typeof (p as Project).createdAt === 'number'
)
} catch {
return []
}
}
function saveProjects(projects: Project[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects))
}
export function PlatformPage() {
const [projects, setProjects] = useState<Project[]>(loadProjects)
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(() => {
const list = loadProjects()
return list.length > 0 ? list[0].id : null
})
const persist = useCallback((next: Project[]) => {
setProjects(next)
saveProjects(next)
}, [])
const handleCreateProject = useCallback(
(project: Project) => {
persist([...projects, project])
setSelectedProjectId(project.id)
},
[projects, persist]
)
const handleDeleteProject = useCallback(
(id: string) => {
const next = projects.filter((p) => p.id !== id)
persist(next)
if (selectedProjectId === id) setSelectedProjectId(next[0]?.id ?? null)
},
[projects, persist, selectedProjectId]
)
const selectedProject = projects.find((p) => p.id === selectedProjectId)
function PlatformLayoutInner() {
const { projectId } = useParams<{ projectId: string }>()
const location = useLocation()
const { projects } = usePlatform()
const project = projectId ? projects.find((p) => p.id === projectId) : null
const isProjectsList = location.pathname === '/projects'
return (
<SidebarProvider>
<AppSidebar
projects={projects}
selectedProjectId={selectedProjectId}
onSelectProject={setSelectedProjectId}
onDeleteProject={handleDeleteProject}
onCreateProject={handleCreateProject}
/>
<AppSidebar />
<SidebarInset className="flex min-h-0 flex-1 flex-col">
<header className="flex h-12 shrink-0 items-center gap-2 border-b border-border/40 px-1 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-2">
@@ -94,40 +34,41 @@ export function PlatformPage() {
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
{selectedProject ? selectedProject.name : 'No project'}
</BreadcrumbPage>
</BreadcrumbItem>
{isProjectsList ? (
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">Projects</BreadcrumbPage>
</BreadcrumbItem>
) : (
<>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink asChild>
<Link to="/projects">Projects</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
{project ? project.name : 'Project'}
</BreadcrumbPage>
</BreadcrumbItem>
</>
)}
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex min-h-0 flex-1 flex-col">
{selectedProjectId && selectedProject ? (
<div className="flex-1 min-h-0 flex flex-col">
<CanvasPage projectId={selectedProjectId} />
</div>
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-4 rounded-xl border border-dashed bg-muted/30 p-8">
<p className="text-sm text-muted-foreground">No project selected. Create one to open the canvas.</p>
<NewProjectDialog
onCreate={handleCreateProject}
trigger={
<Button>
<Plus className="size-4" />
New project
</Button>
}
/>
</div>
)}
<Outlet />
</div>
</SidebarInset>
</SidebarProvider>
)
}
export function PlatformPage() {
return (
<PlatformProvider>
<PlatformLayoutInner />
</PlatformProvider>
)
}