From bb6019ae8d6e416be3a32ca9d611f6e8f706c23d Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Thu, 5 Feb 2026 19:09:07 -0500 Subject: [PATCH] fix(frontend): resolve file loading, display, and cursor issues - Fix API to return JSON response for file content instead of plain text - Fix file display showing [object Object] by properly extracting content field - Fix infinite save loop by tracking last saved content - Remove auto-save that was causing cursor jumping on every keystroke - Add manual save button with disabled state when content unchanged - Add validation in FileList to prevent undefined filenames - Improve error handling for file operations --- backend/internal/api/server.go | 9 ++- frontend/src/App.tsx | 88 ++++++++++++++++++++-------- frontend/src/components/Editor.tsx | 32 +++++----- frontend/src/components/FileList.tsx | 31 ++++++---- 4 files changed, 101 insertions(+), 59 deletions(-) diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go index fd0cc89..4880dc9 100644 --- a/backend/internal/api/server.go +++ b/backend/internal/api/server.go @@ -95,8 +95,13 @@ func (s *Server) handleGetFiles(w http.ResponseWriter, r *http.Request) { s.sendError(w, http.StatusNotFound, err.Error()) return } - w.Header().Set("Content-Type", "text/markdown") - w.Write([]byte(content)) + // Return file content as JSON + response := map[string]string{ + "name": filename, + "content": content, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) return } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 66e057b..ce06e6c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ function App() { const [content, setContent] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [lastSavedContent, setLastSavedContent] = useState(''); const { theme, toggleTheme } = useTheme(); // Fetch files list @@ -23,7 +24,8 @@ function App() { const response = await fetch(`${API_URL}/api/files`); if (!response.ok) throw new Error('Failed to fetch files'); const data = await response.json(); - setFiles(data); + // Convert array of strings to array of File objects + setFiles(Array.isArray(data) ? data.map((file: string) => ({ name: file })) : []); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch files'); } finally { @@ -33,20 +35,27 @@ function App() { // Load file content const loadFile = useCallback(async (filename: string) => { + if (!filename) { + setError('Invalid file selected'); + return; + } try { setLoading(true); setError(null); const response = await fetch(`${API_URL}/api/files/${filename}`); if (!response.ok) throw new Error('Failed to load file'); const data = await response.json(); - setContent(data); + // Only set content if the file is different from current + if (currentFile?.name !== filename) { + setContent(data.content || ''); + } setCurrentFile({ name: filename }); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load file'); } finally { setLoading(false); } - }, []); + }, [currentFile?.name]); // Create new file const createFile = async (filename: string, initialContent: string = '') => { @@ -69,7 +78,7 @@ function App() { // Update file content const updateFile = async () => { - if (!currentFile) return; + if (!currentFile || !content) return; try { setLoading(true); setError(null); @@ -79,6 +88,8 @@ function App() { body: JSON.stringify({ content }), }); if (!response.ok) throw new Error('Failed to update file'); + // Only update lastSavedContent on successful update + setLastSavedContent(content); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update file'); } finally { @@ -108,14 +119,8 @@ function App() { }; // Sync editor content with server - useEffect(() => { - const timeoutId = setTimeout(() => { - if (currentFile && content) { - updateFile(); - } - }, 500); - return () => clearTimeout(timeoutId); - }, [content, currentFile, updateFile]); + // Removed auto-save to prevent cursor jumping + // User can manually save or we'll add a button later // Initial file fetch useEffect(() => { @@ -153,23 +158,38 @@ function App() { + New + {currentFile && content && ( + + )} {loading && files.length === 0 ? (

Loading...

) : ( )} {error && ( @@ -190,7 +210,14 @@ function App() { /> {/* Preview */} - {currentFile && ( + {loading && !currentFile ? ( +
+
+

Preview

+

Loading...

+
+
+ ) : currentFile ? (

Preview

@@ -202,6 +229,15 @@ function App() {
+ ) : ( +
+
+

Preview

+

+ Select a file from the sidebar to view and edit its contents. +

+
+
)} diff --git a/frontend/src/components/Editor.tsx b/frontend/src/components/Editor.tsx index 444d4f9..711a594 100644 --- a/frontend/src/components/Editor.tsx +++ b/frontend/src/components/Editor.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react'; +import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; interface EditorProps { @@ -8,29 +8,25 @@ interface EditorProps { } const Editor: React.FC = ({ content, onChange, disabled }) => { - const textareaRef = useRef(null); - - useEffect(() => { - if (textareaRef.current && textareaRef.current.value !== content) { - textareaRef.current.value = content; - } - }, [content]); - const handleChange = (e: React.ChangeEvent) => { onChange(e.target.value); }; return (
-