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())
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user