From b9d778b379c804ffa9ca407fdf0edfe2e9668061 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Thu, 22 Jan 2026 23:06:43 -0300 Subject: [PATCH] feat: [US-039] - Loading and error states Co-Authored-By: Claude Opus 4.5 --- .../editor/[projectId]/FlowchartEditor.tsx | 15 ++++- src/app/editor/[projectId]/loading.tsx | 28 +--------- src/app/editor/[projectId]/page.tsx | 56 ++++++++++++++++++- src/components/LoadingSpinner.tsx | 40 +++++++++++++ src/components/Toast.tsx | 19 ++++++- 5 files changed, 127 insertions(+), 31 deletions(-) create mode 100644 src/components/LoadingSpinner.tsx diff --git a/src/app/editor/[projectId]/FlowchartEditor.tsx b/src/app/editor/[projectId]/FlowchartEditor.tsx index 3cd6759..60bb57d 100644 --- a/src/app/editor/[projectId]/FlowchartEditor.tsx +++ b/src/app/editor/[projectId]/FlowchartEditor.tsx @@ -413,7 +413,7 @@ function FlowchartEditorInner({ projectId, projectName, initialData }: Flowchart const [contextMenu, setContextMenu] = useState(null) const [conditionEditor, setConditionEditor] = useState(null) const [isSaving, setIsSaving] = useState(false) - const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null) + const [toast, setToast] = useState<{ message: string; type: 'success' | 'error'; action?: { label: string; onClick: () => void } } | null>(null) const [importConfirmDialog, setImportConfirmDialog] = useState<{ pendingData: FlowchartData } | null>(null) @@ -425,6 +425,9 @@ function FlowchartEditorInner({ projectId, projectName, initialData }: Flowchart // Ref for hidden file input const fileInputRef = useRef(null) + // Ref for save function to enable retry without circular dependency + const handleSaveRef = useRef<() => void>(() => {}) + // Check for saved draft on initial render (lazy initialization) const [draftState, setDraftState] = useState<{ showPrompt: boolean @@ -628,12 +631,19 @@ function FlowchartEditorInner({ projectId, projectName, initialData }: Flowchart setToast({ message: 'Project saved successfully', type: 'success' }) } catch (error) { console.error('Failed to save project:', error) - setToast({ message: 'Failed to save project. Please try again.', type: 'error' }) + setToast({ + message: 'Failed to save project.', + type: 'error', + action: { label: 'Retry', onClick: () => { setToast(null); handleSaveRef.current() } }, + }) } finally { setIsSaving(false) } }, [isSaving, nodes, edges, projectId]) + // Keep ref updated with latest handleSave + handleSaveRef.current = handleSave + const handleExport = useCallback(() => { // Convert React Flow state to FlowchartData const flowchartData: FlowchartData = { @@ -1184,6 +1194,7 @@ function FlowchartEditorInner({ projectId, projectName, initialData }: Flowchart message={toast.message} type={toast.type} onClose={() => setToast(null)} + action={toast.action} /> )} diff --git a/src/app/editor/[projectId]/loading.tsx b/src/app/editor/[projectId]/loading.tsx index 9ec9c55..231ffce 100644 --- a/src/app/editor/[projectId]/loading.tsx +++ b/src/app/editor/[projectId]/loading.tsx @@ -1,3 +1,5 @@ +import LoadingSpinner from '@/components/LoadingSpinner' + export default function EditorLoading() { return (
@@ -9,31 +11,7 @@ export default function EditorLoading() {
-
- - - - -

- Loading editor... -

-
+
) diff --git a/src/app/editor/[projectId]/page.tsx b/src/app/editor/[projectId]/page.tsx index 95bb0aa..59ed19d 100644 --- a/src/app/editor/[projectId]/page.tsx +++ b/src/app/editor/[projectId]/page.tsx @@ -1,5 +1,5 @@ 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' @@ -27,7 +27,59 @@ export default async function EditorPage({ params }: PageProps) { .single() if (error || !project) { - notFound() + return ( +
+
+ + + + + +
+
+
+ + + +

+ Project Not Found +

+

+ The project you're looking for doesn't exist or you don't have access to it. +

+ + Back to Dashboard + +
+
+
+ ) } const flowchartData = (project.flowchart_data || { diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..bc2e451 --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,40 @@ +type LoadingSpinnerProps = { + size?: 'sm' | 'md' | 'lg' + message?: string +} + +const sizeClasses = { + sm: 'h-4 w-4', + md: 'h-8 w-8', + lg: 'h-12 w-12', +} + +export default function LoadingSpinner({ size = 'md', message }: LoadingSpinnerProps) { + return ( +
+ + + + + {message && ( +

{message}

+ )} +
+ ) +} diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 18ef18a..52b9687 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -6,16 +6,23 @@ interface ToastProps { message: string type: 'success' | 'error' onClose: () => void + action?: { + label: string + onClick: () => void + } } -export default function Toast({ message, type, onClose }: ToastProps) { +export default function Toast({ message, type, onClose, action }: ToastProps) { useEffect(() => { + // Don't auto-dismiss if there's an action button + if (action) return + const timer = setTimeout(() => { onClose() }, 3000) return () => clearTimeout(timer) - }, [onClose]) + }, [onClose, action]) const bgColor = type === 'success' @@ -60,6 +67,14 @@ export default function Toast({ message, type, onClose }: ToastProps) { > {icon} {message} + {action && ( + + )}