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:
@@ -16,7 +16,7 @@ import { LABEL_COLORS, LabelColorName } from '@/lib/types'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useNotebooks } from '@/context/notebooks-context'
|
import { useNotebooks } from '@/context/notebooks-context'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
|
|
||||||
export interface LabelManagementDialogProps {
|
export interface LabelManagementDialogProps {
|
||||||
/** Mode contrôlé (ex. ouverture depuis la liste des carnets) */
|
/** Mode contrôlé (ex. ouverture depuis la liste des carnets) */
|
||||||
@@ -28,7 +28,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) {
|
|||||||
const { open, onOpenChange } = props
|
const { open, onOpenChange } = props
|
||||||
const { labels, isLoading: loading, addLabel, updateLabel, deleteLabel } = useNotebooks()
|
const { labels, isLoading: loading, addLabel, updateLabel, deleteLabel } = useNotebooks()
|
||||||
const { t, language } = useLanguage()
|
const { t, language } = useLanguage()
|
||||||
const { triggerRefresh } = useNoteRefresh()
|
const { refreshLabels } = useRefresh()
|
||||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
||||||
const [newLabel, setNewLabel] = useState('')
|
const [newLabel, setNewLabel] = useState('')
|
||||||
const [editingColorId, setEditingColorId] = useState<string | null>(null)
|
const [editingColorId, setEditingColorId] = useState<string | null>(null)
|
||||||
@@ -40,7 +40,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) {
|
|||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
try {
|
try {
|
||||||
await addLabel(trimmed, 'gray')
|
await addLabel(trimmed, 'gray')
|
||||||
triggerRefresh()
|
refreshLabels()
|
||||||
setNewLabel('')
|
setNewLabel('')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add label:', error)
|
console.error('Failed to add label:', error)
|
||||||
@@ -52,7 +52,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) {
|
|||||||
try {
|
try {
|
||||||
const labelToDelete = labels.find(l => l.id === id)
|
const labelToDelete = labels.find(l => l.id === id)
|
||||||
await deleteLabel(id)
|
await deleteLabel(id)
|
||||||
triggerRefresh()
|
refreshLabels()
|
||||||
if (labelToDelete) {
|
if (labelToDelete) {
|
||||||
window.dispatchEvent(new CustomEvent('label-deleted', { detail: { name: labelToDelete.name } }))
|
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) => {
|
const handleChangeColor = async (id: string, color: LabelColorName) => {
|
||||||
try {
|
try {
|
||||||
await updateLabel(id, { color })
|
await updateLabel(id, { color })
|
||||||
triggerRefresh()
|
refreshLabels()
|
||||||
setEditingColorId(null)
|
setEditingColorId(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update label color:', error)
|
console.error('Failed to update label color:', error)
|
||||||
|
|||||||
@@ -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 })
|
const FusionModal = dynamic(() => import('./fusion-modal').then(m => ({ default: m.FusionModal })), { ssr: false })
|
||||||
import { useConnectionsCompare } from '@/hooks/use-connections-compare'
|
import { useConnectionsCompare } from '@/hooks/use-connections-compare'
|
||||||
import { useNotebooks } from '@/context/notebooks-context'
|
import { useNotebooks } from '@/context/notebooks-context'
|
||||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
}: NoteCardProps) {
|
}: NoteCardProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const { triggerRefresh } = useNoteRefresh()
|
const { refreshNotes } = useRefresh()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const { t, language } = useLanguage()
|
const { t, language } = useLanguage()
|
||||||
const { notebooks, moveNoteToNotebookOptimistic, refreshLabels } = useNotebooks()
|
const { notebooks, moveNoteToNotebookOptimistic, refreshLabels } = useNotebooks()
|
||||||
@@ -201,7 +201,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
try {
|
try {
|
||||||
await updateNote(noteId, { reminder })
|
await updateNote(noteId, { reminder })
|
||||||
setReminderDate(reminder)
|
setReminderDate(reminder)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
if (reminder) {
|
if (reminder) {
|
||||||
toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() }))
|
toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() }))
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +217,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
const handleMoveToNotebook = async (notebookId: string | null) => {
|
const handleMoveToNotebook = async (notebookId: string | null) => {
|
||||||
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
||||||
setShowNotebookMenu(false)
|
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
|
// Optimistic UI state for instant feedback
|
||||||
@@ -302,7 +302,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
try {
|
try {
|
||||||
await deleteNote(note.id)
|
await deleteNote(note.id)
|
||||||
await refreshLabels()
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to delete note:', error)
|
console.error('Failed to delete note:', error)
|
||||||
setIsHidden(false)
|
setIsHidden(false)
|
||||||
@@ -315,7 +315,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
setIsHidden(true)
|
setIsHidden(true)
|
||||||
try {
|
try {
|
||||||
await restoreNote(note.id)
|
await restoreNote(note.id)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
toast.success(t('trash.noteRestored'))
|
toast.success(t('trash.noteRestored'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restore note:', error)
|
console.error('Failed to restore note:', error)
|
||||||
@@ -329,7 +329,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
setIsHidden(true)
|
setIsHidden(true)
|
||||||
try {
|
try {
|
||||||
await permanentDeleteNote(note.id)
|
await permanentDeleteNote(note.id)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
toast.success(t('trash.notePermanentlyDeleted'))
|
toast.success(t('trash.notePermanentlyDeleted'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to permanently delete note:', error)
|
console.error('Failed to permanently delete note:', error)
|
||||||
@@ -342,7 +342,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
addOptimisticNote({ isPinned: !note.isPinned })
|
addOptimisticNote({ isPinned: !note.isPinned })
|
||||||
await togglePin(note.id, !note.isPinned)
|
await togglePin(note.id, !note.isPinned)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
|
|
||||||
if (!note.isPinned) {
|
if (!note.isPinned) {
|
||||||
toast.success(t('notes.pinned') || 'Note pinned')
|
toast.success(t('notes.pinned') || 'Note pinned')
|
||||||
@@ -356,7 +356,7 @@ export const NoteCard = memo(function NoteCard({
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
addOptimisticNote({ isArchived: !note.isArchived })
|
addOptimisticNote({ isArchived: !note.isArchived })
|
||||||
await toggleArchive(note.id, !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'))
|
toast.success(t('toast.notesFusionSuccess'))
|
||||||
setFusionNotes([])
|
setFusionNotes([])
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import { GhostTags } from '@/components/ghost-tags'
|
|||||||
import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
|
import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
|
||||||
import { TitleSuggestions } from '@/components/title-suggestions'
|
import { TitleSuggestions } from '@/components/title-suggestions'
|
||||||
import { useNotebooks } from '@/context/notebooks-context'
|
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 { ContextualAIChat } from '@/components/contextual-ai-chat'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { fr } from 'date-fns/locale/fr'
|
import { fr } from 'date-fns/locale/fr'
|
||||||
@@ -121,7 +121,7 @@ export function NoteInlineEditor({
|
|||||||
}, [session?.user?.id])
|
}, [session?.user?.id])
|
||||||
const { labels: globalLabels, addLabel } = useNotebooks()
|
const { labels: globalLabels, addLabel } = useNotebooks()
|
||||||
const [, startTransition] = useTransition()
|
const [, startTransition] = useTransition()
|
||||||
const { triggerRefresh } = useNoteRefresh()
|
const { refreshNotes } = useRefresh()
|
||||||
|
|
||||||
// ── Local edit state ──────────────────────────────────────────────────────
|
// ── Local edit state ──────────────────────────────────────────────────────
|
||||||
const [title, setTitle] = useState(note.title || '')
|
const [title, setTitle] = useState(note.title || '')
|
||||||
@@ -311,7 +311,7 @@ export function NoteInlineEditor({
|
|||||||
}
|
}
|
||||||
toast.success(t('toast.notesFusionSuccess'))
|
toast.success(t('toast.notesFusionSuccess'))
|
||||||
setFusionNotes([])
|
setFusionNotes([])
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Quick actions (pin, archive, color, delete) ───────────────────────────
|
// ── Quick actions (pin, archive, color, delete) ───────────────────────────
|
||||||
@@ -334,7 +334,7 @@ export function NoteInlineEditor({
|
|||||||
onArchive?.(note.id)
|
onArchive?.(note.id)
|
||||||
try {
|
try {
|
||||||
await toggleArchive(note.id, !note.isArchived)
|
await toggleArchive(note.id, !note.isArchived)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
} catch {
|
} catch {
|
||||||
// Cannot easily revert since onArchive removes from list
|
// Cannot easily revert since onArchive removes from list
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
@@ -360,7 +360,7 @@ export function NoteInlineEditor({
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
await deleteNote(note.id)
|
await deleteNote(note.id)
|
||||||
onDelete?.(note.id)
|
onDelete?.(note.id)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useTransition } from 'react'
|
import { useState, useTransition, useEffect } from 'react'
|
||||||
import type { Note } from '@/lib/types'
|
import type { Note } from '@/lib/types'
|
||||||
import { getNoteFeedImage, getNotePlainExcerpt, getNoteDisplayTitle } from '@/lib/note-preview'
|
import { getNoteFeedImage, getNotePlainExcerpt, getNoteDisplayTitle } from '@/lib/note-preview'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
import { motion, AnimatePresence } from 'motion/react'
|
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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -39,7 +42,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
|||||||
onOpenHistory?: (note: Note) => void
|
onOpenHistory?: (note: Note) => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const { triggerRefresh } = useNoteRefresh()
|
const { refreshNotes } = useRefresh()
|
||||||
const [, startTransition] = useTransition()
|
const [, startTransition] = useTransition()
|
||||||
|
|
||||||
const handleDelete = (e: React.MouseEvent) => {
|
const handleDelete = (e: React.MouseEvent) => {
|
||||||
@@ -47,7 +50,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
await deleteNote(note.id)
|
await deleteNote(note.id)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
toast.success(t('notes.deleted') || 'Note supprimée')
|
toast.success(t('notes.deleted') || 'Note supprimée')
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
@@ -60,7 +63,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
await toggleArchive(note.id, !note.isArchived)
|
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'))
|
toast.success(note.isArchived ? (t('notes.unarchived') || 'Désarchivée') : (t('notes.archived') || 'Archivée'))
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
@@ -73,7 +76,7 @@ function EditorialNoteMenu({ note, onOpen, onOpenHistory }: {
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
await togglePin(note.id, !note.isPinned)
|
await togglePin(note.id, !note.isPinned)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
}
|
}
|
||||||
@@ -123,6 +126,73 @@ function stringToHue(s: string): number {
|
|||||||
return h % 360
|
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 */
|
/** SVG thumbnail for notes without an image */
|
||||||
function NoteThumbnailPlaceholder({ title, noteId }: { title: string; noteId: string }) {
|
function NoteThumbnailPlaceholder({ title, noteId }: { title: string; noteId: string }) {
|
||||||
// Try to extract the first emoji from the title
|
// Try to extract the first emoji from the title
|
||||||
@@ -180,13 +250,24 @@ export function NotesEditorialView({
|
|||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
}: NotesEditorialViewProps) {
|
}: NotesEditorialViewProps) {
|
||||||
const { t } = useLanguage()
|
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 (
|
return (
|
||||||
<div className="mx-auto w-full max-w-3xl space-y-16">
|
<div className="mx-auto w-full max-w-3xl space-y-16">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{notes.map((note: Note, index: number) => {
|
{notes.map((note: Note, index: number) => {
|
||||||
const title = getNoteDisplayTitle(note, t('notes.untitled') || 'Untitled')
|
const title = getNoteDisplayTitle(note, t('notes.untitled') || 'Untitled')
|
||||||
const img = getNoteFeedImage(note)
|
|
||||||
const excerpt = getNotePlainExcerpt(note)
|
const excerpt = getNotePlainExcerpt(note)
|
||||||
const dateStr = formatNoteDate(note.createdAt)
|
const dateStr = formatNoteDate(note.createdAt)
|
||||||
|
|
||||||
@@ -215,17 +296,7 @@ export function NotesEditorialView({
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 items-start">
|
<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">
|
<EditorialThumbnail note={note} title={title} aiIllustrationEnabled={aiIllustrationEnabled} />
|
||||||
{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>
|
|
||||||
<div className="space-y-3 flex-1">
|
<div className="space-y-3 flex-1">
|
||||||
{excerpt ? (
|
{excerpt ? (
|
||||||
<p className="text-[14px] leading-relaxed text-foreground/80 font-light max-w-lg line-clamp-4">
|
<p className="text-[14px] leading-relaxed text-foreground/80 font-light max-w-lg line-clamp-4">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState, useTransition, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useState, useTransition, useRef } from 'react'
|
||||||
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
||||||
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
@@ -635,7 +636,7 @@ export function NotesTabsView({
|
|||||||
onNoteCreated,
|
onNoteCreated,
|
||||||
}: NotesTabsViewProps) {
|
}: NotesTabsViewProps) {
|
||||||
const { t, language } = useLanguage()
|
const { t, language } = useLanguage()
|
||||||
const { triggerRefresh } = useNoteRefreshOptional()
|
const { refreshNotes } = useRefresh()
|
||||||
const [items, setItems] = useState<Note[]>(notes)
|
const [items, setItems] = useState<Note[]>(notes)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const [isCreating, startCreating] = useTransition()
|
const [isCreating, startCreating] = useTransition()
|
||||||
@@ -796,8 +797,8 @@ export function NotesTabsView({
|
|||||||
})
|
})
|
||||||
setSelectedId(newNote.id)
|
setSelectedId(newNote.id)
|
||||||
onNoteCreated?.(newNote)
|
onNoteCreated?.(newNote)
|
||||||
// NOTE: No triggerRefresh() here — the note is already added to items above.
|
// NOTE: No refreshNotes(note.notebookId) here — the note is already added to items above.
|
||||||
// triggerRefresh() would call getAllNotes() which may return stale cache
|
// refreshNotes(note.notebookId) would call getAllNotes() which may return stale cache
|
||||||
// in production (skipRevalidation:true skips cache invalidation).
|
// in production (skipRevalidation:true skips cache invalidation).
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('notes.createFailed') || 'Impossible de créer la note')
|
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))
|
setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: next } : n))
|
||||||
try {
|
try {
|
||||||
await updateNote(note.id, { isPinned: next }, { skipRevalidation: true })
|
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'))
|
toast.success(next ? (t('notes.pinned') || 'Épinglée') : (t('notes.unpinned') || 'Désépinglée'))
|
||||||
} catch {
|
} catch {
|
||||||
setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: note.isPinned } : n))
|
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)
|
await toggleArchive(note.id, true)
|
||||||
setItems((prev) => prev.filter((n) => n.id !== note.id))
|
setItems((prev) => prev.filter((n) => n.id !== note.id))
|
||||||
setSelectedId((prev) => (prev === note.id ? null : prev))
|
setSelectedId((prev) => (prev === note.id ? null : prev))
|
||||||
triggerRefresh()
|
refreshNotes(note.notebookId)
|
||||||
toast.success(t('notes.archived') || 'Note archivée')
|
toast.success(t('notes.archived') || 'Note archivée')
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('notes.archiveFailed') || 'Archivage échoué')
|
toast.error(t('notes.archiveFailed') || 'Archivage échoué')
|
||||||
@@ -1022,7 +1023,7 @@ export function NotesTabsView({
|
|||||||
onArchive={(noteId) => {
|
onArchive={(noteId) => {
|
||||||
setItems((prev) => prev.filter((n) => n.id !== noteId))
|
setItems((prev) => prev.filter((n) => n.id !== noteId))
|
||||||
setSelectedId((prev) => (prev === noteId ? null : prev))
|
setSelectedId((prev) => (prev === noteId ? null : prev))
|
||||||
triggerRefresh()
|
refreshNotes(selected?.notebookId)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Toggle sidebar button — top-right of editor, always visible */}
|
{/* Toggle sidebar button — top-right of editor, always visible */}
|
||||||
@@ -1057,7 +1058,7 @@ export function NotesTabsView({
|
|||||||
} else {
|
} else {
|
||||||
toast.info(t('reminder.removeReminder'))
|
toast.info(t('reminder.removeReminder'))
|
||||||
}
|
}
|
||||||
triggerRefresh()
|
refreshNotes(items.find(n => n.id === noteId)?.notebookId)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
}
|
}
|
||||||
@@ -1110,7 +1111,7 @@ export function NotesTabsView({
|
|||||||
setItems((prev) => prev.filter((n) => n.id !== noteToDelete.id))
|
setItems((prev) => prev.filter((n) => n.id !== noteToDelete.id))
|
||||||
setSelectedId((prev) => (prev === noteToDelete.id ? null : prev))
|
setSelectedId((prev) => (prev === noteToDelete.id ? null : prev))
|
||||||
setNoteToDelete(null)
|
setNoteToDelete(null)
|
||||||
triggerRefresh()
|
refreshNotes(noteToDelete.notebookId)
|
||||||
toast.success(t('notes.deleted'))
|
toast.success(t('notes.deleted'))
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('notes.deleteFailed'))
|
toast.error(t('notes.deleteFailed'))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
|
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
|
||||||
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
|
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
@@ -47,7 +47,7 @@ interface ReminderNote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationPanel() {
|
export function NotificationPanel() {
|
||||||
const { triggerRefresh } = useNoteRefreshOptional()
|
const { refreshNotes } = useRefresh()
|
||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [requests, setRequests] = useState<ShareRequest[]>([])
|
const [requests, setRequests] = useState<ShareRequest[]>([])
|
||||||
@@ -97,7 +97,7 @@ export function NotificationPanel() {
|
|||||||
description: t('collaboration.nowHasAccess', { name: 'Note' }),
|
description: t('collaboration.nowHasAccess', { name: 'Note' }),
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
})
|
})
|
||||||
triggerRefresh()
|
refreshNotes(null)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[NOTIFICATION] Error:', error)
|
console.error('[NOTIFICATION] Error:', error)
|
||||||
@@ -121,7 +121,7 @@ export function NotificationPanel() {
|
|||||||
try {
|
try {
|
||||||
await toggleReminderDone(noteId, done)
|
await toggleReminderDone(noteId, done)
|
||||||
setReminders(prev => prev.map(r => r.id === noteId ? { ...r, isReminderDone: done } : r))
|
setReminders(prev => prev.map(r => r.id === noteId ? { ...r, isReminderDone: done } : r))
|
||||||
triggerRefresh()
|
refreshNotes(null)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('general.error'))
|
toast.error(t('general.error'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||||||
import { togglePin, deleteNote, dismissFromRecent } from '@/app/actions/notes'
|
import { togglePin, deleteNote, dismissFromRecent } from '@/app/actions/notes'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useNotebooks } from '@/context/notebooks-context'
|
import { useNotebooks } from '@/context/notebooks-context'
|
||||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
import { useRefresh } from '@/lib/use-refresh'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { StickyNote } from 'lucide-react'
|
import { StickyNote } from 'lucide-react'
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ function CompactCard({
|
|||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||||
const { triggerRefresh } = useNoteRefresh()
|
const { refreshNotes } = useRefresh()
|
||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
const [showNotebookMenu, setShowNotebookMenu] = useState(false)
|
||||||
const [, startTransition] = useTransition()
|
const [, startTransition] = useTransition()
|
||||||
@@ -86,7 +86,7 @@ function CompactCard({
|
|||||||
await togglePin(note.id, newPinnedState)
|
await togglePin(note.id, newPinnedState)
|
||||||
|
|
||||||
// Trigger global refresh to update lists
|
// Trigger global refresh to update lists
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
|
|
||||||
if (newPinnedState) {
|
if (newPinnedState) {
|
||||||
@@ -100,7 +100,7 @@ function CompactCard({
|
|||||||
const handleMoveToNotebook = async (notebookId: string | null) => {
|
const handleMoveToNotebook = async (notebookId: string | null) => {
|
||||||
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
await moveNoteToNotebookOptimistic(note.id, notebookId)
|
||||||
setShowNotebookMenu(false)
|
setShowNotebookMenu(false)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (e: React.MouseEvent) => {
|
const handleDelete = async (e: React.MouseEvent) => {
|
||||||
@@ -112,7 +112,7 @@ function CompactCard({
|
|||||||
setIsDeleting(true)
|
setIsDeleting(true)
|
||||||
try {
|
try {
|
||||||
await deleteNote(note.id)
|
await deleteNote(note.id)
|
||||||
triggerRefresh()
|
refreshNotes(note?.notebookId)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete note:', error)
|
console.error('Failed to delete note:', error)
|
||||||
@@ -135,7 +135,7 @@ function CompactCard({
|
|||||||
try {
|
try {
|
||||||
await dismissFromRecent(note.id)
|
await dismissFromRecent(note.id)
|
||||||
// Don't refresh list to prevent immediate replacement
|
// Don't refresh list to prevent immediate replacement
|
||||||
// triggerRefresh()
|
// refreshNotes(note?.notebookId)
|
||||||
// router.refresh()
|
// router.refresh()
|
||||||
toast.success(t('notes.dismissed') || 'Note dismissed from recent')
|
toast.success(t('notes.dismissed') || 'Note dismissed from recent')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
'use client'
|
'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 {
|
interface NoteRefreshContextType {
|
||||||
refreshKey: number
|
refreshKey: number
|
||||||
triggerRefresh: () => void
|
triggerRefresh: () => void
|
||||||
@@ -11,7 +23,7 @@ interface NoteRefreshContextType {
|
|||||||
|
|
||||||
const NoteRefreshContext = createContext<NoteRefreshContextType | undefined>(undefined)
|
const NoteRefreshContext = createContext<NoteRefreshContextType | undefined>(undefined)
|
||||||
|
|
||||||
export function NoteRefreshProvider({ children }: { children: React.ReactNode }) {
|
export function NoteRefreshProvider({ children }: { children: ReactNode }) {
|
||||||
const [refreshKey, setRefreshKey] = useState(0)
|
const [refreshKey, setRefreshKey] = useState(0)
|
||||||
const [notebooksRefreshKey, setNotebooksRefreshKey] = 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() {
|
export function useNoteRefresh() {
|
||||||
const context = useContext(NoteRefreshContext)
|
const context = useContext(NoteRefreshContext)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -40,6 +56,10 @@ export function useNoteRefresh() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `useRefresh()` from `@/lib/use-refresh` instead.
|
||||||
|
* This hook is kept for backward compatibility during migration.
|
||||||
|
*/
|
||||||
export function useNoteRefreshOptional(): NoteRefreshContextType {
|
export function useNoteRefreshOptional(): NoteRefreshContextType {
|
||||||
const context = useContext(NoteRefreshContext)
|
const context = useContext(NoteRefreshContext)
|
||||||
return context ?? { refreshKey: 0, triggerRefresh: () => {}, notebooksRefreshKey: 0, triggerNotebooksRefresh: () => {} }
|
return context ?? { refreshKey: 0, triggerRefresh: () => {}, notebooksRefreshKey: 0, triggerNotebooksRefresh: () => {} }
|
||||||
|
|||||||
Reference in New Issue
Block a user