refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client

This commit is contained in:
Sepehr Ramezani
2026-04-19 19:21:27 +02:00
parent 5296c4da2c
commit 25529a24b8
2476 changed files with 127934 additions and 101962 deletions

View File

@@ -8,6 +8,7 @@ import { getAIProvider } from '@/lib/ai/factory'
import { parseNote as parseNoteUtil, cosineSimilarity, validateEmbedding, calculateRRFK, detectQueryType, getSearchWeights } from '@/lib/utils'
import { getSystemConfig, getConfigNumber, getConfigBoolean, SEARCH_DEFAULTS } from '@/lib/config'
import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service'
import { cleanupNoteImages, parseImageUrls, deleteImageFileSafely } from '@/lib/image-cleanup'
/**
* Champs sélectionnés pour les listes de notes (sans embedding pour économiser ~6KB/note).
@@ -20,6 +21,7 @@ const NOTE_LIST_SELECT = {
color: true,
isPinned: true,
isArchived: true,
trashedAt: true,
type: true,
dismissedFromRecent: true,
checkItems: true,
@@ -219,6 +221,7 @@ export async function getNotes(includeArchived = false) {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
trashedAt: null,
...(includeArchived ? {} : { isArchived: false }),
},
select: NOTE_LIST_SELECT,
@@ -245,6 +248,7 @@ export async function getNotesWithReminders() {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
trashedAt: null,
isArchived: false,
reminder: { not: null }
},
@@ -286,7 +290,8 @@ export async function getArchivedNotes() {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
isArchived: true
isArchived: true,
trashedAt: null
},
select: NOTE_LIST_SELECT,
orderBy: { updatedAt: 'desc' }
@@ -321,6 +326,7 @@ export async function searchNotes(query: string, useSemantic: boolean = false, n
where: {
userId: session.user.id,
isArchived: false,
trashedAt: null,
OR: [
{ title: { contains: query } },
{ content: { contains: query } },
@@ -349,6 +355,7 @@ async function semanticSearch(query: string, userId: string, notebookId?: string
where: {
userId: userId,
isArchived: false,
trashedAt: null,
...(notebookId !== undefined ? { notebookId } : {})
},
include: { noteEmbedding: true }
@@ -650,17 +657,16 @@ export async function updateNote(id: string, data: {
}
}
// Delete a note
// Soft-delete a note (move to trash)
export async function deleteNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
await prisma.note.delete({ where: { id, userId: session.user.id } })
// Sync labels with empty array to trigger cleanup of any orphans
// The syncLabels function will scan all remaining notes and clean up unused labels
await syncLabels(session.user.id, [])
await prisma.note.update({
where: { id, userId: session.user.id },
data: { trashedAt: new Date() }
})
revalidatePath('/')
return { success: true }
@@ -670,6 +676,192 @@ export async function deleteNote(id: string) {
}
}
// Trash actions
export async function trashNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
await prisma.note.update({
where: { id, userId: session.user.id },
data: { trashedAt: new Date() }
})
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error trashing note:', error)
throw new Error('Failed to trash note')
}
}
export async function restoreNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
await prisma.note.update({
where: { id, userId: session.user.id },
data: { trashedAt: null }
})
revalidatePath('/')
revalidatePath('/trash')
return { success: true }
} catch (error) {
console.error('Error restoring note:', error)
throw new Error('Failed to restore note')
}
}
export async function getTrashedNotes() {
const session = await auth();
if (!session?.user?.id) return [];
try {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
trashedAt: { not: null }
},
select: NOTE_LIST_SELECT,
orderBy: { trashedAt: 'desc' }
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching trashed notes:', error)
return []
}
}
export async function permanentDeleteNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
// Fetch images before deleting so we can clean up files
const note = await prisma.note.findUnique({
where: { id, userId: session.user.id },
select: { images: true }
})
const imageUrls = parseImageUrls(note?.images ?? null)
await prisma.note.delete({ where: { id, userId: session.user.id } })
// Clean up orphaned image files (safe: skips if referenced by other notes)
if (imageUrls.length > 0) {
await cleanupNoteImages(id, imageUrls)
}
await syncLabels(session.user.id, [])
revalidatePath('/trash')
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error permanently deleting note:', error)
throw new Error('Failed to permanently delete note')
}
}
export async function emptyTrash() {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
// Fetch trashed notes with images before deleting
const trashedNotes = await prisma.note.findMany({
where: {
userId: session.user.id,
trashedAt: { not: null }
},
select: { id: true, images: true }
})
await prisma.note.deleteMany({
where: {
userId: session.user.id,
trashedAt: { not: null }
}
})
// Clean up image files for all deleted notes
for (const note of trashedNotes) {
const imageUrls = parseImageUrls(note.images)
if (imageUrls.length > 0) {
await cleanupNoteImages(note.id, imageUrls)
}
}
await syncLabels(session.user.id, [])
revalidatePath('/trash')
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error emptying trash:', error)
throw new Error('Failed to empty trash')
}
}
export async function removeImageFromNote(noteId: string, imageIndex: number) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
try {
const note = await prisma.note.findUnique({
where: { id: noteId, userId: session.user.id },
select: { images: true },
})
if (!note) throw new Error('Note not found')
const imageUrls = parseImageUrls(note.images)
if (imageIndex < 0 || imageIndex >= imageUrls.length) throw new Error('Invalid image index')
const removedUrl = imageUrls[imageIndex]
const newImages = imageUrls.filter((_, i) => i !== imageIndex)
await prisma.note.update({
where: { id: noteId },
data: { images: newImages.length > 0 ? JSON.stringify(newImages) : null },
})
// Clean up file if no other note references it
await deleteImageFileSafely(removedUrl, noteId)
return { success: true }
} catch (error) {
console.error('Error removing image:', error)
throw new Error('Failed to remove image')
}
}
export async function cleanupOrphanedImages(imageUrls: string[], noteId: string) {
const session = await auth()
if (!session?.user?.id) return
try {
for (const url of imageUrls) {
await deleteImageFileSafely(url, noteId)
}
} catch {
// Silent — best-effort cleanup
}
}
export async function getTrashCount() {
const session = await auth();
if (!session?.user?.id) return 0;
try {
return await prisma.note.count({
where: {
userId: session.user.id,
trashedAt: { not: null }
}
})
} catch {
return 0
}
}
// Toggle functions
export async function togglePin(id: string, isPinned: boolean) { return updateNote(id, { isPinned }) }
export async function toggleArchive(id: string, isArchived: boolean) { return updateNote(id, { isArchived }) }
@@ -710,7 +902,7 @@ export async function reorderNotes(draggedId: string, targetId: string) {
const targetNote = await prisma.note.findUnique({ where: { id: targetId, userId: session.user.id } })
if (!draggedNote || !targetNote) throw new Error('Notes not found')
const allNotes = await prisma.note.findMany({
where: { userId: session.user.id, isPinned: draggedNote.isPinned, isArchived: false },
where: { userId: session.user.id, isPinned: draggedNote.isPinned, isArchived: false, trashedAt: null },
orderBy: { order: 'asc' }
})
const reorderedNotes = allNotes.filter((n: any) => n.id !== draggedId)
@@ -865,11 +1057,12 @@ export async function syncAllEmbeddings() {
const userId = session.user.id;
let updatedCount = 0;
try {
const notesToSync = await prisma.note.findMany({
where: {
userId,
const notesToSync = await prisma.note.findMany({
where: {
userId,
trashedAt: null,
noteEmbedding: { is: null }
}
}
})
const provider = getAIProvider(await getSystemConfig());
for (const note of notesToSync) {
@@ -905,6 +1098,7 @@ export async function getAllNotes(includeArchived = false) {
prisma.note.findMany({
where: {
userId,
trashedAt: null,
...(includeArchived ? {} : { isArchived: false }),
},
select: NOTE_LIST_SELECT,
@@ -923,6 +1117,7 @@ export async function getAllNotes(includeArchived = false) {
const sharedNotes = acceptedShares
.map(share => share.note)
.filter(note => includeArchived || !note.isArchived)
.map(note => ({ ...note, _isShared: true }))
return [...ownNotes.map(parseNote), ...sharedNotes.map(parseNote)]
} catch (error) {
@@ -944,6 +1139,7 @@ export async function getPinnedNotes(notebookId?: string) {
userId: userId,
isPinned: true,
isArchived: false,
trashedAt: null,
...(notebookId !== undefined ? { notebookId } : {})
},
orderBy: [
@@ -977,6 +1173,7 @@ export async function getRecentNotes(limit: number = 3) {
userId: userId,
contentUpdatedAt: { gte: sevenDaysAgo },
isArchived: false,
trashedAt: null,
dismissedFromRecent: false // Filter out dismissed notes
},
orderBy: { contentUpdatedAt: 'desc' },
@@ -1118,8 +1315,20 @@ export async function getNoteCollaborators(noteId: string) {
throw new Error('Note not found')
}
// Owner can always see collaborators
// Shared users can also see collaborators if they have accepted access
if (note.userId !== session.user.id) {
throw new Error('You do not have access to this note')
const share = await prisma.noteShare.findUnique({
where: {
noteId_userId: {
noteId,
userId: session.user.id
}
}
})
if (!share || share.status !== 'accepted') {
throw new Error('You do not have access to this note')
}
}
// Get all users who have been shared this note (any status)