Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { Note } from '@/lib/types'
|
|
import type { NotebookSchemaPayload, NotePropertyValues } from '@/lib/structured-views/types'
|
|
import { formatPropertyDisplay } from '@/lib/structured-views/property-utils'
|
|
import { getNoteDisplayTitle, getNoteFeedImage } from '@/lib/note-preview'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
type NotesGalleryViewProps = {
|
|
notes: Note[]
|
|
schema: NotebookSchemaPayload
|
|
noteValues: Record<string, NotePropertyValues>
|
|
notebookColor?: string | null
|
|
onOpen: (note: Note) => void
|
|
}
|
|
|
|
export function NotesGalleryView({
|
|
notes,
|
|
schema,
|
|
noteValues,
|
|
notebookColor,
|
|
onOpen,
|
|
}: NotesGalleryViewProps) {
|
|
const { t } = useLanguage()
|
|
const untitled = t('notes.untitled')
|
|
const previewProps = schema.properties.slice(0, 2)
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 max-w-7xl mx-auto">
|
|
{notes.map((note) => (
|
|
<GalleryCard
|
|
key={note.id}
|
|
note={note}
|
|
title={getNoteDisplayTitle(note, untitled)}
|
|
image={getNoteFeedImage(note)}
|
|
notebookColor={notebookColor}
|
|
previewProps={previewProps}
|
|
allProps={schema.properties}
|
|
values={noteValues[note.id] ?? {}}
|
|
onOpen={() => onOpen(note)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function GalleryCard({
|
|
note,
|
|
title,
|
|
image,
|
|
notebookColor,
|
|
previewProps,
|
|
allProps,
|
|
values,
|
|
onOpen,
|
|
}: {
|
|
note: Note
|
|
title: string
|
|
image: string | null
|
|
notebookColor?: string | null
|
|
previewProps: NotebookSchemaPayload['properties']
|
|
allProps: NotebookSchemaPayload['properties']
|
|
values: NotePropertyValues
|
|
onOpen: () => void
|
|
}) {
|
|
const [hover, setHover] = useState(false)
|
|
const accent = notebookColor || '#A47148'
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onOpen}
|
|
onMouseEnter={() => setHover(true)}
|
|
onMouseLeave={() => setHover(false)}
|
|
className="text-left rounded-2xl border border-border/40 bg-card/40 overflow-hidden shadow-sm hover:shadow-md hover:border-border transition-all group"
|
|
>
|
|
<div
|
|
className="aspect-[4/3] relative overflow-hidden"
|
|
style={{ backgroundColor: `${accent}18` }}
|
|
>
|
|
{image ? (
|
|
<img src={image} alt="" className="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" />
|
|
) : note.illustrationSvg ? (
|
|
<div
|
|
className="w-full h-full p-4 [&_svg]:w-full [&_svg]:h-full opacity-80"
|
|
dangerouslySetInnerHTML={{ __html: note.illustrationSvg }}
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<span
|
|
className="font-memento-serif text-4xl opacity-20"
|
|
style={{ color: accent }}
|
|
>
|
|
{title.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="p-4 space-y-2">
|
|
<h3 className="font-memento-serif text-[15px] font-medium line-clamp-2 group-hover:text-brand-accent transition-colors">
|
|
{title}
|
|
</h3>
|
|
{!hover && previewProps.length > 0 && (
|
|
<div className="space-y-1">
|
|
{previewProps.map((p) => (
|
|
<div key={p.id} className="flex gap-2 text-[11px]">
|
|
<span className="text-muted-foreground shrink-0">{p.name}:</span>
|
|
<span className="truncate">{formatPropertyDisplay(p.type, values[p.id])}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{hover && allProps.length > 0 ? (
|
|
<div className="space-y-1.5 pt-1 border-t border-border/30">
|
|
{allProps.map((p) => (
|
|
<div key={p.id} className="flex justify-between gap-2 text-[11px]">
|
|
<span className="text-muted-foreground">{p.name}</span>
|
|
<span className="font-medium text-right truncate max-w-[55%]">
|
|
{formatPropertyDisplay(p.type, values[p.id])}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</button>
|
|
)
|
|
}
|