refactor: migrate remaining components to useRefresh hook

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 <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-08 14:45:50 +00:00
parent 9b8df398dc
commit 574c8b3166
8 changed files with 151 additions and 59 deletions

View File

@@ -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 (
<div className="relative w-full md:w-56 aspect-[4/3] bg-card/80 border border-border overflow-hidden rounded shadow-sm flex-shrink-0 group/thumb">
{img ? (
<img
src={img}
alt=""
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
/>
) : note.illustrationSvg ? (
<div
className="w-full h-full flex items-center justify-center bg-muted/30 p-2 [&_svg]:max-w-full [&_svg]:max-h-full [&_svg]:w-auto [&_svg]:h-auto"
// SVG déjà sanitisé côté serveur (note-illustration.ts)
dangerouslySetInnerHTML={{ __html: note.illustrationSvg }}
aria-hidden
/>
) : (
<>
<NoteThumbnailPlaceholder title={title} noteId={note.id} />
{aiIllustrationEnabled && (
<button
type="button"
aria-label={t('notes.generateIllustration') || 'Générer une illustration IA'}
title={t('notes.generateIllustration') || 'Générer une illustration IA'}
className="absolute bottom-2 right-2 flex h-9 w-9 items-center justify-center rounded-full border border-border bg-background/95 text-foreground shadow-card-rest backdrop-blur-sm transition-colors hover:bg-accent z-10 opacity-0 group-hover/thumb:opacity-100 md:opacity-100 focus-visible:opacity-100"
onClick={handleGenerateSvg}
disabled={busy}
>
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4 text-primary" />}
</button>
)}
</>
)}
</div>
)
}
/** 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 (
<div className="mx-auto w-full max-w-3xl space-y-16">
<AnimatePresence>
{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({
</h2>
<div className="flex flex-col md:flex-row gap-8 items-start">
<div className="w-full md:w-56 aspect-[4/3] bg-white/50 border border-border overflow-hidden rounded shadow-sm flex-shrink-0">
{img ? (
<img
src={img}
alt=""
className="w-full h-full object-cover mix-blend-multiply opacity-80 grayscale contrast-125 hover:grayscale-0 hover:opacity-100 transition-all duration-500"
/>
) : (
<NoteThumbnailPlaceholder title={title} noteId={note.id} />
)}
</div>
<EditorialThumbnail note={note} title={title} aiIllustrationEnabled={aiIllustrationEnabled} />
<div className="space-y-3 flex-1">
{excerpt ? (
<p className="text-[14px] leading-relaxed text-foreground/80 font-light max-w-lg line-clamp-4">