feat: add notes feature and keyboard shortcuts
- Introduced a new Note model in the Prisma schema to support note-taking functionality. - Updated the HeaderNavigation component to include a link to the new Notes page. - Implemented keyboard shortcuts for note actions, enhancing user experience and productivity. - Added dependencies for markdown rendering and formatting tools to support note content.
This commit is contained in:
272
src/services/notes.ts
Normal file
272
src/services/notes.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { prisma } from '@/services/core/database';
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface CreateNoteData {
|
||||
title: string;
|
||||
content: string;
|
||||
userId: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateNoteData {
|
||||
title?: string;
|
||||
content?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Service pour la gestion des notes markdown
|
||||
*/
|
||||
export class NotesService {
|
||||
/**
|
||||
* Récupère toutes les notes d'un utilisateur
|
||||
*/
|
||||
async getNotes(userId: string): Promise<Note[]> {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
|
||||
return notes.map((note) => ({
|
||||
...note,
|
||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une note par son ID
|
||||
*/
|
||||
async getNoteById(noteId: string, userId: string): Promise<Note | null> {
|
||||
const note = await prisma.note.findFirst({
|
||||
where: {
|
||||
id: noteId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!note) return null;
|
||||
|
||||
return {
|
||||
...note,
|
||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle note
|
||||
*/
|
||||
async createNote(data: CreateNoteData): Promise<Note> {
|
||||
const note = await prisma.note.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
content: data.content,
|
||||
userId: data.userId,
|
||||
noteTags: data.tags
|
||||
? {
|
||||
create: data.tags.map((tagName) => ({
|
||||
tag: {
|
||||
connectOrCreate: {
|
||||
where: { name: tagName },
|
||||
create: { name: tagName },
|
||||
},
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...note,
|
||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une note existante
|
||||
*/
|
||||
async updateNote(
|
||||
noteId: string,
|
||||
userId: string,
|
||||
data: UpdateNoteData
|
||||
): Promise<Note> {
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const existingNote = await prisma.note.findFirst({
|
||||
where: {
|
||||
id: noteId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingNote) {
|
||||
throw new Error('Note not found or access denied');
|
||||
}
|
||||
|
||||
// Préparer les données de mise à jour
|
||||
const updateData: {
|
||||
updatedAt: Date;
|
||||
title?: string;
|
||||
content?: string;
|
||||
noteTags?: {
|
||||
deleteMany: Record<string, never>;
|
||||
create: Array<{
|
||||
tag: {
|
||||
connectOrCreate: {
|
||||
where: { name: string };
|
||||
create: { name: string };
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
} = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Ajouter les champs de base s'ils sont fournis
|
||||
if (data.title !== undefined) {
|
||||
updateData.title = data.title;
|
||||
}
|
||||
if (data.content !== undefined) {
|
||||
updateData.content = data.content;
|
||||
}
|
||||
|
||||
// Gérer les tags si fournis
|
||||
if (data.tags !== undefined) {
|
||||
updateData.noteTags = {
|
||||
deleteMany: {}, // Supprimer tous les tags existants
|
||||
create: data.tags.map((tagName) => ({
|
||||
tag: {
|
||||
connectOrCreate: {
|
||||
where: { name: tagName },
|
||||
create: { name: tagName },
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const note = await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: updateData,
|
||||
include: {
|
||||
noteTags: {
|
||||
include: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...note,
|
||||
tags: note.noteTags.map((nt) => nt.tag.name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une note
|
||||
*/
|
||||
async deleteNote(noteId: string, userId: string): Promise<void> {
|
||||
// Vérifier que la note appartient à l'utilisateur
|
||||
const existingNote = await prisma.note.findFirst({
|
||||
where: {
|
||||
id: noteId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingNote) {
|
||||
throw new Error('Note not found or access denied');
|
||||
}
|
||||
|
||||
await prisma.note.delete({
|
||||
where: { id: noteId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche des notes par titre ou contenu
|
||||
*/
|
||||
async searchNotes(userId: string, query: string): Promise<Note[]> {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId,
|
||||
OR: [{ title: { contains: query } }, { content: { contains: query } }],
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des notes d'un utilisateur
|
||||
*/
|
||||
async getNotesStats(userId: string): Promise<{
|
||||
totalNotes: number;
|
||||
totalWords: number;
|
||||
lastUpdated: Date | null;
|
||||
}> {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
content: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
const totalNotes = notes.length;
|
||||
const totalWords = notes.reduce((acc, note) => {
|
||||
return (
|
||||
acc + note.content.split(/\s+/).filter((word) => word.length > 0).length
|
||||
);
|
||||
}, 0);
|
||||
const lastUpdated =
|
||||
notes.length > 0
|
||||
? notes.reduce(
|
||||
(latest, note) =>
|
||||
note.updatedAt > latest ? note.updatedAt : latest,
|
||||
notes[0].updatedAt
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
totalNotes,
|
||||
totalWords,
|
||||
lastUpdated,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const notesService = new NotesService();
|
||||
Reference in New Issue
Block a user