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
|
taskId String? // Tâche associée à la note
|
||||||
folderId String? // Dossier contenant la note
|
folderId String? // Dossier contenant la note
|
||||||
order Int @default(0) // Ordre manuel de la note dans son dossier
|
order Int @default(0) // Ordre manuel de la note dans son dossier
|
||||||
|
isFavorite Boolean @default(false) // Note favorite
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
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 resolvedParams = await params;
|
||||||
const body = await request.json();
|
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(
|
const note = await notesService.updateNote(
|
||||||
resolvedParams.id,
|
resolvedParams.id,
|
||||||
@@ -63,6 +63,7 @@ export async function PUT(
|
|||||||
content,
|
content,
|
||||||
taskId,
|
taskId,
|
||||||
folderId,
|
folderId,
|
||||||
|
isFavorite,
|
||||||
tags,
|
tags,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -283,11 +283,13 @@ function NotesPageContent({
|
|||||||
|
|
||||||
// Filtrer les notes par dossier
|
// Filtrer les notes par dossier
|
||||||
const filteredNotes =
|
const filteredNotes =
|
||||||
selectedFolderId === '__uncategorized__'
|
selectedFolderId === '__favorites__'
|
||||||
? notes.filter((note) => !note.folderId) // Notes sans dossier
|
? notes.filter((note) => note.isFavorite) // Notes favorites
|
||||||
: selectedFolderId
|
: selectedFolderId === '__uncategorized__'
|
||||||
? notes.filter((note) => note.folderId === selectedFolderId)
|
? notes.filter((note) => !note.folderId) // Notes sans dossier
|
||||||
: notes; // Toutes les notes
|
: selectedFolderId
|
||||||
|
? notes.filter((note) => note.folderId === selectedFolderId)
|
||||||
|
: notes; // Toutes les notes
|
||||||
|
|
||||||
// Gérer le changement de dossier
|
// Gérer le changement de dossier
|
||||||
const handleSelectFolder = useCallback(
|
const handleSelectFolder = useCallback(
|
||||||
@@ -296,11 +298,13 @@ function NotesPageContent({
|
|||||||
|
|
||||||
// Sélectionner automatiquement la première note du dossier
|
// Sélectionner automatiquement la première note du dossier
|
||||||
const folderNotes =
|
const folderNotes =
|
||||||
folderId === '__uncategorized__'
|
folderId === '__favorites__'
|
||||||
? notes.filter((note) => !note.folderId)
|
? notes.filter((note) => note.isFavorite)
|
||||||
: folderId
|
: folderId === '__uncategorized__'
|
||||||
? notes.filter((note) => note.folderId === folderId)
|
? notes.filter((note) => !note.folderId)
|
||||||
: notes;
|
: folderId
|
||||||
|
? notes.filter((note) => note.folderId === folderId)
|
||||||
|
: notes;
|
||||||
|
|
||||||
if (folderNotes.length > 0) {
|
if (folderNotes.length > 0) {
|
||||||
setSelectedNote(folderNotes[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
|
// Gérer le drop d'une note sur un dossier
|
||||||
const handleNoteDrop = useCallback(
|
const handleNoteDrop = useCallback(
|
||||||
async (noteId: string, folderId: string | null) => {
|
async (noteId: string, folderId: string | null) => {
|
||||||
@@ -403,6 +421,9 @@ function NotesPageContent({
|
|||||||
onFoldersChange={handleFoldersChange}
|
onFoldersChange={handleFoldersChange}
|
||||||
availableTags={availableTags}
|
availableTags={availableTags}
|
||||||
onNoteDrop={handleNoteDrop}
|
onNoteDrop={handleNoteDrop}
|
||||||
|
favoritesCount={
|
||||||
|
notes.filter((note) => note.isFavorite).length
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -417,6 +438,7 @@ function NotesPageContent({
|
|||||||
selectedNoteId={selectedNote?.id}
|
selectedNoteId={selectedNote?.id}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
availableTags={availableTags}
|
availableTags={availableTags}
|
||||||
|
onNoteUpdate={handleNoteUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface UpdateNoteData {
|
|||||||
content?: string;
|
content?: string;
|
||||||
taskId?: string; // Tâche associée à la note
|
taskId?: string; // Tâche associée à la note
|
||||||
folderId?: string | null; // Dossier contenant la note (null pour retirer du dossier)
|
folderId?: string | null; // Dossier contenant la note (null pour retirer du dossier)
|
||||||
|
isFavorite?: boolean; // Note favorite
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
Star,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { createFolder, updateFolder, deleteFolder } from '@/actions/folders';
|
import { createFolder, updateFolder, deleteFolder } from '@/actions/folders';
|
||||||
import { extractEmojis } from '@/lib/task-emoji';
|
import { extractEmojis } from '@/lib/task-emoji';
|
||||||
@@ -22,6 +23,7 @@ interface FoldersSidebarProps {
|
|||||||
onFoldersChange: () => void;
|
onFoldersChange: () => void;
|
||||||
availableTags: Tag[];
|
availableTags: Tag[];
|
||||||
onNoteDrop?: (noteId: string, folderId: string | null) => void;
|
onNoteDrop?: (noteId: string, folderId: string | null) => void;
|
||||||
|
favoritesCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FolderItemProps {
|
interface FolderItemProps {
|
||||||
@@ -195,6 +197,7 @@ export function FoldersSidebar({
|
|||||||
onFoldersChange,
|
onFoldersChange,
|
||||||
availableTags,
|
availableTags,
|
||||||
onNoteDrop,
|
onNoteDrop,
|
||||||
|
favoritesCount = 0,
|
||||||
}: FoldersSidebarProps) {
|
}: FoldersSidebarProps) {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
@@ -294,6 +297,24 @@ export function FoldersSidebar({
|
|||||||
<span className="flex-1 text-sm">Toutes les notes</span>
|
<span className="flex-1 text-sm">Toutes les notes</span>
|
||||||
</div>
|
</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 */}
|
{/* Uncategorized Notes */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-all duration-200 mb-2 ${
|
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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import { Note } from '@/services/notes';
|
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 { formatDistanceToNow } from 'date-fns';
|
||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
import { Tag } from '@/lib/types';
|
import { Tag } from '@/lib/types';
|
||||||
@@ -33,6 +41,7 @@ interface NotesListProps {
|
|||||||
selectedNoteId?: string;
|
selectedNoteId?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
availableTags?: Tag[];
|
availableTags?: Tag[];
|
||||||
|
onNoteUpdate?: (note: Note) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extraire le titre du contenu markdown
|
// Extraire le titre du contenu markdown
|
||||||
@@ -55,6 +64,7 @@ function SortableNoteItem({
|
|||||||
onDelete,
|
onDelete,
|
||||||
showDeleteConfirm,
|
showDeleteConfirm,
|
||||||
setShowDeleteConfirm,
|
setShowDeleteConfirm,
|
||||||
|
onNoteUpdate,
|
||||||
}: {
|
}: {
|
||||||
note: Note;
|
note: Note;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
@@ -62,7 +72,19 @@ function SortableNoteItem({
|
|||||||
onDelete: (noteId: string, e: React.MouseEvent) => void;
|
onDelete: (noteId: string, e: React.MouseEvent) => void;
|
||||||
showDeleteConfirm: string | null;
|
showDeleteConfirm: string | null;
|
||||||
setShowDeleteConfirm: (id: string | null) => void;
|
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 {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -131,15 +153,35 @@ function SortableNoteItem({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation();
|
onClick={handleToggleFavorite}
|
||||||
setShowDeleteConfirm(note.id);
|
disabled={isPending}
|
||||||
}}
|
className={`p-1 rounded-md transition-colors ${
|
||||||
className="opacity-0 group-hover:opacity-100 p-1 rounded-md hover:bg-[var(--destructive)]/20 text-[var(--destructive)] transition-opacity"
|
note.isFavorite
|
||||||
>
|
? 'text-[var(--accent)] bg-[var(--accent)]/20'
|
||||||
<Trash2 className="w-3 h-3" />
|
: 'hover:bg-[var(--accent)]/20 text-[var(--muted-foreground)] hover:text-[var(--accent)]'
|
||||||
</button>
|
}`}
|
||||||
|
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>
|
||||||
<div className="flex items-center justify-between gap-2 text-xs text-[var(--muted-foreground)]">
|
<div className="flex items-center justify-between gap-2 text-xs text-[var(--muted-foreground)]">
|
||||||
@@ -179,6 +221,7 @@ export function NotesList({
|
|||||||
onReorderNotes,
|
onReorderNotes,
|
||||||
selectedNoteId,
|
selectedNoteId,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
onNoteUpdate,
|
||||||
}: NotesListProps) {
|
}: NotesListProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(
|
||||||
@@ -282,6 +325,7 @@ export function NotesList({
|
|||||||
onDelete={handleDeleteConfirm}
|
onDelete={handleDeleteConfirm}
|
||||||
showDeleteConfirm={showDeleteConfirm}
|
showDeleteConfirm={showDeleteConfirm}
|
||||||
setShowDeleteConfirm={setShowDeleteConfirm}
|
setShowDeleteConfirm={setShowDeleteConfirm}
|
||||||
|
onNoteUpdate={onNoteUpdate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface Note {
|
|||||||
userId: string;
|
userId: string;
|
||||||
taskId?: string; // Tâche associée à la note
|
taskId?: string; // Tâche associée à la note
|
||||||
folderId?: string; // Dossier contenant la note
|
folderId?: string; // Dossier contenant la note
|
||||||
|
isFavorite?: boolean; // Note favorite
|
||||||
task?: Task | null; // Objet Task complet
|
task?: Task | null; // Objet Task complet
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -30,6 +31,7 @@ export interface UpdateNoteData {
|
|||||||
content?: string;
|
content?: string;
|
||||||
taskId?: string; // Tâche associée à la note
|
taskId?: string; // Tâche associée à la note
|
||||||
folderId?: string; // Dossier contenant la note
|
folderId?: string; // Dossier contenant la note
|
||||||
|
isFavorite?: boolean; // Note favorite
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +132,7 @@ export class NotesService {
|
|||||||
...note,
|
...note,
|
||||||
taskId: note.taskId || undefined, // Convertir null en undefined
|
taskId: note.taskId || undefined, // Convertir null en undefined
|
||||||
folderId: note.folderId || 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
|
task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task
|
||||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||||
}));
|
}));
|
||||||
@@ -169,6 +172,7 @@ export class NotesService {
|
|||||||
...note,
|
...note,
|
||||||
taskId: note.taskId || undefined, // Convertir null en undefined
|
taskId: note.taskId || undefined, // Convertir null en undefined
|
||||||
folderId: note.folderId || 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
|
task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task
|
||||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||||
};
|
};
|
||||||
@@ -272,6 +276,7 @@ export class NotesService {
|
|||||||
content?: string;
|
content?: string;
|
||||||
taskId?: string | null;
|
taskId?: string | null;
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
|
isFavorite?: boolean;
|
||||||
noteTags?: {
|
noteTags?: {
|
||||||
deleteMany: Record<string, never>;
|
deleteMany: Record<string, never>;
|
||||||
create: Array<{
|
create: Array<{
|
||||||
@@ -300,6 +305,9 @@ export class NotesService {
|
|||||||
if (data.folderId !== undefined) {
|
if (data.folderId !== undefined) {
|
||||||
updateData.folderId = data.folderId || null;
|
updateData.folderId = data.folderId || null;
|
||||||
}
|
}
|
||||||
|
if (data.isFavorite !== undefined) {
|
||||||
|
updateData.isFavorite = data.isFavorite;
|
||||||
|
}
|
||||||
|
|
||||||
// Gérer les tags si fournis
|
// Gérer les tags si fournis
|
||||||
if (data.tags !== undefined) {
|
if (data.tags !== undefined) {
|
||||||
@@ -354,6 +362,7 @@ export class NotesService {
|
|||||||
...noteWithTags!,
|
...noteWithTags!,
|
||||||
taskId: noteWithTags!.taskId || undefined, // Convertir null en undefined
|
taskId: noteWithTags!.taskId || undefined, // Convertir null en undefined
|
||||||
folderId: noteWithTags!.folderId || 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
|
task: this.mapPrismaTaskToTask(noteWithTags!.task), // Mapper correctement l'objet Task
|
||||||
tags: noteWithTags!.noteTags.map((nt) => nt.tag.name),
|
tags: noteWithTags!.noteTags.map((nt) => nt.tag.name),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user