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:
Gustavo Henrique Santos Souza de Miranda
2026-01-21 12:41:08 -03:00
co-authored by Claude Opus 4.5
parent 4d0ee8e578
commit db742e7fe8
5 changed files with 722 additions and 6 deletions
@@ -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>
)
}
+40
View File
@@ -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>
)
}
+74
View File
@@ -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>
)
}