feat: [US-017] - Editor page with React Flow canvas
Implement the flowchart editor page with React Flow integration: - Install reactflow package for canvas rendering - Create editor page that fetches project data from Supabase - Add loading state with spinner while fetching - Handle unauthorized access and not found with appropriate responses - Render React Flow canvas with grid background - Add header with project name and back link to dashboard - Initialize React Flow with nodes and edges from flowchart_data - Add type converters for FlowchartNode/Edge to React Flow types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4d0ee8e578
commit
db742e7fe8
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
addEdge,
|
||||
Connection,
|
||||
Node,
|
||||
Edge,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import type { FlowchartData, FlowchartNode, FlowchartEdge } from '@/types/flowchart'
|
||||
|
||||
type FlowchartEditorProps = {
|
||||
projectId: string
|
||||
initialData: FlowchartData
|
||||
}
|
||||
|
||||
// Convert our FlowchartNode type to React Flow Node type
|
||||
function toReactFlowNodes(nodes: FlowchartNode[]): Node[] {
|
||||
return nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
position: node.position,
|
||||
data: node.data,
|
||||
}))
|
||||
}
|
||||
|
||||
// Convert our FlowchartEdge type to React Flow Edge type
|
||||
function toReactFlowEdges(edges: FlowchartEdge[]): Edge[] {
|
||||
return edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle,
|
||||
data: edge.data,
|
||||
}))
|
||||
}
|
||||
|
||||
export default function FlowchartEditor({
|
||||
initialData,
|
||||
}: FlowchartEditorProps) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
toReactFlowNodes(initialData.nodes)
|
||||
)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
toReactFlowEdges(initialData.edges)
|
||||
)
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export default function EditorLoading() {
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<header className="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-5 w-5 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
|
||||
<div className="h-6 w-32 animate-pulse rounded bg-zinc-200 dark:bg-zinc-700" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 items-center justify-center bg-zinc-100 dark:bg-zinc-800">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<svg
|
||||
className="h-8 w-8 animate-spin text-blue-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Loading editor...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import FlowchartEditor from './FlowchartEditor'
|
||||
import type { FlowchartData } from '@/types/flowchart'
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ projectId: string }>
|
||||
}
|
||||
|
||||
export default async function EditorPage({ params }: PageProps) {
|
||||
const { projectId } = await params
|
||||
const supabase = await createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { data: project, error } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, flowchart_data')
|
||||
.eq('id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (error || !project) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const flowchartData = (project.flowchart_data || {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
}) as FlowchartData
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<header className="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">
|
||||
{project.name}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1">
|
||||
<FlowchartEditor
|
||||
projectId={project.id}
|
||||
initialData={flowchartData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user