320 lines
9.7 KiB
TypeScript
320 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Image, FileText, Link, List, Plus } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useSession } from 'next-auth/react';
|
|
|
|
interface Note {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
lastModified: string;
|
|
type: string;
|
|
mime: string;
|
|
etag: string;
|
|
}
|
|
|
|
interface EditorProps {
|
|
note?: Note | null;
|
|
onSave?: (note: Note) => void;
|
|
currentFolder?: string;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'Notes', onRefresh }) => {
|
|
const [title, setTitle] = useState(note?.title || '');
|
|
const [content, setContent] = useState(note?.content || '');
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const saveTimeout = useRef<NodeJS.Timeout>();
|
|
const router = useRouter();
|
|
const { data: session, status } = useSession();
|
|
|
|
// Content cache for notes
|
|
const contentCache = useRef<Record<string, { content: string; timestamp: number }>>({});
|
|
const CACHE_EXPIRATION = 15 * 60 * 1000; // 15 minutes in milliseconds
|
|
|
|
useEffect(() => {
|
|
// Redirect to login if not authenticated
|
|
if (status === 'unauthenticated') {
|
|
router.push('/signin');
|
|
}
|
|
}, [status, router]);
|
|
|
|
useEffect(() => {
|
|
const fetchNoteContent = async () => {
|
|
if (note?.id) {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
// First check in-memory cache
|
|
const cachedContent = contentCache.current[note.id];
|
|
if (cachedContent && (Date.now() - cachedContent.timestamp) < CACHE_EXPIRATION) {
|
|
console.log(`Using cached content for note ${note.title}`);
|
|
setContent(cachedContent.content);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Then check localStorage cache
|
|
try {
|
|
const localStorageKey = `note-content-${note.id}`;
|
|
const storedCache = localStorage.getItem(localStorageKey);
|
|
|
|
if (storedCache) {
|
|
const { content, timestamp } = JSON.parse(storedCache);
|
|
|
|
if ((Date.now() - timestamp) < CACHE_EXPIRATION) {
|
|
console.log(`Using localStorage cached content for note ${note.title}`);
|
|
setContent(content);
|
|
|
|
// Update in-memory cache
|
|
contentCache.current[note.id] = { content, timestamp };
|
|
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error accessing localStorage content cache:', error);
|
|
}
|
|
|
|
// If cache miss, fetch from API
|
|
try {
|
|
const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(note.id)}`);
|
|
if (response.status === 401) {
|
|
console.error('Authentication error, redirecting to login');
|
|
router.push('/signin');
|
|
return;
|
|
}
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Failed to fetch note content: ${response.status} ${errorText}`);
|
|
}
|
|
const data = await response.json();
|
|
setContent(data.content);
|
|
|
|
// Update both caches
|
|
const newTimestamp = Date.now();
|
|
contentCache.current[note.id] = { content: data.content, timestamp: newTimestamp };
|
|
|
|
try {
|
|
localStorage.setItem(`note-content-${note.id}`, JSON.stringify({
|
|
content: data.content,
|
|
timestamp: newTimestamp
|
|
}));
|
|
} catch (error) {
|
|
console.error('Error saving content to localStorage:', error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching note content:', error);
|
|
setError('Failed to load note content. Please try again later.');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (note) {
|
|
setTitle(note.title);
|
|
if (note.id) {
|
|
fetchNoteContent();
|
|
} else {
|
|
setContent('');
|
|
}
|
|
} else {
|
|
setTitle('');
|
|
setContent('');
|
|
}
|
|
}, [note, router, CACHE_EXPIRATION]);
|
|
|
|
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setTitle(e.target.value);
|
|
debouncedSave();
|
|
};
|
|
|
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
setContent(e.target.value);
|
|
debouncedSave();
|
|
};
|
|
|
|
const debouncedSave = () => {
|
|
if (saveTimeout.current) {
|
|
clearTimeout(saveTimeout.current);
|
|
}
|
|
saveTimeout.current = setTimeout(() => {
|
|
handleSave();
|
|
}, 1000); // Save after 1 second of inactivity
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!title || !content) return;
|
|
if (!session) {
|
|
console.error('No active session, cannot save');
|
|
setError('You must be logged in to save notes');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setError(null);
|
|
try {
|
|
// Construct the full note ID if it doesn't exist yet
|
|
const noteId = note?.id || `user-${session.user.id}/${currentFolder.toLowerCase()}/${title}${title.endsWith('.md') ? '' : '.md'}`;
|
|
|
|
const endpoint = '/api/storage/files';
|
|
const method = note?.id ? 'PUT' : 'POST';
|
|
|
|
console.log('Saving note:', {
|
|
id: noteId,
|
|
title,
|
|
folder: currentFolder,
|
|
contentLength: content.length
|
|
});
|
|
|
|
const response = await fetch(endpoint, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
id: noteId,
|
|
title,
|
|
content,
|
|
folder: currentFolder.toLowerCase()
|
|
}),
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
console.error('Authentication error, redirecting to login');
|
|
router.push('/signin');
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.text();
|
|
console.error('Failed to save note:', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
data: errorData
|
|
});
|
|
setError(`Failed to save note: ${response.statusText}`);
|
|
throw new Error(`Failed to save note: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const savedNote = await response.json();
|
|
console.log('Note saved successfully:', savedNote);
|
|
|
|
// Update content cache after successful save
|
|
const newTimestamp = Date.now();
|
|
|
|
// Update in-memory cache
|
|
contentCache.current[noteId] = {
|
|
content,
|
|
timestamp: newTimestamp
|
|
};
|
|
|
|
// Update localStorage cache
|
|
try {
|
|
localStorage.setItem(`note-content-${noteId}`, JSON.stringify({
|
|
content,
|
|
timestamp: newTimestamp
|
|
}));
|
|
} catch (error) {
|
|
console.error('Error updating content cache in localStorage:', error);
|
|
}
|
|
|
|
setError(null);
|
|
onSave?.({
|
|
...savedNote,
|
|
content
|
|
});
|
|
onRefresh?.();
|
|
} catch (error) {
|
|
console.error('Error saving note:', error);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
if (!session && status !== 'loading') {
|
|
return (
|
|
<div className="flex flex-col h-full bg-carnet-bg items-center justify-center">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-carnet-text-primary mb-2">
|
|
Authentication Required
|
|
</h2>
|
|
<p className="text-carnet-text-muted mb-4">
|
|
Please log in to access your notes
|
|
</p>
|
|
<button
|
|
onClick={() => router.push('/signin')}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
>
|
|
Go to Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!note) {
|
|
return (
|
|
<div className="flex flex-col h-full bg-carnet-bg items-center justify-center">
|
|
<div className="text-center">
|
|
<FileText className="h-12 w-12 text-carnet-text-muted mx-auto mb-4" />
|
|
<h2 className="text-xl font-semibold text-carnet-text-primary mb-2">
|
|
Sélectionnez une note
|
|
</h2>
|
|
<p className="text-carnet-text-muted">
|
|
Choisissez une note existante ou créez-en une nouvelle
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-carnet-bg">
|
|
{/* Title Bar */}
|
|
<div className="p-4 border-b border-carnet-border">
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={handleTitleChange}
|
|
placeholder="Titre"
|
|
className="w-full text-xl font-semibold text-carnet-text-primary placeholder-carnet-text-muted focus:outline-none bg-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="p-2 m-2 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Editor Area */}
|
|
<div className="flex-1 p-4">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
|
|
</div>
|
|
) : (
|
|
<textarea
|
|
value={content}
|
|
onChange={handleContentChange}
|
|
placeholder="Ecrire..."
|
|
className="w-full h-full resize-none focus:outline-none bg-transparent text-carnet-text-primary placeholder-carnet-text-muted"
|
|
/>
|
|
)}
|
|
</div>
|
|
{isSaving && (
|
|
<div className="absolute bottom-4 right-4 text-sm text-carnet-text-muted">
|
|
Enregistrement...
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|