feat(Notes): add folder management to notes, allowing notes to be categorized into folders, and update related components for folder selection and display
This commit is contained in:
@@ -2,24 +2,36 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Note } from '@/services/notes';
|
||||
import { Folder } from '@/services/folders';
|
||||
import { Task } from '@/lib/types';
|
||||
import { notesClient } from '@/clients/notes';
|
||||
import { NotesList } from '@/components/notes/NotesList';
|
||||
import { MarkdownEditor } from '@/components/notes/MarkdownEditor';
|
||||
import { FoldersSidebar } from '@/components/notes/FoldersSidebar';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { Card } from '@/components/ui';
|
||||
import { TasksProvider, useTasksContext } from '@/contexts/TasksContext';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { FileText, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { getFolders } from '@/actions/folders';
|
||||
|
||||
interface NotesPageClientProps {
|
||||
initialNotes: Note[];
|
||||
initialTags: (Tag & { usage: number })[];
|
||||
initialFolders: Folder[];
|
||||
}
|
||||
|
||||
function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
function NotesPageContent({
|
||||
initialNotes,
|
||||
initialFolders,
|
||||
}: {
|
||||
initialNotes: Note[];
|
||||
initialFolders: Folder[];
|
||||
}) {
|
||||
const [notes, setNotes] = useState<Note[]>(initialNotes);
|
||||
const [folders, setFolders] = useState<Folder[]>(initialFolders);
|
||||
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const { tags: availableTags } = useTasksContext();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -58,6 +70,7 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
const newNote = await notesClient.createNote({
|
||||
title: 'Nouvelle note',
|
||||
content: '# Nouvelle note\n\nCommencez à écrire...',
|
||||
folderId: selectedFolderId || undefined,
|
||||
});
|
||||
|
||||
setNotes((prev) => [newNote, ...prev]);
|
||||
@@ -68,7 +81,7 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
setError('Erreur lors de la création de la note');
|
||||
console.error('Error creating note:', err);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedFolderId]);
|
||||
|
||||
const handleDeleteNote = useCallback(
|
||||
async (noteId: string) => {
|
||||
@@ -123,6 +136,7 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
content: selectedNote.content,
|
||||
tags: selectedNote.tags,
|
||||
taskId: selectedNote.taskId,
|
||||
folderId: selectedNote.folderId,
|
||||
});
|
||||
|
||||
// Mettre à jour la liste des notes mais pas selectedNote pour éviter la perte de focus
|
||||
@@ -162,6 +176,44 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
[selectedNote]
|
||||
);
|
||||
|
||||
const handleFolderChange = useCallback(
|
||||
async (folderId: string | null) => {
|
||||
if (!selectedNote) return;
|
||||
|
||||
console.log(
|
||||
'[handleFolderChange] Changing folder for note:',
|
||||
selectedNote.id,
|
||||
'to folder:',
|
||||
folderId
|
||||
);
|
||||
|
||||
try {
|
||||
// Sauvegarder immédiatement
|
||||
const updateData: { folderId: string | null } = {
|
||||
folderId: folderId,
|
||||
};
|
||||
const updatedNote = await notesClient.updateNote(
|
||||
selectedNote.id,
|
||||
updateData
|
||||
);
|
||||
|
||||
console.log('[handleFolderChange] Note updated:', updatedNote);
|
||||
|
||||
// Mettre à jour la liste des notes
|
||||
setNotes((prev) =>
|
||||
prev.map((note) => (note.id === selectedNote.id ? updatedNote : note))
|
||||
);
|
||||
|
||||
// Mettre à jour la note sélectionnée
|
||||
setSelectedNote(updatedNote);
|
||||
} catch (err) {
|
||||
console.error('[handleFolderChange] Error:', err);
|
||||
setError('Erreur lors du changement de dossier');
|
||||
}
|
||||
},
|
||||
[selectedNote]
|
||||
);
|
||||
|
||||
// Auto-save quand les tags changent
|
||||
useEffect(() => {
|
||||
if (hasUnsavedChanges && selectedNote) {
|
||||
@@ -173,6 +225,86 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
}
|
||||
}, [selectedNote, hasUnsavedChanges, handleSave]);
|
||||
|
||||
// Filtrer les notes par dossier
|
||||
const filteredNotes =
|
||||
selectedFolderId === '__uncategorized__'
|
||||
? notes.filter((note) => !note.folderId) // Notes sans dossier
|
||||
: selectedFolderId
|
||||
? notes.filter((note) => note.folderId === selectedFolderId)
|
||||
: notes; // Toutes les notes
|
||||
|
||||
// Gérer le changement de dossier
|
||||
const handleSelectFolder = useCallback(
|
||||
(folderId: string | null) => {
|
||||
setSelectedFolderId(folderId);
|
||||
|
||||
// Sélectionner automatiquement la première note du dossier
|
||||
const folderNotes =
|
||||
folderId === '__uncategorized__'
|
||||
? notes.filter((note) => !note.folderId)
|
||||
: folderId
|
||||
? notes.filter((note) => note.folderId === folderId)
|
||||
: notes;
|
||||
|
||||
if (folderNotes.length > 0) {
|
||||
setSelectedNote(folderNotes[0]);
|
||||
} else {
|
||||
setSelectedNote(null);
|
||||
}
|
||||
},
|
||||
[notes]
|
||||
);
|
||||
|
||||
// Recharger les dossiers
|
||||
const handleFoldersChange = useCallback(async () => {
|
||||
try {
|
||||
const result = await getFolders();
|
||||
if (result.success && result.data) {
|
||||
setFolders(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching folders:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Gérer le drop d'une note sur un dossier
|
||||
const handleNoteDrop = useCallback(
|
||||
async (noteId: string, folderId: string | null) => {
|
||||
console.log(
|
||||
'[handleNoteDrop] Dropping note:',
|
||||
noteId,
|
||||
'to folder:',
|
||||
folderId
|
||||
);
|
||||
|
||||
try {
|
||||
const updateData: { folderId: string | null } = {
|
||||
folderId: folderId,
|
||||
};
|
||||
const updatedNote = await notesClient.updateNote(noteId, updateData);
|
||||
|
||||
console.log('[handleNoteDrop] Note updated:', updatedNote);
|
||||
|
||||
// Mettre à jour la liste des notes
|
||||
setNotes((prev) =>
|
||||
prev.map((note) => (note.id === noteId ? updatedNote : note))
|
||||
);
|
||||
|
||||
// Si la note sélectionnée est celle qu'on déplace, la mettre à jour aussi
|
||||
if (selectedNote?.id === noteId) {
|
||||
setSelectedNote(updatedNote);
|
||||
}
|
||||
|
||||
// Recharger les dossiers pour mettre à jour les compteurs
|
||||
handleFoldersChange();
|
||||
} catch (err) {
|
||||
console.error('[handleNoteDrop] Error:', err);
|
||||
setError('Erreur lors du déplacement de la note');
|
||||
}
|
||||
},
|
||||
[selectedNote, handleFoldersChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--background)] flex flex-col">
|
||||
<Header title="Notes" subtitle="Gestionnaire de notes markdown" />
|
||||
@@ -182,9 +314,9 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
variant="glass"
|
||||
className="flex h-full rounded-2xl overflow-hidden"
|
||||
>
|
||||
{/* Notes List Sidebar */}
|
||||
{/* Combined Sidebar: Folders + Notes List */}
|
||||
<div
|
||||
className={`${sidebarCollapsed ? 'w-12' : 'w-80'} flex-shrink-0 border-r border-[var(--border)] transition-all duration-300 ease-in-out`}
|
||||
className={`${sidebarCollapsed ? 'w-12' : 'w-80'} flex-shrink-0 border-r border-[var(--border)] transition-all duration-300 ease-in-out flex flex-col`}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<div className="h-full flex flex-col items-center py-4">
|
||||
@@ -200,6 +332,7 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`${glassDivider} flex items-center justify-between p-3`}
|
||||
>
|
||||
@@ -213,15 +346,31 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<NotesList
|
||||
notes={notes}
|
||||
onSelectNote={handleSelectNote}
|
||||
onCreateNote={handleCreateNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
selectedNoteId={selectedNote?.id}
|
||||
isLoading={false}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
|
||||
{/* Folders Section */}
|
||||
<div className="flex-shrink-0 border-b border-[var(--border)]">
|
||||
<FoldersSidebar
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId || undefined}
|
||||
onSelectFolder={handleSelectFolder}
|
||||
onFoldersChange={handleFoldersChange}
|
||||
availableTags={availableTags}
|
||||
onNoteDrop={handleNoteDrop}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notes List */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<NotesList
|
||||
notes={filteredNotes}
|
||||
onSelectNote={handleSelectNote}
|
||||
onCreateNote={handleCreateNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
selectedNoteId={selectedNote?.id}
|
||||
isLoading={false}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -250,6 +399,47 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
<h2 className="text-lg font-semibold text-[var(--foreground)] truncate">
|
||||
{getNoteTitle(selectedNote.content)}
|
||||
</h2>
|
||||
{selectedNote.folderId &&
|
||||
(() => {
|
||||
const findFolder = (
|
||||
foldersList: typeof folders
|
||||
): (typeof folders)[0] | null => {
|
||||
for (const folder of foldersList) {
|
||||
if (folder.id === selectedNote.folderId)
|
||||
return folder;
|
||||
if (folder.children) {
|
||||
const found = findFolder(folder.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const currentFolder = findFolder(folders);
|
||||
const folderTag = currentFolder?.tagId
|
||||
? availableTags.find(
|
||||
(t) => t.id === currentFolder.tagId
|
||||
)
|
||||
: null;
|
||||
|
||||
return currentFolder ? (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
📁 {currentFolder.name}
|
||||
</span>
|
||||
{folderTag && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs"
|
||||
style={{
|
||||
backgroundColor: `${folderTag.color}20`,
|
||||
color: folderTag.color,
|
||||
}}
|
||||
>
|
||||
<span>{folderTag.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-[var(--muted-foreground)]">
|
||||
{hasUnsavedChanges && (
|
||||
@@ -279,6 +469,9 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
selectedTaskId={selectedNote.taskId}
|
||||
selectedTask={selectedNote.task}
|
||||
onTaskChange={handleTaskChange}
|
||||
selectedFolderId={selectedNote.folderId}
|
||||
availableFolders={folders}
|
||||
onFolderChange={handleFolderChange}
|
||||
onCreateNote={handleCreateNote}
|
||||
onToggleSidebar={() =>
|
||||
setSidebarCollapsed(!sidebarCollapsed)
|
||||
@@ -316,10 +509,14 @@ function NotesPageContent({ initialNotes }: { initialNotes: Note[] }) {
|
||||
export function NotesPageClient({
|
||||
initialNotes,
|
||||
initialTags,
|
||||
initialFolders,
|
||||
}: NotesPageClientProps) {
|
||||
return (
|
||||
<TasksProvider initialTasks={[]} initialTags={initialTags}>
|
||||
<NotesPageContent initialNotes={initialNotes} />
|
||||
<NotesPageContent
|
||||
initialNotes={initialNotes}
|
||||
initialFolders={initialFolders}
|
||||
/>
|
||||
</TasksProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user