From 75d31e86ac87034bf6db2be62695535f4ceafff3 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 12 Jan 2026 10:52:44 +0100 Subject: [PATCH] feat(Notes): add favorite functionality to notes, allowing users to toggle favorites and filter notes accordingly --- .../migration.sql | 3 + prisma/schema.prisma | 1 + src/actions/notes.ts | 32 +++++++++ src/app/api/notes/[id]/route.ts | 3 +- src/app/notes/NotesPageClient.tsx | 42 +++++++++--- src/clients/notes.ts | 1 + src/components/notes/FoldersSidebar.tsx | 21 ++++++ src/components/notes/NotesList.tsx | 66 +++++++++++++++---- src/services/notes.ts | 9 +++ 9 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 prisma/migrations/20260110000000_add_note_favorite/migration.sql diff --git a/prisma/migrations/20260110000000_add_note_favorite/migration.sql b/prisma/migrations/20260110000000_add_note_favorite/migration.sql new file mode 100644 index 0000000..4fcd563 --- /dev/null +++ b/prisma/migrations/20260110000000_add_note_favorite/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Note" ADD COLUMN "isFavorite" BOOLEAN NOT NULL DEFAULT 0; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 92501ec..0e9039e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -146,6 +146,7 @@ model Note { taskId String? // Tâche associée à la note folderId String? // Dossier contenant la note order Int @default(0) // Ordre manuel de la note dans son dossier + isFavorite Boolean @default(false) // Note favorite createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) diff --git a/src/actions/notes.ts b/src/actions/notes.ts index 75f9cda..c38a5ff 100644 --- a/src/actions/notes.ts +++ b/src/actions/notes.ts @@ -29,3 +29,35 @@ export async function reorderNotes( }; } } + +/** + * Bascule l'état favori d'une note + */ +export async function toggleNoteFavorite(noteId: string) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + // Récupérer la note actuelle pour connaître son état favori + const currentNote = await notesService.getNoteById(noteId, session.user.id); + if (!currentNote) { + return { success: false, error: 'Note non trouvée' }; + } + + // Basculer l'état favori + const updatedNote = await notesService.updateNote(noteId, session.user.id, { + isFavorite: !currentNote.isFavorite, + }); + + revalidatePath('/notes'); + return { success: true, note: updatedNote }; + } catch (error) { + console.error('Error toggling note favorite:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue', + }; + } +} diff --git a/src/app/api/notes/[id]/route.ts b/src/app/api/notes/[id]/route.ts index 84d0f57..5d09be0 100644 --- a/src/app/api/notes/[id]/route.ts +++ b/src/app/api/notes/[id]/route.ts @@ -53,7 +53,7 @@ export async function PUT( const resolvedParams = await params; const body = await request.json(); - const { title, content, taskId, folderId, tags } = body; + const { title, content, taskId, folderId, isFavorite, tags } = body; const note = await notesService.updateNote( resolvedParams.id, @@ -63,6 +63,7 @@ export async function PUT( content, taskId, folderId, + isFavorite, tags, } ); diff --git a/src/app/notes/NotesPageClient.tsx b/src/app/notes/NotesPageClient.tsx index 85d3acd..00dc89b 100644 --- a/src/app/notes/NotesPageClient.tsx +++ b/src/app/notes/NotesPageClient.tsx @@ -283,11 +283,13 @@ function NotesPageContent({ // 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 + selectedFolderId === '__favorites__' + ? notes.filter((note) => note.isFavorite) // Notes favorites + : 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( @@ -296,11 +298,13 @@ function NotesPageContent({ // 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; + folderId === '__favorites__' + ? notes.filter((note) => note.isFavorite) + : folderId === '__uncategorized__' + ? notes.filter((note) => !note.folderId) + : folderId + ? notes.filter((note) => note.folderId === folderId) + : notes; if (folderNotes.length > 0) { setSelectedNote(folderNotes[0]); @@ -323,6 +327,20 @@ function NotesPageContent({ } }, []); + // Gérer la mise à jour d'une note (pour les favoris) + const handleNoteUpdate = useCallback( + (updatedNote: Note) => { + setNotes((prev) => + prev.map((note) => (note.id === updatedNote.id ? updatedNote : note)) + ); + // Mettre à jour aussi la note sélectionnée si c'est celle-ci + if (selectedNote?.id === updatedNote.id) { + setSelectedNote(updatedNote); + } + }, + [selectedNote] + ); + // Gérer le drop d'une note sur un dossier const handleNoteDrop = useCallback( async (noteId: string, folderId: string | null) => { @@ -403,6 +421,9 @@ function NotesPageContent({ onFoldersChange={handleFoldersChange} availableTags={availableTags} onNoteDrop={handleNoteDrop} + favoritesCount={ + notes.filter((note) => note.isFavorite).length + } /> @@ -417,6 +438,7 @@ function NotesPageContent({ selectedNoteId={selectedNote?.id} isLoading={false} availableTags={availableTags} + onNoteUpdate={handleNoteUpdate} /> diff --git a/src/clients/notes.ts b/src/clients/notes.ts index 5a1575f..0e2115e 100644 --- a/src/clients/notes.ts +++ b/src/clients/notes.ts @@ -14,6 +14,7 @@ export interface UpdateNoteData { content?: string; taskId?: string; // Tâche associée à la note folderId?: string | null; // Dossier contenant la note (null pour retirer du dossier) + isFavorite?: boolean; // Note favorite tags?: string[]; } diff --git a/src/components/notes/FoldersSidebar.tsx b/src/components/notes/FoldersSidebar.tsx index 2a822f0..6ab78ec 100644 --- a/src/components/notes/FoldersSidebar.tsx +++ b/src/components/notes/FoldersSidebar.tsx @@ -11,6 +11,7 @@ import { Trash2, ChevronRight, ChevronDown, + Star, } from 'lucide-react'; import { createFolder, updateFolder, deleteFolder } from '@/actions/folders'; import { extractEmojis } from '@/lib/task-emoji'; @@ -22,6 +23,7 @@ interface FoldersSidebarProps { onFoldersChange: () => void; availableTags: Tag[]; onNoteDrop?: (noteId: string, folderId: string | null) => void; + favoritesCount?: number; } interface FolderItemProps { @@ -195,6 +197,7 @@ export function FoldersSidebar({ onFoldersChange, availableTags, onNoteDrop, + favoritesCount = 0, }: FoldersSidebarProps) { const [isPending, startTransition] = useTransition(); const [showCreateDialog, setShowCreateDialog] = useState(false); @@ -294,6 +297,24 @@ export function FoldersSidebar({ Toutes les notes + {/* Favorites */} +
onSelectFolder('__favorites__')} + > + + Favoris + {favoritesCount > 0 && ( + + {favoritesCount} + + )} +
+ {/* Uncategorized Notes */}
void; } // Extraire le titre du contenu markdown @@ -55,6 +64,7 @@ function SortableNoteItem({ onDelete, showDeleteConfirm, setShowDeleteConfirm, + onNoteUpdate, }: { note: Note; isSelected: boolean; @@ -62,7 +72,19 @@ function SortableNoteItem({ onDelete: (noteId: string, e: React.MouseEvent) => void; showDeleteConfirm: string | null; setShowDeleteConfirm: (id: string | null) => void; + onNoteUpdate?: (note: Note) => void; }) { + const [isPending, startTransition] = useTransition(); + + const handleToggleFavorite = (e: React.MouseEvent) => { + e.stopPropagation(); + startTransition(async () => { + const result = await toggleNoteFavorite(note.id); + if (result.success && result.note && onNoteUpdate) { + onNoteUpdate(result.note); + } + }); + }; const { attributes, listeners, @@ -131,15 +153,35 @@ function SortableNoteItem({
) : ( - +
+ + +
)}
@@ -179,6 +221,7 @@ export function NotesList({ onReorderNotes, selectedNoteId, isLoading = false, + onNoteUpdate, }: NotesListProps) { const [searchQuery, setSearchQuery] = useState(''); const [showDeleteConfirm, setShowDeleteConfirm] = useState( @@ -282,6 +325,7 @@ export function NotesList({ onDelete={handleDeleteConfirm} showDeleteConfirm={showDeleteConfirm} setShowDeleteConfirm={setShowDeleteConfirm} + onNoteUpdate={onNoteUpdate} /> ))} diff --git a/src/services/notes.ts b/src/services/notes.ts index 5f00b90..28e84e7 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -10,6 +10,7 @@ export interface Note { userId: string; taskId?: string; // Tâche associée à la note folderId?: string; // Dossier contenant la note + isFavorite?: boolean; // Note favorite task?: Task | null; // Objet Task complet createdAt: Date; updatedAt: Date; @@ -30,6 +31,7 @@ export interface UpdateNoteData { content?: string; taskId?: string; // Tâche associée à la note folderId?: string; // Dossier contenant la note + isFavorite?: boolean; // Note favorite tags?: string[]; } @@ -130,6 +132,7 @@ export class NotesService { ...note, taskId: note.taskId || undefined, // Convertir null en undefined folderId: note.folderId || undefined, // Convertir null en undefined + isFavorite: note.isFavorite || false, // Convertir null en false task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task tags: note.noteTags.map((nt) => nt.tag.name), })); @@ -169,6 +172,7 @@ export class NotesService { ...note, taskId: note.taskId || undefined, // Convertir null en undefined folderId: note.folderId || undefined, // Convertir null en undefined + isFavorite: note.isFavorite || false, // Convertir null en false task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task tags: note.noteTags.map((nt) => nt.tag.name), }; @@ -272,6 +276,7 @@ export class NotesService { content?: string; taskId?: string | null; folderId?: string | null; + isFavorite?: boolean; noteTags?: { deleteMany: Record; create: Array<{ @@ -300,6 +305,9 @@ export class NotesService { if (data.folderId !== undefined) { updateData.folderId = data.folderId || null; } + if (data.isFavorite !== undefined) { + updateData.isFavorite = data.isFavorite; + } // Gérer les tags si fournis if (data.tags !== undefined) { @@ -354,6 +362,7 @@ export class NotesService { ...noteWithTags!, taskId: noteWithTags!.taskId || undefined, // Convertir null en undefined folderId: noteWithTags!.folderId || undefined, // Convertir null en undefined + isFavorite: noteWithTags!.isFavorite || false, // Convertir null en false task: this.mapPrismaTaskToTask(noteWithTags!.task), // Mapper correctement l'objet Task tags: noteWithTags!.noteTags.map((nt) => nt.tag.name), };