sepehr f0b41572bc feat: Memento avec dates, Markdown, reminders et auth
Tests Playwright validés :
- Création de notes: OK
- Modification titre: OK
- Modification contenu: OK
- Markdown éditable avec preview: OK

Fonctionnalités:
- date-fns: dates relatives sur cards
- react-markdown + remark-gfm
- Markdown avec toggle edit/preview
- Recherche améliorée (titre/contenu/labels/checkItems)
- Reminder recurrence/location (schema)
- NextAuth.js + User/Account/Session
- userId dans Note (optionnel)
- 4 migrations créées

Ready for production + auth integration
2026-01-04 16:04:24 +01:00

275 lines
7.0 KiB
TypeScript

'use server'
import { revalidatePath } from 'next/cache'
import prisma from '@/lib/prisma'
import { Note, CheckItem } from '@/lib/types'
// Helper function to parse JSON strings from database
function parseNote(dbNote: any): Note {
return {
...dbNote,
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
}
}
// Get all notes (non-archived by default)
export async function getNotes(includeArchived = false) {
try {
const notes = await prisma.note.findMany({
where: includeArchived ? {} : { isArchived: false },
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' }
]
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching notes:', error)
return []
}
}
// Get archived notes only
export async function getArchivedNotes() {
try {
const notes = await prisma.note.findMany({
where: { isArchived: true },
orderBy: { updatedAt: 'desc' }
})
return notes.map(parseNote)
} catch (error) {
console.error('Error fetching archived notes:', error)
return []
}
}
// Search notes
export async function searchNotes(query: string) {
try {
if (!query.trim()) {
return await getNotes()
}
const notes = await prisma.note.findMany({
where: {
isArchived: false,
OR: [
{ title: { contains: query } },
{ content: { contains: query } },
{ labels: { contains: query } },
{ checkItems: { contains: query } }
]
},
orderBy: [
{ isPinned: 'desc' },
{ updatedAt: 'desc' }
]
})
// Enhanced ranking: prioritize title matches
const rankedNotes = notes.map(note => {
const parsedNote = parseNote(note)
let score = 0
// Title match gets highest score
if (parsedNote.title?.toLowerCase().includes(query.toLowerCase())) {
score += 10
}
// Content match
if (parsedNote.content.toLowerCase().includes(query.toLowerCase())) {
score += 5
}
// Label match
if (parsedNote.labels?.some(label => label.toLowerCase().includes(query.toLowerCase()))) {
score += 3
}
// CheckItems match
if (parsedNote.checkItems?.some(item => item.text.toLowerCase().includes(query.toLowerCase()))) {
score += 2
}
return { note: parsedNote, score }
})
// Sort by score descending, then by existing order (pinned/updated)
return rankedNotes
.sort((a, b) => b.score - a.score)
.map(item => item.note)
} catch (error) {
console.error('Error searching notes:', error)
return []
}
}
// Create a new note
export async function createNote(data: {
title?: string
content: string
color?: string
type?: 'text' | 'checklist'
checkItems?: CheckItem[]
labels?: string[]
images?: string[]
isArchived?: boolean
reminder?: Date | null
isMarkdown?: boolean
}) {
try {
const note = await prisma.note.create({
data: {
title: data.title || null,
content: data.content,
color: data.color || 'default',
type: data.type || 'text',
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
labels: data.labels ? JSON.stringify(data.labels) : null,
images: data.images ? JSON.stringify(data.images) : null,
isArchived: data.isArchived || false,
reminder: data.reminder || null,
isMarkdown: data.isMarkdown || false,
}
})
revalidatePath('/')
return parseNote(note)
} catch (error) {
console.error('Error creating note:', error)
throw new Error('Failed to create note')
}
}
// Update a note
export async function updateNote(id: string, data: {
title?: string | null
content?: string
color?: string
isPinned?: boolean
isArchived?: boolean
type?: 'text' | 'checklist'
checkItems?: CheckItem[] | null
labels?: string[] | null
images?: string[] | null
reminder?: Date | null
isMarkdown?: boolean
}) {
try {
// Stringify JSON fields if they exist
const updateData: any = { ...data }
if ('checkItems' in data) {
updateData.checkItems = data.checkItems ? JSON.stringify(data.checkItems) : null
}
if ('labels' in data) {
updateData.labels = data.labels ? JSON.stringify(data.labels) : null
}
if ('images' in data) {
updateData.images = data.images ? JSON.stringify(data.images) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id },
data: updateData
})
revalidatePath('/')
return parseNote(note)
} catch (error) {
console.error('Error updating note:', error)
throw new Error('Failed to update note')
}
}
// Delete a note
export async function deleteNote(id: string) {
try {
await prisma.note.delete({
where: { id }
})
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error deleting note:', error)
throw new Error('Failed to delete note')
}
}
// Toggle pin status
export async function togglePin(id: string, isPinned: boolean) {
return updateNote(id, { isPinned })
}
// Toggle archive status
export async function toggleArchive(id: string, isArchived: boolean) {
return updateNote(id, { isArchived })
}
// Update note color
export async function updateColor(id: string, color: string) {
return updateNote(id, { color })
}
// Update note labels
export async function updateLabels(id: string, labels: string[]) {
return updateNote(id, { labels })
}
// Get all unique labels
export async function getAllLabels() {
try {
const notes = await prisma.note.findMany({
select: { labels: true }
})
const labelsSet = new Set<string>()
notes.forEach(note => {
const labels = note.labels ? JSON.parse(note.labels) : null
if (labels) {
labels.forEach((label: string) => labelsSet.add(label))
}
})
return Array.from(labelsSet).sort()
} catch (error) {
console.error('Error fetching labels:', error)
return []
}
}
// Reorder notes (drag and drop)
export async function reorderNotes(draggedId: string, targetId: string) {
try {
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
if (!draggedNote || !targetNote) {
throw new Error('Notes not found')
}
// Swap the order values
await prisma.$transaction([
prisma.note.update({
where: { id: draggedId },
data: { order: targetNote.order }
}),
prisma.note.update({
where: { id: targetId },
data: { order: draggedNote.order }
})
])
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error reordering notes:', error)
throw new Error('Failed to reorder notes')
}
}