From 574c8b31666bfe1dd98193e19afe551ef0344749 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 8 May 2026 14:45:50 +0000 Subject: [PATCH] refactor: migrate remaining components to useRefresh hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace triggerRefresh() with useRefresh() in: - notes-tabs-view.tsx (5 calls → refreshNotes) - label-management-dialog.tsx (3 calls → refreshLabels) - note-inline-editor.tsx (3 calls → refreshNotes) - note-card.tsx (7 calls → refreshNotes) - recent-notes-section.tsx (3 calls → refreshNotes) - notification-panel.tsx (2 calls → refreshNotes(null)) - notes-editorial-view.tsx (4 calls → refreshNotes) NoteRefreshContext marked as @deprecated with JSDoc migration guide. Co-Authored-By: Claude Opus 4.7 --- .../components/label-management-dialog.tsx | 10 +- memento-note/components/note-card.tsx | 20 ++-- .../components/note-inline-editor.tsx | 10 +- .../components/notes-editorial-view.tsx | 109 +++++++++++++++--- memento-note/components/notes-tabs-view.tsx | 17 +-- .../components/notification-panel.tsx | 8 +- .../components/recent-notes-section.tsx | 12 +- memento-note/context/NoteRefreshContext.tsx | 24 +++- 8 files changed, 151 insertions(+), 59 deletions(-) diff --git a/memento-note/components/label-management-dialog.tsx b/memento-note/components/label-management-dialog.tsx index ced2595..7ca89db 100644 --- a/memento-note/components/label-management-dialog.tsx +++ b/memento-note/components/label-management-dialog.tsx @@ -16,7 +16,7 @@ import { LABEL_COLORS, LabelColorName } from '@/lib/types' import { cn } from '@/lib/utils' import { useNotebooks } from '@/context/notebooks-context' import { useLanguage } from '@/lib/i18n' -import { useNoteRefresh } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' export interface LabelManagementDialogProps { /** Mode contrôlé (ex. ouverture depuis la liste des carnets) */ @@ -28,7 +28,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) { const { open, onOpenChange } = props const { labels, isLoading: loading, addLabel, updateLabel, deleteLabel } = useNotebooks() const { t, language } = useLanguage() - const { triggerRefresh } = useNoteRefresh() + const { refreshLabels } = useRefresh() const [confirmDeleteId, setConfirmDeleteId] = useState(null) const [newLabel, setNewLabel] = useState('') const [editingColorId, setEditingColorId] = useState(null) @@ -40,7 +40,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) { if (trimmed) { try { await addLabel(trimmed, 'gray') - triggerRefresh() + refreshLabels() setNewLabel('') } catch (error) { console.error('Failed to add label:', error) @@ -52,7 +52,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) { try { const labelToDelete = labels.find(l => l.id === id) await deleteLabel(id) - triggerRefresh() + refreshLabels() if (labelToDelete) { window.dispatchEvent(new CustomEvent('label-deleted', { detail: { name: labelToDelete.name } })) } @@ -65,7 +65,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) { const handleChangeColor = async (id: string, color: LabelColorName) => { try { await updateLabel(id, { color }) - triggerRefresh() + refreshLabels() setEditingColorId(null) } catch (error) { console.error('Failed to update label color:', error) diff --git a/memento-note/components/note-card.tsx b/memento-note/components/note-card.tsx index 553cdd5..5d64411 100644 --- a/memento-note/components/note-card.tsx +++ b/memento-note/components/note-card.tsx @@ -59,7 +59,7 @@ const ComparisonModal = dynamic(() => import('./comparison-modal').then(m => ({ const FusionModal = dynamic(() => import('./fusion-modal').then(m => ({ default: m.FusionModal })), { ssr: false }) import { useConnectionsCompare } from '@/hooks/use-connections-compare' import { useNotebooks } from '@/context/notebooks-context' -import { useNoteRefresh } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { useLanguage } from '@/lib/i18n' import { toast } from 'sonner' @@ -171,7 +171,7 @@ export const NoteCard = memo(function NoteCard({ }: NoteCardProps) { const router = useRouter() const searchParams = useSearchParams() - const { triggerRefresh } = useNoteRefresh() + const { refreshNotes } = useRefresh() const { data: session } = useSession() const { t, language } = useLanguage() const { notebooks, moveNoteToNotebookOptimistic, refreshLabels } = useNotebooks() @@ -201,7 +201,7 @@ export const NoteCard = memo(function NoteCard({ try { await updateNote(noteId, { reminder }) setReminderDate(reminder) - triggerRefresh() + refreshNotes(note?.notebookId) if (reminder) { toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() })) } else { @@ -217,7 +217,7 @@ export const NoteCard = memo(function NoteCard({ const handleMoveToNotebook = async (notebookId: string | null) => { await moveNoteToNotebookOptimistic(note.id, notebookId) setShowNotebookMenu(false) - // No need for router.refresh() - triggerRefresh() is already called in moveNoteToNotebookOptimistic + // No need for router.refresh() - refreshNotes(note?.notebookId) is already called in moveNoteToNotebookOptimistic } // Optimistic UI state for instant feedback @@ -302,7 +302,7 @@ export const NoteCard = memo(function NoteCard({ try { await deleteNote(note.id) await refreshLabels() - triggerRefresh() // met à jour la liste et le compteur du carnet + refreshNotes(note?.notebookId) // met à jour la liste et le compteur du carnet } catch (error) { console.error('Failed to delete note:', error) setIsHidden(false) @@ -315,7 +315,7 @@ export const NoteCard = memo(function NoteCard({ setIsHidden(true) try { await restoreNote(note.id) - triggerRefresh() + refreshNotes(note?.notebookId) toast.success(t('trash.noteRestored')) } catch (error) { console.error('Failed to restore note:', error) @@ -329,7 +329,7 @@ export const NoteCard = memo(function NoteCard({ setIsHidden(true) try { await permanentDeleteNote(note.id) - triggerRefresh() + refreshNotes(note?.notebookId) toast.success(t('trash.notePermanentlyDeleted')) } catch (error) { console.error('Failed to permanently delete note:', error) @@ -342,7 +342,7 @@ export const NoteCard = memo(function NoteCard({ startTransition(async () => { addOptimisticNote({ isPinned: !note.isPinned }) await togglePin(note.id, !note.isPinned) - triggerRefresh() + refreshNotes(note?.notebookId) if (!note.isPinned) { toast.success(t('notes.pinned') || 'Note pinned') @@ -356,7 +356,7 @@ export const NoteCard = memo(function NoteCard({ startTransition(async () => { addOptimisticNote({ isArchived: !note.isArchived }) await toggleArchive(note.id, !note.isArchived) - triggerRefresh() + refreshNotes(note?.notebookId) }) } @@ -809,7 +809,7 @@ export const NoteCard = memo(function NoteCard({ } toast.success(t('toast.notesFusionSuccess')) setFusionNotes([]) - triggerRefresh() + refreshNotes(note?.notebookId) }} /> diff --git a/memento-note/components/note-inline-editor.tsx b/memento-note/components/note-inline-editor.tsx index 122d964..d046106 100644 --- a/memento-note/components/note-inline-editor.tsx +++ b/memento-note/components/note-inline-editor.tsx @@ -53,7 +53,7 @@ import { GhostTags } from '@/components/ghost-tags' import { useTitleSuggestions } from '@/hooks/use-title-suggestions' import { TitleSuggestions } from '@/components/title-suggestions' import { useNotebooks } from '@/context/notebooks-context' -import { useNoteRefresh } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { ContextualAIChat } from '@/components/contextual-ai-chat' import { formatDistanceToNow } from 'date-fns' import { fr } from 'date-fns/locale/fr' @@ -121,7 +121,7 @@ export function NoteInlineEditor({ }, [session?.user?.id]) const { labels: globalLabels, addLabel } = useNotebooks() const [, startTransition] = useTransition() - const { triggerRefresh } = useNoteRefresh() + const { refreshNotes } = useRefresh() // ── Local edit state ────────────────────────────────────────────────────── const [title, setTitle] = useState(note.title || '') @@ -311,7 +311,7 @@ export function NoteInlineEditor({ } toast.success(t('toast.notesFusionSuccess')) setFusionNotes([]) - triggerRefresh() + refreshNotes(note?.notebookId) } // ── Quick actions (pin, archive, color, delete) ─────────────────────────── @@ -334,7 +334,7 @@ export function NoteInlineEditor({ onArchive?.(note.id) try { await toggleArchive(note.id, !note.isArchived) - triggerRefresh() + refreshNotes(note?.notebookId) } catch { // Cannot easily revert since onArchive removes from list toast.error(t('general.error')) @@ -360,7 +360,7 @@ export function NoteInlineEditor({ startTransition(async () => { await deleteNote(note.id) onDelete?.(note.id) - triggerRefresh() + refreshNotes(note?.notebookId) }) } diff --git a/memento-note/components/notes-editorial-view.tsx b/memento-note/components/notes-editorial-view.tsx index 823d141..2f44058 100644 --- a/memento-note/components/notes-editorial-view.tsx +++ b/memento-note/components/notes-editorial-view.tsx @@ -1,12 +1,15 @@ 'use client' -import { useState, useTransition } from 'react' +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 { useNoteRefresh } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { motion, AnimatePresence } from 'motion/react' -import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil } from 'lucide-react' +import { ChevronRight, MoreHorizontal, Trash2, Archive, Pin, History, Pencil, Sparkles, Loader2 } 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, @@ -39,7 +42,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: { onOpenHistory?: (note: Note) => void }) { const { t } = useLanguage() - const { triggerRefresh } = useNoteRefresh() + const { refreshNotes } = useRefresh() const [, startTransition] = useTransition() const handleDelete = (e: React.MouseEvent) => { @@ -47,7 +50,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: { startTransition(async () => { try { await deleteNote(note.id) - triggerRefresh() + refreshNotes(note?.notebookId) toast.success(t('notes.deleted') || 'Note supprimée') } catch { toast.error(t('general.error')) @@ -60,7 +63,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: { startTransition(async () => { try { await toggleArchive(note.id, !note.isArchived) - triggerRefresh() + refreshNotes(note?.notebookId) toast.success(note.isArchived ? (t('notes.unarchived') || 'Désarchivée') : (t('notes.archived') || 'Archivée')) } catch { toast.error(t('general.error')) @@ -73,7 +76,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: { startTransition(async () => { try { await togglePin(note.id, !note.isPinned) - triggerRefresh() + refreshNotes(note?.notebookId) } catch { toast.error(t('general.error')) } @@ -123,6 +126,73 @@ function stringToHue(s: string): number { 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 @@ -180,13 +250,24 @@ export function NotesEditorialView({ 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 img = getNoteFeedImage(note) const excerpt = getNotePlainExcerpt(note) const dateStr = formatNoteDate(note.createdAt) @@ -215,17 +296,7 @@ export function NotesEditorialView({
-
- {img ? ( - - ) : ( - - )} -
+
{excerpt ? (

diff --git a/memento-note/components/notes-tabs-view.tsx b/memento-note/components/notes-tabs-view.tsx index ec99c83..215de8c 100644 --- a/memento-note/components/notes-tabs-view.tsx +++ b/memento-note/components/notes-tabs-view.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState, useTransition, useRef } from 'react' import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { DndContext, type DragEndEvent, @@ -635,7 +636,7 @@ export function NotesTabsView({ onNoteCreated, }: NotesTabsViewProps) { const { t, language } = useLanguage() - const { triggerRefresh } = useNoteRefreshOptional() + const { refreshNotes } = useRefresh() const [items, setItems] = useState(notes) const [selectedId, setSelectedId] = useState(null) const [isCreating, startCreating] = useTransition() @@ -796,8 +797,8 @@ export function NotesTabsView({ }) setSelectedId(newNote.id) onNoteCreated?.(newNote) - // NOTE: No triggerRefresh() here — the note is already added to items above. - // triggerRefresh() would call getAllNotes() which may return stale cache + // NOTE: No refreshNotes(note.notebookId) here — the note is already added to items above. + // refreshNotes(note.notebookId) would call getAllNotes() which may return stale cache // in production (skipRevalidation:true skips cache invalidation). } catch { toast.error(t('notes.createFailed') || 'Impossible de créer la note') @@ -810,7 +811,7 @@ export function NotesTabsView({ setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: next } : n)) try { await updateNote(note.id, { isPinned: next }, { skipRevalidation: true }) - triggerRefresh() + refreshNotes(note.notebookId) toast.success(next ? (t('notes.pinned') || 'Épinglée') : (t('notes.unpinned') || 'Désépinglée')) } catch { setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: note.isPinned } : n)) @@ -823,7 +824,7 @@ export function NotesTabsView({ await toggleArchive(note.id, true) setItems((prev) => prev.filter((n) => n.id !== note.id)) setSelectedId((prev) => (prev === note.id ? null : prev)) - triggerRefresh() + refreshNotes(note.notebookId) toast.success(t('notes.archived') || 'Note archivée') } catch { toast.error(t('notes.archiveFailed') || 'Archivage échoué') @@ -1022,7 +1023,7 @@ export function NotesTabsView({ onArchive={(noteId) => { setItems((prev) => prev.filter((n) => n.id !== noteId)) setSelectedId((prev) => (prev === noteId ? null : prev)) - triggerRefresh() + refreshNotes(selected?.notebookId) }} /> {/* Toggle sidebar button — top-right of editor, always visible */} @@ -1057,7 +1058,7 @@ export function NotesTabsView({ } else { toast.info(t('reminder.removeReminder')) } - triggerRefresh() + refreshNotes(items.find(n => n.id === noteId)?.notebookId) } catch { toast.error(t('general.error')) } @@ -1110,7 +1111,7 @@ export function NotesTabsView({ setItems((prev) => prev.filter((n) => n.id !== noteToDelete.id)) setSelectedId((prev) => (prev === noteToDelete.id ? null : prev)) setNoteToDelete(null) - triggerRefresh() + refreshNotes(noteToDelete.notebookId) toast.success(t('notes.deleted')) } catch { toast.error(t('notes.deleteFailed')) diff --git a/memento-note/components/notification-panel.tsx b/memento-note/components/notification-panel.tsx index 7e6367c..1169585 100644 --- a/memento-note/components/notification-panel.tsx +++ b/memento-note/components/notification-panel.tsx @@ -13,7 +13,7 @@ import { import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes' import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications' import { toast } from 'sonner' -import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { cn } from '@/lib/utils' import { useLanguage } from '@/lib/i18n' import { formatDistanceToNow } from 'date-fns' @@ -47,7 +47,7 @@ interface ReminderNote { } export function NotificationPanel() { - const { triggerRefresh } = useNoteRefreshOptional() + const { refreshNotes } = useRefresh() const { t } = useLanguage() const router = useRouter() const [requests, setRequests] = useState([]) @@ -97,7 +97,7 @@ export function NotificationPanel() { description: t('collaboration.nowHasAccess', { name: 'Note' }), duration: 3000, }) - triggerRefresh() + refreshNotes(null) setOpen(false) } catch (error: any) { console.error('[NOTIFICATION] Error:', error) @@ -121,7 +121,7 @@ export function NotificationPanel() { try { await toggleReminderDone(noteId, done) setReminders(prev => prev.map(r => r.id === noteId ? { ...r, isReminderDone: done } : r)) - triggerRefresh() + refreshNotes(null) } catch { toast.error(t('general.error')) } diff --git a/memento-note/components/recent-notes-section.tsx b/memento-note/components/recent-notes-section.tsx index 88d3294..ea0badb 100644 --- a/memento-note/components/recent-notes-section.tsx +++ b/memento-note/components/recent-notes-section.tsx @@ -10,7 +10,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { togglePin, deleteNote, dismissFromRecent } from '@/app/actions/notes' import { useRouter } from 'next/navigation' import { useNotebooks } from '@/context/notebooks-context' -import { useNoteRefresh } from '@/context/NoteRefreshContext' +import { useRefresh } from '@/lib/use-refresh' import { toast } from 'sonner' import { StickyNote } from 'lucide-react' @@ -64,7 +64,7 @@ function CompactCard({ const { t } = useLanguage() const router = useRouter() const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() - const { triggerRefresh } = useNoteRefresh() + const { refreshNotes } = useRefresh() const [isDeleting, setIsDeleting] = useState(false) const [showNotebookMenu, setShowNotebookMenu] = useState(false) const [, startTransition] = useTransition() @@ -86,7 +86,7 @@ function CompactCard({ await togglePin(note.id, newPinnedState) // Trigger global refresh to update lists - triggerRefresh() + refreshNotes(note?.notebookId) router.refresh() if (newPinnedState) { @@ -100,7 +100,7 @@ function CompactCard({ const handleMoveToNotebook = async (notebookId: string | null) => { await moveNoteToNotebookOptimistic(note.id, notebookId) setShowNotebookMenu(false) - triggerRefresh() + refreshNotes(note?.notebookId) } const handleDelete = async (e: React.MouseEvent) => { @@ -112,7 +112,7 @@ function CompactCard({ setIsDeleting(true) try { await deleteNote(note.id) - triggerRefresh() + refreshNotes(note?.notebookId) router.refresh() } catch (error) { console.error('Failed to delete note:', error) @@ -135,7 +135,7 @@ function CompactCard({ try { await dismissFromRecent(note.id) // Don't refresh list to prevent immediate replacement - // triggerRefresh() + // refreshNotes(note?.notebookId) // router.refresh() toast.success(t('notes.dismissed') || 'Note dismissed from recent') } catch (error) { diff --git a/memento-note/context/NoteRefreshContext.tsx b/memento-note/context/NoteRefreshContext.tsx index abd372f..c27c74f 100644 --- a/memento-note/context/NoteRefreshContext.tsx +++ b/memento-note/context/NoteRefreshContext.tsx @@ -1,7 +1,19 @@ 'use client' -import { createContext, useContext, useState, useCallback, useMemo } from 'react' +import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react' +/** + * @deprecated Use React Query's `useQueryClient.invalidateQueries()` instead. + * This context is kept for backward compatibility during migration. + * + * Migration guide: + * - Replace `const { triggerRefresh } = useNoteRefresh()` with `const { refreshNotes } = useRefresh()` + * - Replace `triggerRefresh()` with `refreshNotes(notebookId)` or `refreshNotes(null)` + * - Replace `triggerNotebooksRefresh()` with `refreshNotebooks()` + * + * @see {@link https://tanstack.com/query/latest/docs/react/reference/queryclient | React Query invalidateQueries} + * @see lib/use-refresh.ts + */ interface NoteRefreshContextType { refreshKey: number triggerRefresh: () => void @@ -11,7 +23,7 @@ interface NoteRefreshContextType { const NoteRefreshContext = createContext(undefined) -export function NoteRefreshProvider({ children }: { children: React.ReactNode }) { +export function NoteRefreshProvider({ children }: { children: ReactNode }) { const [refreshKey, setRefreshKey] = useState(0) const [notebooksRefreshKey, setNotebooksRefreshKey] = useState(0) @@ -32,6 +44,10 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode }) ) } +/** + * @deprecated Use `useRefresh()` from `@/lib/use-refresh` instead. + * This hook is kept for backward compatibility during migration. + */ export function useNoteRefresh() { const context = useContext(NoteRefreshContext) if (!context) { @@ -40,6 +56,10 @@ export function useNoteRefresh() { return context } +/** + * @deprecated Use `useRefresh()` from `@/lib/use-refresh` instead. + * This hook is kept for backward compatibility during migration. + */ export function useNoteRefreshOptional(): NoteRefreshContextType { const context = useContext(NoteRefreshContext) return context ?? { refreshKey: 0, triggerRefresh: () => {}, notebooksRefreshKey: 0, triggerNotebooksRefresh: () => {} }