perf: memo GridCard, fuse save fns, fix slash tab active color
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react'
|
||||
import { useMemo, useState, useEffect, useCallback, memo } from 'react'
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
@@ -103,27 +103,46 @@ function NoteLabelsRow({
|
||||
function NoteGridIllustrationButton({
|
||||
busy,
|
||||
onClick,
|
||||
onDelete,
|
||||
hasIllustration,
|
||||
className,
|
||||
}: {
|
||||
busy: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
onDelete?: (e: React.MouseEvent) => void
|
||||
hasIllustration?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
return (
|
||||
<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={cn(
|
||||
'absolute bottom-2 end-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',
|
||||
className,
|
||||
<div className={cn('absolute bottom-2 end-2 flex items-center gap-1.5 z-10', className)}>
|
||||
{hasIllustration && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('notes.deleteIllustration') || 'Supprimer l\'illustration'}
|
||||
title={t('notes.deleteIllustration') || 'Supprimer l\'illustration'}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 shadow-card-rest backdrop-blur-sm transition-colors"
|
||||
onClick={onDelete}
|
||||
disabled={busy}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4 text-primary" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={hasIllustration
|
||||
? (t('notes.regenerateIllustration') || 'Regénérer l\'illustration')
|
||||
: (t('notes.generateIllustration') || 'Générer une illustration IA')}
|
||||
title={hasIllustration
|
||||
? (t('notes.regenerateIllustration') || 'Regénérer')
|
||||
: (t('notes.generateIllustration') || 'Générer une illustration IA')}
|
||||
className="flex h-8 w-8 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"
|
||||
onClick={onClick}
|
||||
disabled={busy}
|
||||
>
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Sparkles className="h-3.5 w-3.5 text-primary" />}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,10 +150,12 @@ function NoteGridThumbnail({
|
||||
note,
|
||||
aiIllustrationEnabled,
|
||||
onNoteIllustrationGenerated,
|
||||
onNoteIllustrationDeleted,
|
||||
}: {
|
||||
note: Note
|
||||
aiIllustrationEnabled?: boolean
|
||||
onNoteIllustrationGenerated?: (noteId: string) => void | Promise<void>
|
||||
onNoteIllustrationDeleted?: (noteId: string) => void | Promise<void>
|
||||
}) {
|
||||
const { t } = useLanguage()
|
||||
const [busy, setBusy] = useState(false)
|
||||
@@ -142,7 +163,7 @@ function NoteGridThumbnail({
|
||||
|
||||
const handleGenerateSvg = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!aiIllustrationEnabled || busy || img) return
|
||||
if (!aiIllustrationEnabled || busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
const res = await generateNoteIllustrationSvg(note.id, { skipRevalidation: true })
|
||||
@@ -157,7 +178,22 @@ function NoteGridThumbnail({
|
||||
}
|
||||
}
|
||||
|
||||
const aiButtonClass = 'opacity-0 group-hover/card:opacity-100 focus-visible:opacity-100'
|
||||
const handleDeleteSvg = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (busy) return
|
||||
setBusy(true)
|
||||
try {
|
||||
const { updateNote } = await import('@/app/actions/notes')
|
||||
await updateNote(note.id, { illustrationSvg: null })
|
||||
await onNoteIllustrationDeleted?.(note.id)
|
||||
} catch {
|
||||
toast.error('Erreur lors de la suppression')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const aiButtonClass = 'opacity-0 group-hover/card:opacity-100 focus-visible:opacity-100 transition-opacity duration-200'
|
||||
|
||||
if (img) {
|
||||
return (
|
||||
@@ -177,7 +213,13 @@ function NoteGridThumbnail({
|
||||
aria-hidden
|
||||
/>
|
||||
{aiIllustrationEnabled && (
|
||||
<NoteGridIllustrationButton busy={busy} onClick={handleGenerateSvg} className={aiButtonClass} />
|
||||
<NoteGridIllustrationButton
|
||||
busy={busy}
|
||||
onClick={handleGenerateSvg}
|
||||
onDelete={handleDeleteSvg}
|
||||
hasIllustration
|
||||
className={aiButtonClass}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -188,12 +230,17 @@ function NoteGridThumbnail({
|
||||
<FileText size={28} strokeWidth={1.25} />
|
||||
</div>
|
||||
{aiIllustrationEnabled && (
|
||||
<NoteGridIllustrationButton busy={busy} onClick={handleGenerateSvg} className={aiButtonClass} />
|
||||
<NoteGridIllustrationButton
|
||||
busy={busy}
|
||||
onClick={handleGenerateSvg}
|
||||
className={aiButtonClass}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export type { NoteCollectionActions } from '@/lib/note-change-sync'
|
||||
|
||||
type NotesListViewsProps = {
|
||||
@@ -454,6 +501,7 @@ type GridCardSharedProps = {
|
||||
onDeleteNote?: (note: Note) => void | Promise<void>
|
||||
onMoveToNotebook?: (note: Note, notebookId: string | null) => void | Promise<void>
|
||||
onNoteIllustrationGenerated?: (noteId: string) => void | Promise<void>
|
||||
onNoteIllustrationDeleted?: (noteId: string) => void | Promise<void>
|
||||
isOverlay?: boolean
|
||||
}
|
||||
|
||||
@@ -623,7 +671,7 @@ function NotesGridSection({
|
||||
)
|
||||
}
|
||||
|
||||
function SortableGridCard(props: GridCardSharedProps) {
|
||||
const SortableGridCard = memo(function SortableGridCard(props: GridCardSharedProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: props.note.id,
|
||||
})
|
||||
@@ -647,9 +695,9 @@ function SortableGridCard(props: GridCardSharedProps) {
|
||||
<GridCard {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function GridCard({
|
||||
const GridCard = memo(function GridCard({
|
||||
note,
|
||||
index,
|
||||
untitled,
|
||||
@@ -661,6 +709,7 @@ function GridCard({
|
||||
onDeleteNote,
|
||||
onMoveToNotebook,
|
||||
onNoteIllustrationGenerated,
|
||||
onNoteIllustrationDeleted,
|
||||
isOverlay = false,
|
||||
}: GridCardSharedProps) {
|
||||
const router = useRouter()
|
||||
@@ -699,6 +748,7 @@ function GridCard({
|
||||
note={note}
|
||||
aiIllustrationEnabled={aiIllustrationEnabled}
|
||||
onNoteIllustrationGenerated={onNoteIllustrationGenerated}
|
||||
onNoteIllustrationDeleted={onNoteIllustrationDeleted}
|
||||
/>
|
||||
{note.isPinned && (
|
||||
<div className="absolute top-3 start-3 bg-background/90 backdrop-blur-sm p-1.5 rounded-full shadow-sm border border-border/40 text-amber-500">
|
||||
@@ -771,4 +821,4 @@ function GridCard({
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user