feat(Notes): add favorite functionality to notes, allowing users to toggle favorites and filter notes accordingly
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Note" ADD COLUMN "isFavorite" BOOLEAN NOT NULL DEFAULT 0;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -417,6 +438,7 @@ function NotesPageContent({
|
||||
selectedNoteId={selectedNote?.id}
|
||||
isLoading={false}
|
||||
availableTags={availableTags}
|
||||
onNoteUpdate={handleNoteUpdate}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
<span className="flex-1 text-sm">Toutes les notes</span>
|
||||
</div>
|
||||
|
||||
{/* Favorites */}
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all duration-200 mb-1 ${
|
||||
selectedFolderId === '__favorites__'
|
||||
? 'bg-[var(--accent)]/10 text-[var(--accent)] border-l-2 border-[var(--accent)]'
|
||||
: 'hover:bg-[var(--card)]/60 text-[var(--foreground)]'
|
||||
}`}
|
||||
onClick={() => onSelectFolder('__favorites__')}
|
||||
>
|
||||
<Star className="w-4 h-4 flex-shrink-0 fill-current" />
|
||||
<span className="flex-1 text-sm">Favoris</span>
|
||||
{favoritesCount > 0 && (
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
{favoritesCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Uncategorized Notes */}
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all duration-200 mb-2 ${
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
import { Note } from '@/services/notes';
|
||||
import { Search, Plus, Calendar, Trash2, GripVertical } from 'lucide-react';
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Calendar,
|
||||
Trash2,
|
||||
GripVertical,
|
||||
Star,
|
||||
} from 'lucide-react';
|
||||
import { toggleNoteFavorite } from '@/actions/notes';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { Tag } from '@/lib/types';
|
||||
@@ -33,6 +41,7 @@ interface NotesListProps {
|
||||
selectedNoteId?: string;
|
||||
isLoading?: boolean;
|
||||
availableTags?: Tag[];
|
||||
onNoteUpdate?: (note: Note) => 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({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowDeleteConfirm(note.id);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded-md hover:bg-[var(--destructive)]/20 text-[var(--destructive)] transition-opacity"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={handleToggleFavorite}
|
||||
disabled={isPending}
|
||||
className={`p-1 rounded-md transition-colors ${
|
||||
note.isFavorite
|
||||
? 'text-[var(--accent)] bg-[var(--accent)]/20'
|
||||
: 'hover:bg-[var(--accent)]/20 text-[var(--muted-foreground)] hover:text-[var(--accent)]'
|
||||
}`}
|
||||
title={
|
||||
note.isFavorite
|
||||
? 'Retirer des favoris'
|
||||
: 'Ajouter aux favoris'
|
||||
}
|
||||
>
|
||||
<Star
|
||||
className={`w-3 h-3 ${note.isFavorite ? 'fill-current' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowDeleteConfirm(note.id);
|
||||
}}
|
||||
className="p-1 rounded-md hover:bg-[var(--destructive)]/20 text-[var(--destructive)]"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 text-xs text-[var(--muted-foreground)]">
|
||||
@@ -179,6 +221,7 @@ export function NotesList({
|
||||
onReorderNotes,
|
||||
selectedNoteId,
|
||||
isLoading = false,
|
||||
onNoteUpdate,
|
||||
}: NotesListProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(
|
||||
@@ -282,6 +325,7 @@ export function NotesList({
|
||||
onDelete={handleDeleteConfirm}
|
||||
showDeleteConfirm={showDeleteConfirm}
|
||||
setShowDeleteConfirm={setShowDeleteConfirm}
|
||||
onNoteUpdate={onNoteUpdate}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
@@ -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<string, never>;
|
||||
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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user