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:
Julien Froidefond
2025-10-09 13:38:09 +02:00
parent 1fe59f26e4
commit 6c86ce44f1
15 changed files with 4354 additions and 96 deletions

272
src/services/notes.ts Normal file
View 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();