perf: memo GridCard, fuse save fns, fix slash tab active color
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m32s
CI / Deploy production (on server) (push) Has been skipped

This commit is contained in:
Antigravity
2026-06-14 14:06:05 +00:00
parent a8785ed4f1
commit a623454347
120 changed files with 12301 additions and 785 deletions

View File

@@ -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>
)
}
})