NeahStable/components/carnet/editor.tsx

353 lines
13 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';
import { noteContentCache } from '@/lib/cache-utils';
import { HealthForm } from './health-form';
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<string>(note?.title || '');
const [content, setContent] = useState<string>(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();
useEffect(() => {
// Redirect to login if not authenticated
if (status === 'unauthenticated') {
router.push('/signin');
}
}, [status, router]);
useEffect(() => {
const fetchNoteContent = async () => {
if (note?.id) {
// Skip fetching if id is temporary (starts with 'temp-')
// Temporary ids are used for new notes that haven't been saved yet
if (note.id.startsWith('temp-')) {
console.log(`Skipping fetch for temporary note id: ${note.id}`);
setIsLoading(false);
setContent(note.content || '');
return;
}
// If content is already provided (e.g., for mission files), use it directly
if (note.content !== undefined && note.content !== '') {
setContent(note.content);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
// Check cache first
const cachedContent = noteContentCache.get<string>(note.id);
if (cachedContent) {
console.log(`Using cached content for note ${note.title}`);
setContent(cachedContent);
setIsLoading(false);
return;
}
// For mission files, don't try to fetch via storage API
if (note.id.startsWith('missions/')) {
console.warn('Mission file content should be provided directly, not fetched');
setIsLoading(false);
return;
}
// 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.status === 404) {
// File not found - this is normal for new notes that haven't been saved yet
console.log(`Note not found (404) for id: ${note.id}, treating as new note`);
setContent('');
setIsLoading(false);
return;
}
if (response.status === 403) {
// Unauthorized access - don't show error, just clear content
console.warn(`Unauthorized access to note: ${note.id}`);
setContent('');
setError(null); // Don't show error for unauthorized access
setIsLoading(false);
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.message || errorData.error || `Failed to fetch note content: ${response.status}`;
// Only show error for non-404/403 errors
if (response.status !== 404 && response.status !== 403) {
throw new Error(errorMessage);
} else {
setContent('');
setIsLoading(false);
return;
}
}
const data = await response.json();
setContent(data.content || '');
// Update cache
noteContentCache.set(note.id, data.content || '');
} catch (error) {
console.error('Error fetching note content:', error);
// Only show error if it's not a 404 or 403 (which we handle above)
const errorMessage = error instanceof Error ? error.message : 'Failed to load note content. Please try again later.';
// Don't show error for file not found or unauthorized - these are handled above
if (!errorMessage.includes('404') && !errorMessage.includes('403') && !errorMessage.includes('File not found') && !errorMessage.includes('Unauthorized')) {
setError(errorMessage);
} else {
setError(null);
setContent('');
}
} finally {
setIsLoading(false);
}
}
};
if (note) {
// For Diary and Health, if it's a new note (no id), set title to today's date
if ((currentFolder === 'Diary' || currentFolder === 'Health') && !note.id) {
const today = new Date();
// Use French locale for date formatting: "16 janvier 2026"
const dateStr = today.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
setTitle(dateStr);
} else {
setTitle(note.title || '');
}
// Set content from note if provided, otherwise it will be fetched
if (note.content !== undefined) {
setContent(note.content);
}
if (note.id) {
fetchNoteContent();
} else {
if (note.content === undefined) {
setContent('');
}
}
} else {
setTitle('');
setContent('');
}
}, [note, router, currentFolder]);
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
debouncedSave();
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
debouncedSave();
};
// Handle content change from HealthForm (direct string update)
const handleHealthContentChange = (newContent: string) => {
setContent(newContent);
// For Health folder, pass the newContent directly to debouncedSave to ensure we use the latest content
// This is important because setContent is async and content state might not be updated when debouncedSave fires
debouncedSave(newContent);
};
const debouncedSave = (contentToSave?: string) => {
if (saveTimeout.current) {
clearTimeout(saveTimeout.current);
}
saveTimeout.current = setTimeout(() => {
console.log(`[Editor] debouncedSave triggered for folder: ${currentFolder}`);
// Use the provided contentToSave if available, otherwise use state content
handleSave(contentToSave);
}, 1000); // Save after 1 second of inactivity
};
const handleSave = async (contentToSave?: string) => {
// Use the provided contentToSave if available, otherwise use state content
// This ensures we use the latest content from HealthForm even if state hasn't updated yet
const contentToUse = contentToSave !== undefined ? contentToSave : content;
console.log(`[Editor] handleSave called - folder: ${currentFolder}, title: "${title}", content length: ${contentToUse?.length || 0}`);
if (!title || !contentToUse) {
console.log(`[Editor] handleSave: Skipping save - title: "${title}", content: "${contentToUse}"`);
return;
}
if (!session) {
console.error('No active session, cannot save');
setError('You must be logged in to save notes');
return;
}
// For Health folder, don't save if content is just empty JSON
if (currentFolder === 'Health') {
try {
const parsed = JSON.parse(contentToUse);
const isEmpty = Object.keys(parsed).length === 0 || (Object.keys(parsed).length === 1 && parsed.date);
if (isEmpty) {
console.log('[Editor] handleSave: Skipping save for empty Health form');
return; // Don't save empty health forms
}
} catch {
// If not valid JSON, continue with save
}
}
setIsSaving(true);
setError(null);
// Instead of saving directly, construct a Note object and pass it to onSave
// This ensures handleSaveNote in page.tsx handles all the logic consistently
try {
// Construct a Note object with current state
const noteToSave: Note = {
id: note?.id || '', // Use existing note.id if available, otherwise empty (will be determined in handleSaveNote)
title: title, // Use the title from local state
content: contentToUse, // Use the provided content or state content
lastModified: note?.lastModified || new Date().toISOString(),
type: note?.type || 'file',
mime: note?.mime || 'text/markdown',
etag: note?.etag || ''
};
console.log('[Editor] Calling onSave with note (delegating to handleSaveNote):', { ...noteToSave, content: `[${contentToUse.length} chars]` });
// Pass the note to onSave callback, which will call handleSaveNote in page.tsx
// handleSaveNote will handle the actual API call with proper duplicate detection
onSave?.(noteToSave);
// Note: onRefresh is called, but handleSaveNote already calls fetchNotes
// So we don't need to call onRefresh here to avoid double fetch
// 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"
disabled={currentFolder === 'Diary' || currentFolder === 'Health'}
className={`w-full text-xl font-semibold text-carnet-text-primary placeholder-carnet-text-muted focus:outline-none bg-transparent ${
(currentFolder === 'Diary' || currentFolder === 'Health') ? 'opacity-60 cursor-not-allowed' : ''
}`}
title={currentFolder === 'Diary' || currentFolder === 'Health' ? 'Le titre est automatiquement la date du jour pour les journaux' : ''}
/>
</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 overflow-y-auto">
{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>
) : currentFolder === 'Health' ? (
// Use HealthForm for Health folder
<HealthForm
content={content}
onContentChange={handleHealthContentChange}
date={note?.lastModified}
/>
) : (
<div className="p-4">
<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"
style={{ minHeight: '400px' }}
/>
</div>
)}
</div>
{isSaving && (
<div className="absolute bottom-4 right-4 text-sm text-carnet-text-muted">
Enregistrement...
</div>
)}
</div>
);
};