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())
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
}

View File

@@ -14,6 +14,7 @@ function App() {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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,11 +158,25 @@ function App() {
+ New
</button>
</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 ? (
<p className="text-secondary">Loading...</p>
) : (
<ul className="space-y-2">
{files.map((file) => (
{files.map((file) => {
if (!file || !file.name) {
console.warn('Invalid file object:', file);
return null;
}
return (
<li
key={file.name}
className={`p-2 rounded cursor-pointer transition ${
@@ -169,7 +188,8 @@ function App() {
>
<span className="truncate block">{file.name}</span>
</li>
))}
);
})}
</ul>
)}
{error && (
@@ -190,7 +210,14 @@ function App() {
/>
{/* 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="bg-primary p-4">
<h2 className="text-lg font-semibold mb-4">Preview</h2>
@@ -202,6 +229,15 @@ function App() {
</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>
</main>

View File

@@ -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<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>) => {
onChange(e.target.value);
};
return (
<div className="h-1/2 border-r border-custom p-4">
{disabled ? (
<div className="w-full h-full bg-gray-100 rounded flex items-center justify-center text-gray-400">
Loading file...
</div>
) : (
<textarea
ref={textareaRef}
value={content}
onChange={handleChange}
disabled={disabled}
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>
);
};

View File

@@ -36,7 +36,11 @@ const FileList: React.FC<FileListProps> = ({
<p className="text-gray-500">Loading...</p>
) : (
<ul className="space-y-2">
{files.map((file) => (
{files.map((file) => {
if (!file || !file.name) {
return null;
}
return (
<li
key={file.name}
className={`p-2 rounded cursor-pointer transition ${
@@ -48,7 +52,8 @@ const FileList: React.FC<FileListProps> = ({
>
<span className="truncate block">{file.name}</span>
</li>
))}
);
})}
</ul>
)}
</div>