'use client' import { useState, useTransition, useEffect } from 'react' import type { Note } from '@/lib/types' import { getNoteFeedImage, getNotePlainExcerpt, getNoteDisplayTitle } from '@/lib/note-preview' import { useLanguage } from '@/lib/i18n' import { useRefresh } from '@/lib/use-refresh' import { motion, AnimatePresence } from 'motion/react' import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil, Sparkles, Loader2, Bell, FolderOpen, StickyNote } from 'lucide-react' import { useSession } from 'next-auth/react' import { getAISettings } from '@/app/actions/ai-settings' import { generateNoteIllustrationSvg } from '@/app/actions/note-illustration' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@/components/ui/dropdown-menu' import { deleteNote, toggleArchive, togglePin, updateNote } from '@/app/actions/notes' import { ReminderDialog } from '@/components/reminder-dialog' import { useNotebooks } from '@/context/notebooks-context' import { toast } from 'sonner' type NotesEditorialViewProps = { notes: Note[] onOpen: (note: Note, readOnly?: boolean) => void notebookName?: string onOpenHistory?: (note: Note) => void } function formatNoteDate(date: Date | string): string { const d = typeof date === 'string' ? new Date(date) : date return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }).toUpperCase() } function EditorialNoteMenu({ note, onOpen, onOpenHistory }: { note: Note onOpen: (note: Note) => void onOpenHistory?: (note: Note) => void }) { const { t } = useLanguage() const { refreshNotes } = useRefresh() const { notebooks } = useNotebooks() const [, startTransition] = useTransition() const [showReminder, setShowReminder] = useState(false) const [showNotebookMenu, setShowNotebookMenu] = useState(false) const handleDelete = (e: React.MouseEvent) => { e.stopPropagation() startTransition(async () => { try { await deleteNote(note.id) refreshNotes(note?.notebookId) toast.success(t('notes.deleted') || 'Note supprimée') } catch { toast.error(t('general.error')) } }) } const handleArchive = (e: React.MouseEvent) => { e.stopPropagation() startTransition(async () => { try { await toggleArchive(note.id, !note.isArchived) refreshNotes(note?.notebookId) toast.success(note.isArchived ? (t('notes.unarchived') || 'Désarchivée') : (t('notes.archived') || 'Archivée')) } catch { toast.error(t('general.error')) } }) } const handlePin = (e: React.MouseEvent) => { e.stopPropagation() startTransition(async () => { try { await togglePin(note.id, !note.isPinned) refreshNotes(note?.notebookId) } catch { toast.error(t('general.error')) } }) } const handleMoveToNotebook = (notebookId: string | null) => { startTransition(async () => { try { await updateNote(note.id, { notebookId }) refreshNotes(note?.notebookId) toast.success(t('notebookSuggestion.movedToNotebook') || 'Note déplacée') } catch { toast.error(t('general.error')) } }) } return ( e.stopPropagation()}> { e.stopPropagation(); onOpen(note) }}> {t('notes.open') || 'Ouvrir'} {note.isPinned ? (t('notes.unpin') || 'Désépingler') : (t('notes.pin') || 'Épingler')} {note.isArchived ? (t('notes.unarchive') || 'Désarchiver') : (t('notes.archive') || 'Archiver')} {onOpenHistory && ( { e.stopPropagation(); onOpenHistory(note) }}> {t('notes.history') || 'Historique'} )} {/* Rappel */} { e.stopPropagation(); setShowReminder(true) }}> {note.reminder ? (t('reminder.changeReminder') || 'Modifier le rappel') : (t('reminder.setReminder') || 'Définir un rappel')} { startTransition(async () => { await updateNote(note.id, { reminder: date }) refreshNotes(note?.notebookId) setShowReminder(false) }) }} onRemove={() => { startTransition(async () => { await updateNote(note.id, { reminder: null }) refreshNotes(note?.notebookId) setShowReminder(false) }) }} /> {/* Déplacer vers notebook */} e.stopPropagation()}> {t('notebookSuggestion.moveToNotebook') || 'Déplacer vers notebook'} { e.stopPropagation(); handleMoveToNotebook(null) }}> {t('notebookSuggestion.generalNotes') || 'Notes générales'} {notebooks.map((nb: any) => ( { e.stopPropagation(); handleMoveToNotebook(nb.id) }}> {nb.name} ))} {t('notes.delete') || 'Supprimer'} ) } /** Deterministic hue from a string — consistent per note */ function stringToHue(s: string): number { let h = 0 for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffff return h % 360 } function EditorialThumbnail({ note, title, aiIllustrationEnabled, }: { note: Note title: string aiIllustrationEnabled: boolean }) { const { t } = useLanguage() const { refreshNotes } = useRefresh() const [busy, setBusy] = useState(false) const img = getNoteFeedImage(note) const handleGenerateSvg = async (e: React.MouseEvent) => { e.stopPropagation() if (!aiIllustrationEnabled || busy || img) return setBusy(true) try { const res = await generateNoteIllustrationSvg(note.id) if (!res.ok) { toast.error(res.error) } else { toast.success(t('notes.illustrationGenerated') || 'Illustration générée') refreshNotes(note?.notebookId) } } finally { setBusy(false) } } return (
{img ? ( ) : note.illustrationSvg ? (
) : ( <> {aiIllustrationEnabled && ( )} )}
) } /** SVG thumbnail for notes without an image */ function NoteThumbnailPlaceholder({ title, noteId }: { title: string; noteId: string }) { // Try to extract the first emoji from the title const emojiMatch = title.match(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u) const emoji = emojiMatch?.[0] const letter = title.replace(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu, '').trim()[0]?.toUpperCase() || '?' const hue = stringToHue(noteId) return (
{/* Decorative concentric circles */} {emoji ? ( {emoji} ) : ( {letter} )}
) } export function NotesEditorialView({ notes, onOpen, notebookName, onOpenHistory, }: NotesEditorialViewProps) { const { t } = useLanguage() const { data: session } = useSession() const [aiIllustrationEnabled, setAiIllustrationEnabled] = useState(false) useEffect(() => { if (!session?.user?.id) { setAiIllustrationEnabled(false) return } getAISettings(session.user.id) .then((s) => setAiIllustrationEnabled(s.paragraphRefactor !== false)) .catch(() => setAiIllustrationEnabled(false)) }, [session?.user?.id]) return (
{notes.map((note: Note, index: number) => { const title = getNoteDisplayTitle(note, t('notes.untitled') || 'Untitled') const excerpt = getNotePlainExcerpt(note) const dateStr = formatNoteDate(note.createdAt) return ( onOpen(note)} > {/* Date / breadcrumb */}
{notebookName ? `${notebookName} — ${dateStr}` : dateStr}
{/* Actions menu — absolutely positioned at top-right */}
e.stopPropagation()} >

{title}

{excerpt ? (

{excerpt}

) : (

{t('notes.noContent')}

)} {t('notes.readMore') || 'Read more'}
) })}
) }