feat: enhance platform context with recent project tracking and access recording
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
* Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo), View (Fit View, Theme).
|
* Menubar for the canvas page: Project (Import/Export), Edit (Undo/Redo), View (Fit View, Theme).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Menubar,
|
Menubar,
|
||||||
MenubarContent,
|
MenubarContent,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@/components/ui/menubar'
|
} from '@/components/ui/menubar'
|
||||||
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
import { Kbd, KbdGroup } from '@/components/ui/kbd'
|
||||||
import { useTheme } from '@/lib/themeContext'
|
import { useTheme } from '@/lib/themeContext'
|
||||||
|
import { usePlatform } from '@/app/platform/platformContext'
|
||||||
import { ArrowLeft, Download, FolderOpen, Moon, Redo2, Sun, Undo2 } from 'lucide-react'
|
import { ArrowLeft, Download, FolderOpen, Moon, Redo2, Sun, Undo2 } from 'lucide-react'
|
||||||
|
|
||||||
export type CanvasMenubarProps = {
|
export type CanvasMenubarProps = {
|
||||||
@@ -48,6 +49,12 @@ export function CanvasMenubar({
|
|||||||
onFitView,
|
onFitView,
|
||||||
}: CanvasMenubarProps) {
|
}: CanvasMenubarProps) {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
const { projectId } = useParams<{ projectId: string }>()
|
||||||
|
const { projects } = usePlatform()
|
||||||
|
const projectName = useMemo(
|
||||||
|
() => (projectId ? projects.find((p) => p.id === projectId)?.name ?? null : null),
|
||||||
|
[projectId, projects]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (ev: KeyboardEvent) => {
|
const onKeyDown = (ev: KeyboardEvent) => {
|
||||||
@@ -72,11 +79,12 @@ export function CanvasMenubar({
|
|||||||
}, [undo, redo, canUndo, canRedo])
|
}, [undo, redo, canUndo, canRedo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menubar className="shrink-0 rounded-none border-x-0 border-t-0 border-b-0">
|
<div className="relative flex h-9 w-full shrink-0 items-center border-b border-border/40 bg-background">
|
||||||
|
<Menubar className="flex-1 shrink-0 rounded-none border-0 border-b-0 bg-transparent p-0 shadow-none">
|
||||||
<Link
|
<Link
|
||||||
to="/projects"
|
to="/projects"
|
||||||
aria-label="Back to projects"
|
aria-label="Back to projects"
|
||||||
className="flex shrink-0 items-center rounded-sm px-2 py-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground"
|
className="flex shrink-0 items-center rounded-sm px-2 py-1 ml-1 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="size-4" />
|
<ArrowLeft className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -154,5 +162,11 @@ export function CanvasMenubar({
|
|||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
</MenubarMenu>
|
</MenubarMenu>
|
||||||
</Menubar>
|
</Menubar>
|
||||||
|
{projectName && (
|
||||||
|
<span className="pointer-events-none absolute left-1/2 -translate-x-1/2 truncate max-w-[40%] text-sm font-medium text-foreground">
|
||||||
|
{projectName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Platform sidebar: projects list (ordered, draggable), create, rename, delete.
|
* Platform sidebar: All projects, Recently used, New project.
|
||||||
* "Projects" link to list; empty state when no projects.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -13,29 +12,12 @@ import {
|
|||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from '@/components/ui/sidebar'
|
} from '@/components/ui/sidebar'
|
||||||
import {
|
import { Plus, ListTodo } from 'lucide-react'
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { MoreHorizontal, Plus, Trash2, Pencil, FolderOpen, ListTodo } from 'lucide-react'
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useSidebar } from '@/components/ui/sidebar'
|
import { useSidebar } from '@/components/ui/sidebar'
|
||||||
import { usePlatform } from './platformContext'
|
import { usePlatform } from './platformContext'
|
||||||
@@ -44,22 +26,17 @@ import { NewProjectDialog } from './NewProjectDialog'
|
|||||||
import type { Project } from './types'
|
import type { Project } from './types'
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const { isMobile, state, setOpen } = useSidebar()
|
const { state, setOpen } = useSidebar()
|
||||||
const isCollapsed = state === 'collapsed'
|
const isCollapsed = state === 'collapsed'
|
||||||
const { orderedProjects, createProject, deleteProject, renameProject, reorderProjects, projectOrder } = usePlatform()
|
const { orderedProjects, createProject, recentProjectIds } = usePlatform()
|
||||||
|
const recentProjects = useMemo(() => {
|
||||||
|
const byId = new Map(orderedProjects.map((p) => [p.id, p]))
|
||||||
|
return recentProjectIds.map((id) => byId.get(id)).filter((p): p is Project => p != null)
|
||||||
|
}, [orderedProjects, recentProjectIds])
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { projectId: selectedProjectId } = useParams<{ projectId: string }>()
|
const { projectId: selectedProjectId } = useParams<{ projectId: string }>()
|
||||||
const dragRef = useRef(false)
|
|
||||||
const [renameTarget, setRenameTarget] = useState<Project | null>(null)
|
|
||||||
const [renameValue, setRenameValue] = useState('')
|
|
||||||
|
|
||||||
const handleSelectProject = useCallback(
|
const handleSelectProject = useCallback((id: string) => navigate(`/projects/${id}`), [navigate])
|
||||||
(id: string) => {
|
|
||||||
if (dragRef.current) return
|
|
||||||
navigate(`/projects/${id}`)
|
|
||||||
},
|
|
||||||
[navigate]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleCreateProject = useCallback(
|
const handleCreateProject = useCallback(
|
||||||
(project: Parameters<typeof createProject>[0]) => {
|
(project: Parameters<typeof createProject>[0]) => {
|
||||||
@@ -69,58 +46,6 @@ export function AppSidebar() {
|
|||||||
[createProject, navigate]
|
[createProject, navigate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDeleteProject = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
deleteProject(id)
|
|
||||||
if (selectedProjectId === id) navigate('/projects', { replace: true })
|
|
||||||
},
|
|
||||||
[deleteProject, selectedProjectId, navigate]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleRenameOpen = useCallback((project: Project) => {
|
|
||||||
setRenameTarget(project)
|
|
||||||
setRenameValue(project.name)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleRenameSubmit = useCallback(() => {
|
|
||||||
if (renameTarget && renameValue.trim()) {
|
|
||||||
renameProject(renameTarget.id, renameValue.trim())
|
|
||||||
setRenameTarget(null)
|
|
||||||
setRenameValue('')
|
|
||||||
}
|
|
||||||
}, [renameTarget, renameValue, renameProject])
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: React.DragEvent, projectId: string) => {
|
|
||||||
dragRef.current = true
|
|
||||||
e.dataTransfer.setData('text/plain', projectId)
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
|
||||||
dragRef.current = false
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.dataTransfer.dropEffect = 'move'
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(e: React.DragEvent, dropTargetId: string) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const draggedId = e.dataTransfer.getData('text/plain')
|
|
||||||
if (!draggedId || draggedId === dropTargetId) return
|
|
||||||
const dropIdx = projectOrder.indexOf(dropTargetId)
|
|
||||||
if (dropIdx === -1) return
|
|
||||||
const next = projectOrder.filter((id) => id !== draggedId)
|
|
||||||
const insertIdx = next.indexOf(dropTargetId)
|
|
||||||
next.splice(insertIdx >= 0 ? insertIdx : next.length, 0, draggedId)
|
|
||||||
reorderProjects(next)
|
|
||||||
dragRef.current = false
|
|
||||||
},
|
|
||||||
[projectOrder, reorderProjects]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar collapsible="icon" className="relative">
|
<Sidebar collapsible="icon" className="relative">
|
||||||
@@ -202,7 +127,6 @@ export function AppSidebar() {
|
|||||||
onClick={isCollapsed ? (e) => { if ((e.target as HTMLElement).closest('button') || (e.target as HTMLElement).closest('a')) return; setOpen(true) } : undefined}
|
onClick={isCollapsed ? (e) => { if ((e.target as HTMLElement).closest('button') || (e.target as HTMLElement).closest('a')) return; setOpen(true) } : undefined}
|
||||||
>
|
>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Projects</SidebarGroupLabel>
|
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="All projects" isActive={!selectedProjectId}>
|
<SidebarMenuButton asChild tooltip="All projects" isActive={!selectedProjectId}>
|
||||||
@@ -212,35 +136,17 @@ export function AppSidebar() {
|
|||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
{orderedProjects.length === 0 ? (
|
</SidebarMenu>
|
||||||
<SidebarMenuItem>
|
</SidebarGroup>
|
||||||
<NewProjectDialog
|
{recentProjects.length > 0 && (
|
||||||
onCreate={handleCreateProject}
|
<SidebarGroup>
|
||||||
existingNames={orderedProjects.map((p) => p.name)}
|
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">Recently used</SidebarGroupLabel>
|
||||||
trigger={
|
<SidebarMenu>
|
||||||
<SidebarMenuButton className="text-sidebar-foreground/70 w-full cursor-pointer">
|
{recentProjects.map((project) => {
|
||||||
<Plus className="size-4" />
|
|
||||||
<span>Create your first project</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{orderedProjects.map((project) => {
|
|
||||||
const Icon = getProjectIcon(project.iconId)
|
const Icon = getProjectIcon(project.iconId)
|
||||||
const isActive = selectedProjectId === project.id
|
const isActive = selectedProjectId === project.id
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem
|
<SidebarMenuItem key={project.id}>
|
||||||
key={project.id}
|
|
||||||
data-project-id={project.id}
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => handleDragStart(e, project.id)}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDrop={(e) => handleDrop(e, project.id)}
|
|
||||||
className="cursor-grab active:cursor-grabbing"
|
|
||||||
>
|
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
tooltip={project.name}
|
tooltip={project.name}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
@@ -249,40 +155,14 @@ export function AppSidebar() {
|
|||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
<span>{project.name}</span>
|
<span>{project.name}</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<SidebarMenuAction showOnHover>
|
|
||||||
<MoreHorizontal />
|
|
||||||
<span className="sr-only">Actions for {project.name}</span>
|
|
||||||
</SidebarMenuAction>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-48 rounded-lg"
|
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
|
||||||
align={isMobile ? 'end' : 'start'}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to={`/projects/${project.id}`}>
|
|
||||||
<FolderOpen className="size-4" />
|
|
||||||
Open
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handleRenameOpen(project)}>
|
|
||||||
<Pencil className="size-4" />
|
|
||||||
Rename
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
onClick={() => handleDeleteProject(project.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
Delete project
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
)}
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<NewProjectDialog
|
<NewProjectDialog
|
||||||
onCreate={handleCreateProject}
|
onCreate={handleCreateProject}
|
||||||
@@ -290,48 +170,17 @@ export function AppSidebar() {
|
|||||||
trigger={
|
trigger={
|
||||||
<SidebarMenuButton className="text-sidebar-foreground/70 w-full cursor-pointer">
|
<SidebarMenuButton className="text-sidebar-foreground/70 w-full cursor-pointer">
|
||||||
<Plus className="size-4" />
|
<Plus className="size-4" />
|
||||||
<span>New project</span>
|
<span>{orderedProjects.length === 0 ? 'Create your first project' : 'New project'}</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter className="relative z-10" />
|
<SidebarFooter className="relative z-10" />
|
||||||
<SidebarRail className="relative z-10" />
|
<SidebarRail className="relative z-10" />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
<Dialog open={!!renameTarget} onOpenChange={(open) => !open && setRenameTarget(null)}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Rename project</DialogTitle>
|
|
||||||
<DialogDescription>Enter a new name for this project.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Input
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') handleRenameSubmit()
|
|
||||||
if (e.key === 'Escape') setRenameTarget(null)
|
|
||||||
}}
|
|
||||||
placeholder="Project name"
|
|
||||||
/>
|
|
||||||
{renameTarget && renameValue.trim() && orderedProjects.some((p) => p.id !== renameTarget.id && p.name.toLowerCase() === renameValue.trim().toLowerCase()) && (
|
|
||||||
<p className="text-xs text-amber-600 dark:text-amber-500">A project with this name already exists.</p>
|
|
||||||
)}
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setRenameTarget(null)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleRenameSubmit} disabled={!renameValue.trim()}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { AppSidebar } from './AppSidebar'
|
|||||||
|
|
||||||
function PlatformLayoutInner() {
|
function PlatformLayoutInner() {
|
||||||
const { projectId } = useParams<{ projectId: string }>()
|
const { projectId } = useParams<{ projectId: string }>()
|
||||||
const { projects } = usePlatform()
|
const { recordProjectAccess } = usePlatform()
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,6 +18,10 @@ function PlatformLayoutInner() {
|
|||||||
else setSidebarOpen(true)
|
else setSidebarOpen(true)
|
||||||
}, [projectId])
|
}, [projectId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId) recordProjectAccess(projectId)
|
||||||
|
}, [projectId, recordProjectAccess])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { PROJECT_VERSION } from './projectGraphStorage'
|
|||||||
|
|
||||||
const STORAGE_KEY = 'zui_platform_projects'
|
const STORAGE_KEY = 'zui_platform_projects'
|
||||||
const ORDER_STORAGE_KEY = 'zui_platform_project_order'
|
const ORDER_STORAGE_KEY = 'zui_platform_project_order'
|
||||||
|
const RECENT_STORAGE_KEY = 'zui_platform_recent_project_ids'
|
||||||
|
const RECENT_MAX = 5
|
||||||
|
|
||||||
function loadOrder(): string[] {
|
function loadOrder(): string[] {
|
||||||
try {
|
try {
|
||||||
@@ -26,6 +28,21 @@ function saveOrder(ids: string[]) {
|
|||||||
localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids))
|
localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadRecentIds(): string[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(RECENT_STORAGE_KEY)
|
||||||
|
if (!raw) return []
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string').slice(0, RECENT_MAX) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecentIds(ids: string[]) {
|
||||||
|
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids))
|
||||||
|
}
|
||||||
|
|
||||||
function loadProjects(): Project[] {
|
function loadProjects(): Project[] {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
@@ -80,6 +97,9 @@ export type PlatformContextValue = {
|
|||||||
projectOrder: string[]
|
projectOrder: string[]
|
||||||
/** Projects in display order (sidebar order, then lastEditedAt) */
|
/** Projects in display order (sidebar order, then lastEditedAt) */
|
||||||
orderedProjects: Project[]
|
orderedProjects: Project[]
|
||||||
|
/** Last RECENT_MAX accessed project IDs (most recent first) */
|
||||||
|
recentProjectIds: string[]
|
||||||
|
recordProjectAccess: (id: string) => void
|
||||||
persist: (next: Project[]) => void
|
persist: (next: Project[]) => void
|
||||||
createProject: (project: Project) => void
|
createProject: (project: Project) => void
|
||||||
deleteProject: (id: string) => void
|
deleteProject: (id: string) => void
|
||||||
@@ -94,6 +114,7 @@ const PlatformContext = createContext<PlatformContextValue | null>(null)
|
|||||||
export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [projects, setProjects] = useState<Project[]>(loadProjects)
|
const [projects, setProjects] = useState<Project[]>(loadProjects)
|
||||||
const [projectOrder, setProjectOrder] = useState<string[]>(loadOrder)
|
const [projectOrder, setProjectOrder] = useState<string[]>(loadOrder)
|
||||||
|
const [recentProjectIds, setRecentProjectIds] = useState<string[]>(loadRecentIds)
|
||||||
|
|
||||||
const persist = useCallback((next: Project[]) => {
|
const persist = useCallback((next: Project[]) => {
|
||||||
setProjects(next)
|
setProjects(next)
|
||||||
@@ -122,6 +143,11 @@ export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
|||||||
saveOrder(nextOrder)
|
saveOrder(nextOrder)
|
||||||
return nextOrder
|
return nextOrder
|
||||||
})
|
})
|
||||||
|
setRecentProjectIds((prev) => {
|
||||||
|
const next = prev.filter((oid) => oid !== id)
|
||||||
|
saveRecentIds(next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[projects, persist]
|
[projects, persist]
|
||||||
)
|
)
|
||||||
@@ -148,6 +174,14 @@ export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
|||||||
saveOrder(orderedIds)
|
saveOrder(orderedIds)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const recordProjectAccess = useCallback((id: string) => {
|
||||||
|
setRecentProjectIds((prev) => {
|
||||||
|
const next = [id, ...prev.filter((x) => x !== id)].slice(0, RECENT_MAX)
|
||||||
|
saveRecentIds(next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const restoreProject = useCallback(
|
const restoreProject = useCallback(
|
||||||
(project: Project, graphSnapshot: GraphSnapshot | null) => {
|
(project: Project, graphSnapshot: GraphSnapshot | null) => {
|
||||||
persist([...projects, project])
|
persist([...projects, project])
|
||||||
@@ -177,6 +211,8 @@ export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
|||||||
projects,
|
projects,
|
||||||
projectOrder,
|
projectOrder,
|
||||||
orderedProjects,
|
orderedProjects,
|
||||||
|
recentProjectIds,
|
||||||
|
recordProjectAccess,
|
||||||
persist,
|
persist,
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
@@ -189,6 +225,8 @@ export function PlatformProvider({ children }: { children: React.ReactNode }) {
|
|||||||
projects,
|
projects,
|
||||||
projectOrder,
|
projectOrder,
|
||||||
orderedProjects,
|
orderedProjects,
|
||||||
|
recentProjectIds,
|
||||||
|
recordProjectAccess,
|
||||||
persist,
|
persist,
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
|
|||||||
Reference in New Issue
Block a user