'use client' import { useState, useTransition, useEffect, useRef } from 'react' import type { Note } from '@/lib/types' import { getNoteFeedImage, getNotePlainExcerpt, getNoteDisplayTitle } from '@/lib/note-preview' import { useLanguage } from '@/lib/i18n' import { emitNoteChange, type NoteCollectionActions } from '@/lib/note-change-sync' import { motion, AnimatePresence } from 'motion/react' import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil, Sparkles, Loader2, Bell, FolderOpen, FileText } from 'lucide-react' import { useLabelsQuery } from '@/lib/query-hooks' 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, } from '@/components/ui/dropdown-menu' import { MoveToNotebookPickerPortal } from '@/components/move-to-notebook-picker' 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' import { fr } from 'date-fns/locale/fr' import { enUS } from 'date-fns/locale/en-US' import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date' import { cn } from '@/lib/utils' import { useHydrated } from '@/lib/use-hydrated' type NotesEditorialViewProps = { notes: Note[] onOpen: (note: Note, readOnly?: boolean) => void notebookName?: string onOpenHistory?: (note: Note) => void } & NoteCollectionActions function formatNoteDate(date: Date | string, language: string): string { const d = typeof date === 'string' ? new Date(date) : date const locale = language === 'fr' ? fr : enUS if (language === 'fa') { return formatAbsoluteDateLocalized(d, language, 'd MMM yyyy', locale) } const month = d.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }) const day = d.getUTCDate() const year = d.getUTCFullYear() return `${month.toUpperCase()} ${day}, ${year}` } export function EditorialNoteMenu({ note, onOpen, onOpenHistory, onTogglePin, onDeleteNote, onArchiveNote, onMoveToNotebook, onNotePatch, }: { note: Note onOpen: (note: Note) => void onOpenHistory?: (note: Note) => void } & NoteCollectionActions) { const { t } = useLanguage() const { notebooks } = useNotebooks() const [, startTransition] = useTransition() const [showReminder, setShowReminder] = useState(false) const [movePickerOpen, setMovePickerOpen] = useState(false) const menuTriggerRef = useRef(null) const handleDelete = (e: React.MouseEvent) => { e.stopPropagation() if (onDeleteNote) { onDeleteNote(note) return } startTransition(async () => { try { await deleteNote(note.id, { skipRevalidation: true }) emitNoteChange({ type: 'deleted', noteId: note.id, notebookId: note.notebookId }) toast.success(t('notes.deleted') || 'Note supprimée') } catch { toast.error(t('general.error')) } }) } const handleArchive = (e: React.MouseEvent) => { e.stopPropagation() if (onArchiveNote) { onArchiveNote(note) return } startTransition(async () => { try { await toggleArchive(note.id, !note.isArchived, { skipRevalidation: true }) emitNoteChange({ type: 'updated', note: { ...note, isArchived: !note.isArchived } }) 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() if (onTogglePin) { onTogglePin(note) return } startTransition(async () => { try { const nextPinned = !note.isPinned await togglePin(note.id, nextPinned, { skipRevalidation: true }) emitNoteChange({ type: 'updated', note: { ...note, isPinned: nextPinned } }) } catch { toast.error(t('general.error')) } }) } const handleMoveToNotebook = (notebookId: string | null) => { if (onMoveToNotebook) { onMoveToNotebook(note, notebookId) return } startTransition(async () => { try { await updateNote(note.id, { notebookId }, { skipRevalidation: true }) emitNoteChange({ type: 'updated', note: { ...note, notebookId } }) toast.success(t('notebookSuggestion.movedToNotebook') || 'Note déplacée') } catch { toast.error(t('general.error')) } }) } const patchReminder = (reminder: Date | null) => { startTransition(async () => { try { await updateNote(note.id, { reminder }, { skipRevalidation: true }) const patch = { reminder: reminder?.toISOString() ?? null } onNotePatch?.(note.id, patch) emitNoteChange({ type: 'updated', note: { ...note, reminder: patch.reminder } }) setShowReminder(false) } 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')} {/* Déplacer vers un carnet */} { e.stopPropagation() setMovePickerOpen(true) }} > {t('notebookSuggestion.moveToNotebook') || 'Déplacer vers…'} {t('notes.delete') || 'Supprimer'} {/* ReminderDialog hors du DropdownMenu pour éviter les conflits de portail */} patchReminder(date)} onRemove={() => patchReminder(null)} /> ) } /** 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, onNoteIllustrationGenerated, }: { note: Note title: string aiIllustrationEnabled: boolean onNoteIllustrationGenerated?: (noteId: string) => void | Promise }) { const { t } = useLanguage() 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, { skipRevalidation: true }) if (!res.ok) { toast.error(res.error) } else { toast.success(t('notes.illustrationGenerated') || 'Illustration générée') await onNoteIllustrationGenerated?.(note.id) } } finally { setBusy(false) } } return (
{img ? ( ) : note.illustrationSvg ? (
) : ( <> {aiIllustrationEnabled && ( )} )}
) } /** SVG thumbnail for notes without an image — icône document (ref. architectural-grid), pas initiale */ function NoteThumbnailPlaceholder({ noteId }: { noteId: string }) { const hue = stringToHue(noteId) return (
) } function NoteTag({ labelName, allLabels }: { labelName: string; allLabels: any[] }) { const labelDef = allLabels?.find(l => l.name === labelName) const isAI = labelDef?.type === 'ai' return (
{isAI && } {labelName}
) } export function NotesEditorialView({ notes, onOpen, notebookName, onOpenHistory, onTogglePin, onDeleteNote, onArchiveNote, onMoveToNotebook, onNotePatch, onNoteIllustrationGenerated, }: NotesEditorialViewProps) { const { t, language } = useLanguage() const { data: session } = useSession() const { data: allLabels } = useLabelsQuery() const hydrated = useHydrated() 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, language) const editorialRtl = language === 'fa' || language === 'ar' return ( onOpen(note)} > {/* Date / breadcrumb — isolated bidi so Latin notebook name + Jalali date don’t reorder wrongly */}
{notebookName ? ( <> {notebookName} {dateStr} ) : ( {dateStr} )}
{/* Actions menu — absolutely positioned at top-right */}
e.stopPropagation()} >

{title}

{note.labels && note.labels.length > 0 && (
{note.labels.slice(0, 2).map((labelName) => ( ))}
)} {excerpt ? (

{excerpt}

) : (

{t('notes.noContent')}

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