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
This commit is contained in:
2026-02-05 19:09:07 -05:00
parent 67c4bdf0c7
commit bb6019ae8d
4 changed files with 101 additions and 59 deletions

View File

@@ -95,8 +95,13 @@ func (s *Server) handleGetFiles(w http.ResponseWriter, r *http.Request) {
s.sendError(w, http.StatusNotFound, err.Error()) s.sendError(w, http.StatusNotFound, err.Error())
return return
} }
w.Header().Set("Content-Type", "text/markdown") // Return file content as JSON
w.Write([]byte(content)) response := map[string]string{
"name": filename,
"content": content,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return return
} }

View File

@@ -14,6 +14,7 @@ function App() {
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [lastSavedContent, setLastSavedContent] = useState('');
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
// Fetch files list // Fetch files list
@@ -23,7 +24,8 @@ function App() {
const response = await fetch(`${API_URL}/api/files`); const response = await fetch(`${API_URL}/api/files`);
if (!response.ok) throw new Error('Failed to fetch files'); if (!response.ok) throw new Error('Failed to fetch files');
const data = await response.json(); 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) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch files'); setError(err instanceof Error ? err.message : 'Failed to fetch files');
} finally { } finally {
@@ -33,20 +35,27 @@ function App() {
// Load file content // Load file content
const loadFile = useCallback(async (filename: string) => { const loadFile = useCallback(async (filename: string) => {
if (!filename) {
setError('Invalid file selected');
return;
}
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const response = await fetch(`${API_URL}/api/files/${filename}`); const response = await fetch(`${API_URL}/api/files/${filename}`);
if (!response.ok) throw new Error('Failed to load file'); if (!response.ok) throw new Error('Failed to load file');
const data = await response.json(); 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 }); setCurrentFile({ name: filename });
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load file'); setError(err instanceof Error ? err.message : 'Failed to load file');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [currentFile?.name]);
// Create new file // Create new file
const createFile = async (filename: string, initialContent: string = '') => { const createFile = async (filename: string, initialContent: string = '') => {
@@ -69,7 +78,7 @@ function App() {
// Update file content // Update file content
const updateFile = async () => { const updateFile = async () => {
if (!currentFile) return; if (!currentFile || !content) return;
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -79,6 +88,8 @@ function App() {
body: JSON.stringify({ content }), body: JSON.stringify({ content }),
}); });
if (!response.ok) throw new Error('Failed to update file'); if (!response.ok) throw new Error('Failed to update file');
// Only update lastSavedContent on successful update
setLastSavedContent(content);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update file'); setError(err instanceof Error ? err.message : 'Failed to update file');
} finally { } finally {
@@ -108,14 +119,8 @@ function App() {
}; };
// Sync editor content with server // Sync editor content with server
useEffect(() => { // Removed auto-save to prevent cursor jumping
const timeoutId = setTimeout(() => { // User can manually save or we'll add a button later
if (currentFile && content) {
updateFile();
}
}, 500);
return () => clearTimeout(timeoutId);
}, [content, currentFile, updateFile]);
// Initial file fetch // Initial file fetch
useEffect(() => { useEffect(() => {
@@ -153,23 +158,38 @@ function App() {
+ New + New
</button> </button>
</div> </div>
{currentFile && content && (
<button
onClick={updateFile}
disabled={loading || content === lastSavedContent}
className="w-full mb-2 px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition text-sm disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Save
</button>
)}
{loading && files.length === 0 ? ( {loading && files.length === 0 ? (
<p className="text-secondary">Loading...</p> <p className="text-secondary">Loading...</p>
) : ( ) : (
<ul className="space-y-2"> <ul className="space-y-2">
{files.map((file) => ( {files.map((file) => {
<li if (!file || !file.name) {
key={file.name} console.warn('Invalid file object:', file);
className={`p-2 rounded cursor-pointer transition ${ return null;
currentFile?.name === file.name }
? 'bg-blue-500 text-white' return (
: 'hover:bg-opacity-80' <li
}`} key={file.name}
onClick={() => loadFile(file.name)} className={`p-2 rounded cursor-pointer transition ${
> currentFile?.name === file.name
<span className="truncate block">{file.name}</span> ? 'bg-blue-500 text-white'
</li> : 'hover:bg-opacity-80'
))} }`}
onClick={() => loadFile(file.name)}
>
<span className="truncate block">{file.name}</span>
</li>
);
})}
</ul> </ul>
)} )}
{error && ( {error && (
@@ -190,7 +210,14 @@ function App() {
/> />
{/* Preview */} {/* Preview */}
{currentFile && ( {loading && !currentFile ? (
<div className="flex-1 overflow-y-auto border-t border-custom">
<div className="bg-primary p-4">
<h2 className="text-lg font-semibold mb-4">Preview</h2>
<p className="text-gray-500">Loading...</p>
</div>
</div>
) : currentFile ? (
<div className="flex-1 overflow-y-auto border-t border-custom"> <div className="flex-1 overflow-y-auto border-t border-custom">
<div className="bg-primary p-4"> <div className="bg-primary p-4">
<h2 className="text-lg font-semibold mb-4">Preview</h2> <h2 className="text-lg font-semibold mb-4">Preview</h2>
@@ -202,6 +229,15 @@ function App() {
</div> </div>
</div> </div>
</div> </div>
) : (
<div className="flex-1 overflow-y-auto border-t border-custom">
<div className="bg-primary p-4">
<h2 className="text-lg font-semibold mb-4">Preview</h2>
<p className="text-gray-500">
Select a file from the sidebar to view and edit its contents.
</p>
</div>
</div>
)} )}
</div> </div>
</main> </main>

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react'; import React from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
interface EditorProps { interface EditorProps {
@@ -8,29 +8,25 @@ interface EditorProps {
} }
const Editor: React.FC<EditorProps> = ({ content, onChange, disabled }) => { const Editor: React.FC<EditorProps> = ({ content, onChange, disabled }) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current && textareaRef.current.value !== content) {
textareaRef.current.value = content;
}
}, [content]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(e.target.value); onChange(e.target.value);
}; };
return ( return (
<div className="h-1/2 border-r border-custom p-4"> <div className="h-1/2 border-r border-custom p-4">
<textarea {disabled ? (
ref={textareaRef} <div className="w-full h-full bg-gray-100 rounded flex items-center justify-center text-gray-400">
value={content} Loading file...
onChange={handleChange} </div>
disabled={disabled} ) : (
placeholder="Start writing your markdown here..." <textarea
className="w-full h-full bg-transparent resize-none outline-none font-mono text-sm leading-relaxed" value={content}
spellCheck={false} onChange={handleChange}
/> placeholder="Start writing your markdown here..."
className="w-full h-full bg-transparent resize-none outline-none font-mono text-sm leading-relaxed"
spellCheck={false}
/>
)}
</div> </div>
); );
}; };

View File

@@ -36,19 +36,24 @@ const FileList: React.FC<FileListProps> = ({
<p className="text-gray-500">Loading...</p> <p className="text-gray-500">Loading...</p>
) : ( ) : (
<ul className="space-y-2"> <ul className="space-y-2">
{files.map((file) => ( {files.map((file) => {
<li if (!file || !file.name) {
key={file.name} return null;
className={`p-2 rounded cursor-pointer transition ${ }
currentFile?.name === file.name return (
? 'bg-blue-500 text-white' <li
: 'hover:bg-gray-200' key={file.name}
}`} className={`p-2 rounded cursor-pointer transition ${
onClick={() => onFileClick(file)} currentFile?.name === file.name
> ? 'bg-blue-500 text-white'
<span className="truncate block">{file.name}</span> : 'hover:bg-gray-200'
</li> }`}
))} onClick={() => onFileClick(file)}
>
<span className="truncate block">{file.name}</span>
</li>
);
})}
</ul> </ul>
)} )}
</div> </div>