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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,23 +158,38 @@ 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) => (
|
||||
<li
|
||||
key={file.name}
|
||||
className={`p-2 rounded cursor-pointer transition ${
|
||||
currentFile?.name === file.name
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'hover:bg-opacity-80'
|
||||
}`}
|
||||
onClick={() => loadFile(file.name)}
|
||||
>
|
||||
<span className="truncate block">{file.name}</span>
|
||||
</li>
|
||||
))}
|
||||
{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 ${
|
||||
currentFile?.name === file.name
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'hover:bg-opacity-80'
|
||||
}`}
|
||||
onClick={() => loadFile(file.name)}
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
<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}
|
||||
/>
|
||||
{disabled ? (
|
||||
<div className="w-full h-full bg-gray-100 rounded flex items-center justify-center text-gray-400">
|
||||
Loading file...
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
value={content}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,19 +36,24 @@ const FileList: React.FC<FileListProps> = ({
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{files.map((file) => (
|
||||
<li
|
||||
key={file.name}
|
||||
className={`p-2 rounded cursor-pointer transition ${
|
||||
currentFile?.name === file.name
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'hover:bg-gray-200'
|
||||
}`}
|
||||
onClick={() => onFileClick(file)}
|
||||
>
|
||||
<span className="truncate block">{file.name}</span>
|
||||
</li>
|
||||
))}
|
||||
{files.map((file) => {
|
||||
if (!file || !file.name) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={file.name}
|
||||
className={`p-2 rounded cursor-pointer transition ${
|
||||
currentFile?.name === file.name
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'hover:bg-gray-200'
|
||||
}`}
|
||||
onClick={() => onFileClick(file)}
|
||||
>
|
||||
<span className="truncate block">{file.name}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user