diff --git a/memento-note/app/(main)/page.tsx b/memento-note/app/(main)/page.tsx index f94863a..5909ea7 100644 --- a/memento-note/app/(main)/page.tsx +++ b/memento-note/app/(main)/page.tsx @@ -8,19 +8,11 @@ export default async function HomePage() { getAISettings(), ]) - const notesViewMode = - settings?.notesViewMode === 'masonry' - ? ('masonry' as const) - : settings?.notesViewMode === 'tabs' - ? ('tabs' as const) - : ('masonry' as const) - return ( ( initialNotes.filter(n => n.isPinned) ) - const [notesViewMode, setNotesViewMode] = useState(initialSettings.notesViewMode) const [noteHistoryMode] = useState<'manual' | 'auto'>(initialSettings.noteHistoryMode) const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null) const [isLoading, setIsLoading] = useState(false) @@ -98,16 +95,15 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { } }, [shouldSuggestLabels, suggestNotebookId]) - // Sidebar carnet / inbox: forceList → liste éditoriale + fermer l'éditeur plein écran (comme la ref. architectural-grid) + // Sidebar carnet / inbox: fermer l'éditeur plein écran (comme la ref. architectural-grid) useEffect(() => { - const forceList = searchParams.get('forceList') - if (forceList !== '1') return - setNotesViewMode(prev => (prev === 'tabs' ? 'masonry' : prev)) - setEditingNote(null) - const params = new URLSearchParams(searchParams.toString()) - params.delete('forceList') - const newUrl = params.toString() ? `/?${params.toString()}` : '/' - router.replace(newUrl, { scroll: false }) + if (searchParams.get('forceList') === '1') { + setEditingNote(null) + const params = new URLSearchParams(searchParams.toString()) + params.delete('forceList') + const newUrl = params.toString() ? `/?${params.toString()}` : '/' + router.replace(newUrl, { scroll: false }) + } }, [searchParams, router]) const notebookFilter = searchParams.get('notebook') @@ -234,7 +230,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { useEffect(() => { const handler = (e: Event) => { const { name } = (e as CustomEvent).detail - if (!name) return const removeLabel = (note: Note) => { const currentLabels = note.labels || [] const updated = currentLabels.filter((l) => l.toLowerCase() !== name.toLowerCase()) @@ -346,13 +341,13 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { return trail }, [currentNotebook, notebooks]) + useEffect(() => { setControls({ - isTabsMode: notesViewMode === 'tabs', openNoteComposer: () => handleAddNote(), }) return () => setControls(null) - }, [notesViewMode, setControls]) + }, [setControls]) // Apply sort order to notes const sortedNotes = useMemo(() => { @@ -373,8 +368,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { alpha: t('sidebar.sortAlpha') || 'A → Z', } - const isTabs = notesViewMode === 'tabs' - const isEditorialMode = !isTabs const handleEditorClose = useCallback(() => { setEditingNote(null) @@ -399,8 +392,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { return (
{editingNote ? ( @@ -413,10 +405,9 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) { /> ) : (
-
+
{currentNotebook && notebookPath.length > 0 && ( @@ -571,18 +562,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
{isLoading ? (
{t('general.loading')}
- ) : isTabs ? ( - setEditingNote({ note, readOnly })} - onSizeChange={handleSizeChange} - currentNotebookId={searchParams.get('notebook')} - noteHistoryMode={noteHistoryMode} - onOpenHistory={handleOpenHistory} - onEnableHistory={handleEnableHistory} - onNoteCreated={handleNoteCreated} - /> ) : (
{sortedPinnedNotes.length > 0 && ( diff --git a/memento-note/components/masonry-grid.css b/memento-note/components/masonry-grid.css deleted file mode 100644 index 1f89b3c..0000000 --- a/memento-note/components/masonry-grid.css +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Masonry Grid — Deux modes d'affichage : - * 1. Variable : CSS Grid avec tailles small/medium/large - * 2. Uniform : CSS Columns masonry (comme Google Keep) - */ - -/* ─── Container ──────────────────────────────────── */ -.masonry-container { - width: 100%; - padding: 0 8px 40px 8px; -} - -/* ═══════════════════════════════════════════════════ - MODE 1 : VARIABLE (CSS Grid avec tailles différentes) - ═══════════════════════════════════════════════════ */ - -.masonry-css-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - grid-auto-rows: auto; - gap: 12px; - align-items: start; - grid-auto-flow: dense; -} - -.masonry-sortable-item[data-size="medium"] { - grid-column: span 2; -} - -.masonry-sortable-item[data-size="large"] { - grid-column: span 3; -} - -/* ═══════════════════════════════════════════════════ - MODE 2 : UNIFORM — CSS Columns masonry (Google Keep) - ═══════════════════════════════════════════════════ */ - -.masonry-container[data-card-size-mode="uniform"] .masonry-css-grid { - display: block; - column-width: 240px; - column-gap: 12px; - orphans: 1; - widows: 1; -} - -.masonry-container[data-card-size-mode="uniform"] .masonry-sortable-item, -.masonry-container[data-card-size-mode="uniform"] .masonry-sortable-item[data-size="medium"], -.masonry-container[data-card-size-mode="uniform"] .masonry-sortable-item[data-size="large"] { - break-inside: avoid; - margin-bottom: 12px; - display: inline-block; - width: 100%; - grid-column: unset; -} - -/* ─── Sortable items ─────────────────────────────── */ -.masonry-sortable-item { - break-inside: avoid; - box-sizing: border-box; - will-change: transform; - transition: opacity 0.15s ease-out; -} - -/* ─── Note card base ─────────────────────────────── */ -.note-card { - width: 100% !important; - min-width: 0; - box-sizing: border-box; -} - -/* ─── Drag overlay ───────────────────────────────── */ -.masonry-drag-overlay { - cursor: grabbing; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25), 0 8px 16px rgba(0, 0, 0, 0.15); - border-radius: 12px; - opacity: 0.95; - pointer-events: none; -} - -/* ─── Mobile (< 480px) ───────────────────────────── */ -@media (max-width: 479px) { - .masonry-css-grid { - grid-template-columns: 1fr; - gap: 10px; - } - - .masonry-sortable-item[data-size="medium"], - .masonry-sortable-item[data-size="large"] { - grid-column: span 1; - } - - .masonry-container[data-card-size-mode="uniform"] .masonry-css-grid { - column-width: 100%; - column-gap: 10px; - } - - .masonry-container { - padding: 0 4px 16px 4px; - } -} - -/* ─── Small tablet (480–767px) ───────────────────── */ -@media (min-width: 480px) and (max-width: 767px) { - .masonry-css-grid { - grid-template-columns: repeat(2, 1fr); - gap: 10px; - } - - .masonry-sortable-item[data-size="large"] { - grid-column: span 2; - } - - .masonry-container { - padding: 0 8px 20px 8px; - } -} - -/* ─── Tablet (768–1023px) ────────────────────────── */ -@media (min-width: 768px) and (max-width: 1023px) { - .masonry-css-grid { - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 12px; - } -} - -/* ─── Desktop (1024–1279px) ─────────────────────── */ -@media (min-width: 1024px) and (max-width: 1279px) { - .masonry-css-grid { - grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); - gap: 12px; - } -} - -/* ─── Large Desktop (1280px+) ───────────────────── */ -@media (min-width: 1280px) { - .masonry-css-grid { - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 14px; - } - - .masonry-container { - max-width: 1600px; - margin: 0 auto; - padding: 0 12px 32px 12px; - } -} - -/* ─── Print ──────────────────────────────────────── */ -@media print { - .masonry-sortable-item { - break-inside: avoid; - page-break-inside: avoid; - } -} - -/* ─── Reduced motion ─────────────────────────────── */ -@media (prefers-reduced-motion: reduce) { - .masonry-sortable-item { - transition: none; - } -} diff --git a/memento-note/components/masonry-grid.tsx b/memento-note/components/masonry-grid.tsx deleted file mode 100644 index afb6541..0000000 --- a/memento-note/components/masonry-grid.tsx +++ /dev/null @@ -1,346 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback, memo, useMemo, useRef } from 'react'; -import { - DndContext, - DragEndEvent, - DragOverlay, - DragStartEvent, - PointerSensor, - TouchSensor, - closestCenter, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import { - SortableContext, - arrayMove, - rectSortingStrategy, - useSortable, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; -import { Note } from '@/lib/types'; -import { NoteCard } from './note-card'; -import { updateFullOrderWithoutRevalidation } from '@/app/actions/notes'; -import { useEditorUI } from '@/context/editor-ui-context'; -import { useLanguage } from '@/lib/i18n'; -import { useCardSizeMode } from '@/hooks/use-card-size-mode'; -import dynamic from 'next/dynamic'; -import './masonry-grid.css'; - -// Lazy-load NoteEditor — uniquement chargé au clic -const NoteEditor = dynamic( - () => import('./note-editor').then(m => ({ default: m.NoteEditor })), - { ssr: false } -); - -interface MasonryGridProps { - notes: Note[]; - onEdit?: (note: Note, readOnly?: boolean) => void; - onSizeChange?: (noteId: string, size: 'small' | 'medium' | 'large') => void; - isTrashView?: boolean; - noteHistoryEnabled?: boolean; - noteHistoryMode?: 'manual' | 'auto'; - onOpenHistory?: (note: Note) => void; -} - -// ───────────────────────────────────────────── -// Sortable Note Item -// ───────────────────────────────────────────── -interface SortableNoteProps { - note: Note; - onEdit: (note: Note, readOnly?: boolean) => void; - onSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void; - onDragStartNote?: (noteId: string) => void; - onDragEndNote?: () => void; - isDragging?: boolean; - isOverlay?: boolean; - isTrashView?: boolean; - noteHistoryEnabled?: boolean; - onOpenHistory?: (note: Note) => void; -} - -const SortableNoteItem = memo(function SortableNoteItem({ - note, - onEdit, - onSizeChange, - onDragStartNote, - onDragEndNote, - isDragging, - isOverlay, - isTrashView, - noteHistoryEnabled, - onOpenHistory, -}: SortableNoteProps) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging: isSortableDragging, - } = useSortable({ id: note.id }); - - const style: React.CSSProperties = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isSortableDragging && !isOverlay ? 0.3 : 1, - }; - - return ( -
- onSizeChange(note.id, newSize)} - noteHistoryEnabled={noteHistoryEnabled} - onOpenHistory={onOpenHistory} - /> -
- ); -}) - -// ───────────────────────────────────────────── -// Sortable Grid Section (pinned or others) -// ───────────────────────────────────────────── -interface SortableGridSectionProps { - notes: Note[]; - onEdit: (note: Note, readOnly?: boolean) => void; - onSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void; - draggedNoteId: string | null; - onDragStartNote: (noteId: string) => void; - onDragEndNote: () => void; - isTrashView?: boolean; - noteHistoryEnabled?: boolean; - onOpenHistory?: (note: Note) => void; -} - -const SortableGridSection = memo(function SortableGridSection({ - notes, - onEdit, - onSizeChange, - draggedNoteId, - onDragStartNote, - onDragEndNote, - isTrashView, - noteHistoryEnabled, - onOpenHistory, -}: SortableGridSectionProps) { - const ids = useMemo(() => notes.map(n => n.id), [notes]); - - return ( - -
- {notes.map(note => ( - - ))} -
-
- ); -}); - -// ───────────────────────────────────────────── -// Main MasonryGrid component -// ───────────────────────────────────────────── -export function MasonryGrid({ - notes, - onEdit, - onSizeChange, - isTrashView, - noteHistoryEnabled = false, - onOpenHistory, -}: MasonryGridProps) { - const { t } = useLanguage(); - const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null); - const { startDrag, endDrag, draggedNoteId } = useEditorUI(); - const cardSizeMode = useCardSizeMode(); - const isUniformMode = cardSizeMode === 'uniform'; - - // Local notes state for optimistic size/order updates - const [localNotes, setLocalNotes] = useState(notes); - const prevNotesRef = useRef(notes); - - if (notes !== prevNotesRef.current) { - const localSizeMap = new Map(localNotes.map(n => [n.id, n.size])); - const localOrderMap = new Map(localNotes.map((n, i) => [n.id, i])); - const newLocalNotes = notes.map(n => ({ ...n, size: localSizeMap.get(n.id) ?? n.size })); - newLocalNotes.sort((a, b) => { - const oA = localOrderMap.get(a.id) - const oB = localOrderMap.get(b.id) - if (oA !== undefined && oB !== undefined) return oA - oB - if (oA !== undefined) return -1 - if (oB !== undefined) return 1 - return 0 - }) - setLocalNotes(newLocalNotes); - prevNotesRef.current = notes; - } - - const pinnedNotes = useMemo(() => localNotes.filter(n => n.isPinned), [localNotes]); - const othersNotes = useMemo(() => localNotes.filter(n => !n.isPinned), [localNotes]); - - const [activeId, setActiveId] = useState(null); - const activeNote = useMemo( - () => localNotes.find(n => n.id === activeId) ?? null, - [localNotes, activeId] - ); - - const handleEdit = useCallback((note: Note, readOnly?: boolean) => { - if (onEdit) { - onEdit(note, readOnly); - } else { - setEditingNote({ note, readOnly }); - } - }, [onEdit]); - - const handleSizeChange = useCallback((noteId: string, newSize: 'small' | 'medium' | 'large') => { - setLocalNotes(prev => prev.map(n => n.id === noteId ? { ...n, size: newSize } : n)); - onSizeChange?.(noteId, newSize); - }, [onSizeChange]); - - // @dnd-kit sensors — pointer (desktop) + touch (mobile) - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { distance: 8 }, // Évite les activations accidentelles - }), - useSensor(TouchSensor, { - activationConstraint: { delay: 200, tolerance: 8 }, // Long-press sur mobile - }) - ); - - const localNotesRef = useRef(localNotes) - useEffect(() => { - localNotesRef.current = localNotes - }, [localNotes]) - - const handleDragStart = useCallback((event: DragStartEvent) => { - setActiveId(event.active.id as string); - startDrag(event.active.id as string); - }, [startDrag]); - - const handleDragEnd = useCallback(async (event: DragEndEvent) => { - const { active, over } = event; - setActiveId(null); - endDrag(); - - if (!over || active.id === over.id) return; - - const current = localNotesRef.current - const activeIdx = current.findIndex(n => n.id === active.id) - const overIdx = current.findIndex(n => n.id === over.id) - if (activeIdx === -1 || overIdx === -1) return - - const activeNote = current[activeIdx] - const overNote = current[overIdx] - - if (activeNote.isPinned !== overNote.isPinned) return - - const reordered = arrayMove(current, activeIdx, overIdx); - if (reordered.length === 0) return; - - setLocalNotes(reordered); - const ids = reordered.map(n => n.id); - updateFullOrderWithoutRevalidation(ids).catch(err => { - console.error('Failed to persist order:', err); - }); - }, [endDrag]); - - return ( - -
- {pinnedNotes.length > 0 && ( -
-

- {t('notes.pinned')} -

- -
- )} - - {othersNotes.length > 0 && ( -
- {pinnedNotes.length > 0 && ( -

- {t('notes.others')} -

- )} - -
- )} -
- - {/* DragOverlay — montre une copie flottante pendant le drag */} - - {activeNote ? ( -
- handleSizeChange(activeNote.id, newSize)} - noteHistoryEnabled={noteHistoryEnabled} - onOpenHistory={onOpenHistory} - /> -
- ) : null} -
- - {editingNote && ( - setEditingNote(null)} - /> - )} -
- ); -} diff --git a/memento-note/components/notes-main-section.tsx b/memento-note/components/notes-main-section.tsx deleted file mode 100644 index 95ab56d..0000000 --- a/memento-note/components/notes-main-section.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client' - -import dynamic from 'next/dynamic' -import { Note } from '@/lib/types' -import { NotesTabsView } from '@/components/notes-tabs-view' - -const MasonryGridLazy = dynamic( - () => import('@/components/masonry-grid').then((m) => m.MasonryGrid), - { - ssr: false, - loading: () => ( -
- ), - } -) - -export type NotesViewMode = 'masonry' | 'tabs' - -interface NotesMainSectionProps { - notes: Note[] - viewMode: NotesViewMode - onEdit?: (note: Note, readOnly?: boolean) => void - onSizeChange?: (noteId: string, size: 'small' | 'medium' | 'large') => void - currentNotebookId?: string | null - noteHistoryMode?: 'manual' | 'auto' - onOpenHistory?: (note: Note) => void - onEnableHistory?: (noteId: string) => Promise - onNoteCreated?: (note: Note) => void -} - -export function NotesMainSection({ - notes, - viewMode, - onEdit, - onSizeChange, - currentNotebookId, - noteHistoryMode = 'manual', - onOpenHistory, - onEnableHistory, - onNoteCreated, -}: NotesMainSectionProps) { - if (viewMode === 'tabs') { - return ( -
- -
- ) - } - - return ( -
- -
- ) -} diff --git a/memento-note/components/notes-tabs-view.tsx b/memento-note/components/notes-tabs-view.tsx deleted file mode 100644 index 840de18..0000000 --- a/memento-note/components/notes-tabs-view.tsx +++ /dev/null @@ -1,1141 +0,0 @@ -'use client' - -import { useCallback, useEffect, useMemo, useState, useTransition, useRef } from 'react' -import { useNoteRefreshOptional } from '@/context/NoteRefreshContext' -import { useRefresh } from '@/lib/use-refresh' -import { - DndContext, - type DragEndEvent, - KeyboardSensor, - PointerSensor, - closestCenter, - useSensor, - useSensors, -} from '@dnd-kit/core' -import { - SortableContext, - arrayMove, - verticalListSortingStrategy, - sortableKeyboardCoordinates, - useSortable, -} from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' -import { Note, NOTE_COLORS, NoteColor, NoteType } from '@/lib/types' -import { cn } from '@/lib/utils' -import { NoteInlineEditor } from '@/components/note-inline-editor' -import { useLanguage } from '@/lib/i18n' -import { getNoteDisplayTitle } from '@/lib/note-preview' -import { - updateFullOrderWithoutRevalidation, - createNote, - deleteNote, - updateNote, - toggleArchive, -} from '@/app/actions/notes' -import { useNotebooks } from '@/context/notebooks-context' -import { - GripVertical, - ListChecks, - Pin, - PinOff, - FileText, - Plus, - Loader2, - Trash2, - ListFilter, - FolderInput, - Archive, - Share2, - Check, - Hash, - History, - PanelRightClose, - PanelRightOpen, - PanelLeftClose, - PanelLeftOpen, - Bell, -} from 'lucide-react' -import { Button } from '@/components/ui/button' -import { ReminderDialog } from '@/components/reminder-dialog' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuLabel, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' -import { toast } from 'sonner' -import { format, type Locale } from 'date-fns' -import { fr } from 'date-fns/locale/fr' -import { enUS } from 'date-fns/locale/en-US' - -interface NotesTabsViewProps { - notes: Note[] - onEdit?: (note: Note, readOnly?: boolean) => void - currentNotebookId?: string | null - noteHistoryMode?: 'manual' | 'auto' - onOpenHistory?: (note: Note) => void - onEnableHistory?: (noteId: string) => Promise - onNoteCreated?: (note: Note) => void -} - -type SortOrder = 'date-desc' | 'date-asc' | 'title-asc' | 'title-desc' - -// Color accent strip for each note -const COLOR_ACCENT: Record = { - default: 'bg-primary', - red: 'bg-red-400', - orange: 'bg-orange-400', - yellow: 'bg-amber-400', - green: 'bg-emerald-400', - teal: 'bg-teal-400', - blue: 'bg-slate-400', - purple: 'bg-violet-400', - pink: 'bg-fuchsia-400', - gray: 'bg-gray-400', -} - -// Background tint gradient for selected note panel -const COLOR_PANEL_BG: Record = { - default: 'from-background to-background', - red: 'from-red-50/60 dark:from-red-950/20 to-background', - orange: 'from-orange-50/60 dark:from-orange-950/20 to-background', - yellow: 'from-amber-50/60 dark:from-amber-950/20 to-background', - green: 'from-emerald-50/60 dark:from-emerald-950/20 to-background', - teal: 'from-teal-50/60 dark:from-teal-950/20 to-background', - blue: 'from-slate-50/60 dark:from-slate-950/20 to-background', - purple: 'from-violet-50/60 dark:from-violet-950/20 to-background', - pink: 'from-fuchsia-50/60 dark:from-fuchsia-950/20 to-background', - gray: 'from-gray-50/60 dark:from-gray-900/20 to-background', -} - -const COLOR_ICON: Record = { - default: 'text-primary', - red: 'text-red-500', - orange: 'text-orange-500', - yellow: 'text-amber-500', - green: 'text-emerald-500', - teal: 'text-teal-500', - blue: 'text-slate-600', - purple: 'text-violet-500', - pink: 'text-fuchsia-500', - gray: 'text-gray-500', -} - -function getColorKey(note: Note): NoteColor { - return (typeof note.color === 'string' && note.color in NOTE_COLORS - ? note.color - : 'default') as NoteColor -} - -function getDateLocale(language: string) { - if (language === 'fr') return fr - if (language === 'fa') return require('date-fns/locale').faIR - return enUS -} - -function formatNoteDate(date: Date | string, locale: Locale): string { - const d = typeof date === 'string' ? new Date(date) : date - return format(d, 'd MMM yyyy', { locale }) -} - -function countWords(content: string | null | undefined): number { - if (!content) return 0 - return content.trim().split(/\s+/).filter(Boolean).length -} - -// ─── Sortable List Item ─────────────────────────────────────────────────────── - -function SortableNoteListItem({ - note, - selected, - onSelect, - onDelete, - reorderLabel, - deleteLabel, - language, - untitledLabel, -}: { - note: Note - selected: boolean - onSelect: () => void - onDelete: () => void - reorderLabel: string - deleteLabel: string - language: string - untitledLabel: string -}) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: note.id, - }) - - const style = { - transform: CSS.Transform.toString(transform), - transition, - zIndex: isDragging ? 50 : undefined, - } - - const ck = getColorKey(note) - const title = getNoteDisplayTitle(note, untitledLabel) - const snippet = - note.type === 'checklist' - ? (note.checkItems?.map((i) => i.text).join(' · ') || '').substring(0, 200) - : (note.content || '').substring(0, 200) - - const dateLocale = getDateLocale(language) - const dateStr = formatNoteDate(note.updatedAt, dateLocale) - - return ( -
- {/* Left accent bar — solid when selected, transparent otherwise */} -
- - {/* Main card content */} -
- - {/* Row 1: type icon + date */} -
-
- {note.type === 'checklist' ? ( - - ) : ( - - )} - {note.isPinned && ( - - )} -
- - {dateStr} - -
- - {/* Row 2: title */} -

- {title} -

- - {/* Row 3: snippet */} - {snippet && ( -

- {snippet} -

- )} - - {/* Row 4: label chips */} - {Array.isArray(note.labels) && note.labels.length > 0 && ( -
- {note.labels.slice(0, 3).map((label) => ( - - - {label} - - ))} - {note.labels.length > 3 && ( - - +{note.labels.length - 3} - - )} -
- )} -
- - {/* Actions column: drag + delete on hover */} -
- - -
-
- ) -} - -// ─── Note Meta Sidebar ──────────────────────────────────────────────────────── - -function SidebarSection({ title }: { title: string }) { - return ( -
-

- {title} -

-
-
- ) -} - -function SidebarActionBtn({ - icon, - label, - onClick, - disabled = false, -}: { - icon: React.ReactNode - label: string - onClick: () => void - disabled?: boolean -}) { - return ( - - ) -} - -function NoteMetaSidebar({ - note, - onPinToggle, - onArchive, - onOpenHistory, - onEnableHistory, - onUpdateReminder, -}: { - note: Note - onPinToggle: (note: Note) => void - onArchive: (note: Note) => void - onOpenHistory?: (note: Note) => void - onEnableHistory?: (noteId: string) => Promise - onUpdateReminder?: (noteId: string, reminder: Date | null) => void -}) { - const { t } = useLanguage() - const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks() - const [moveOpen, setMoveOpen] = useState(false) - const [isMoving, setIsMoving] = useState(false) - const [showReminder, setShowReminder] = useState(false) - - // t() returns the key itself when not found — use this wrapper for safe fallbacks - const ts = (key: string, fallback: string) => { - const v = t(key as Parameters[0]) - return v === key ? fallback : v - } - - const wordCount = countWords(note.content) - - const noteTypeLabel = - note.type === 'checklist' - ? ts('notes.typeChecklist', 'Checklist') - : note.isMarkdown - ? ts('notes.typeMarkdown', 'Markdown') - : ts('notes.typeText', 'Text') - - const handleMoveToNotebook = async (notebookId: string | null) => { - setIsMoving(true) - try { - await moveNoteToNotebookOptimistic(note.id, notebookId) - setMoveOpen(false) - toast.success(ts('notebookSuggestion.movedToNotebook', 'Note déplacée')) - } catch { - toast.error(ts('notes.moveFailed', 'Déplacement échoué')) - } finally { - setIsMoving(false) - } - } - - const handleHistory = async () => { - if (!note.historyEnabled) { - if (!onEnableHistory) { - toast.info(ts('notes.historyDisabledDesc', "L'historique est désactivé pour cette note.")) - return - } - try { - await onEnableHistory(note.id) - toast.success(ts('notes.historyEnabled', 'Historique activé')) - onOpenHistory?.(note) - } catch { - toast.error(ts('general.error', 'Erreur')) - } - return - } - - onOpenHistory?.(note) - } - - return ( - - ) -} - -// ─── Main Component ─────────────────────────────────────────────────────────── - -export function NotesTabsView({ - notes, - onEdit, - currentNotebookId, - - noteHistoryMode = 'manual', - onOpenHistory, - onEnableHistory, - onNoteCreated, -}: NotesTabsViewProps) { - const { t, language } = useLanguage() - const { refreshNotes } = useRefresh() - const [items, setItems] = useState(notes) - const [selectedId, setSelectedId] = useState(null) - const [isCreating, startCreating] = useTransition() - const [noteToDelete, setNoteToDelete] = useState(null) - const [sortOrder, setSortOrder] = useState('date-desc') - const [sidebarOpen, setSidebarOpen] = useState(true) - const [listOpen, setListOpen] = useState(true) - - // Auto-hide meta sidebar when AI opens; restore previous state when AI closes - const prevSidebarOpen = useRef(true) - useEffect(() => { - const handler = (e: Event) => { - const visible = (e as CustomEvent).detail - if (visible) { - prevSidebarOpen.current = sidebarOpen - setSidebarOpen(false) - } else { - setSidebarOpen(prevSidebarOpen.current) - } - } - window.addEventListener('contextual-ai-visibility', handler) - return () => window.removeEventListener('contextual-ai-visibility', handler) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sidebarOpen]) - - const prevNotesRef = useRef(notes) - - useEffect(() => { - setItems((prev) => { - const prevIds = prev.map((n) => n.id).join(',') - const incomingIds = notes.map((n) => n.id).join(',') - - const merge = (fresh: Note, local: Note) => { - // Detect if the server explicitly changed content since last sync - const prevServer = prevNotesRef.current.find((n) => n.id === fresh.id) - const serverContentChanged = prevServer ? prevServer.content !== fresh.content : false - const serverTitleChanged = prevServer ? prevServer.title !== fresh.title : false - const serverCheckItemsChanged = prevServer - ? JSON.stringify(prevServer.checkItems) !== JSON.stringify(fresh.checkItems) - : false - const labelsChanged = - JSON.stringify(fresh.labels?.slice().sort()) !== - JSON.stringify(local.labels?.slice().sort()) - - return { - ...fresh, - title: serverTitleChanged ? fresh.title : local.title || fresh.title, - content: serverContentChanged ? fresh.content : local.content, - checkItems: serverCheckItemsChanged ? fresh.checkItems : local.checkItems, - labels: labelsChanged ? fresh.labels : local.labels, - } - } - - let result: Note[] - if (prevIds === incomingIds) { - result = prev.map((local) => { - const fresh = notes.find((n) => n.id === local.id) - return fresh ? merge(fresh, local) : local - }) - } else { - result = notes.map((fresh) => { - const local = prev.find((p) => p.id === fresh.id) - return local ? merge(fresh, local) : fresh - }) - } - - return result - }) - - prevNotesRef.current = notes - }, [notes]) - - useEffect(() => { - if (items.length === 0) { - setSelectedId(null) - return - } - setSelectedId((prev) => - prev && items.some((n) => n.id === prev) ? prev : items[0].id - ) - }, [items]) - - useEffect(() => { - const handler = (e: Event) => { - const { name } = (e as CustomEvent).detail - if (!name) return - setItems((prev) => - prev.map((note) => { - const currentLabels = note.labels || [] - const updated = currentLabels.filter((l) => l.toLowerCase() !== name.toLowerCase()) - if (updated.length === currentLabels.length) return note - return { ...note, labels: updated.length > 0 ? updated : null } - }) - ) - } - window.addEventListener('label-deleted', handler) - return () => window.removeEventListener('label-deleted', handler) - }, []) - - // Sorted display items — pinned notes always float to the top - const sortedItems = useMemo(() => { - const pinned = items.filter(n => n.isPinned) - const unpinned = items.filter(n => !n.isPinned) - const sortFn = (a: Note, b: Note) => { - if (sortOrder === 'date-desc') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() - if (sortOrder === 'date-asc') return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime() - if (sortOrder === 'title-asc') return (a.title || '').localeCompare(b.title || '') - if (sortOrder === 'title-desc') return (b.title || '').localeCompare(a.title || '') - return 0 - } - return [...pinned.sort(sortFn), ...unpinned.sort(sortFn)] - }, [items, sortOrder]) - - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) - ) - - const handleDragEnd = useCallback( - async (event: DragEndEvent) => { - const { active, over } = event - if (!over || active.id === over.id) return - const oldIndex = items.findIndex((n) => n.id === active.id) - const newIndex = items.findIndex((n) => n.id === over.id) - if (oldIndex < 0 || newIndex < 0) return - const reordered = arrayMove(items, oldIndex, newIndex) - setItems(reordered) - try { - await updateFullOrderWithoutRevalidation(reordered.map((n) => n.id)) - } catch { - setItems(notes) - toast.error(t('notes.moveFailed')) - } - }, - [items, notes, t] - ) - - const selected = items.find((n) => n.id === selectedId) ?? null - const colorKey = selected ? getColorKey(selected) : 'default' - - - const handleCreateNote = (noteType: NoteType = 'richtext') => { - startCreating(async () => { - try { - const newNote = await createNote({ - content: '', - type: noteType, - checkItems: noteType === 'checklist' ? [{ id: Date.now().toString(), text: '', checked: false }] : undefined, - title: undefined, - notebookId: currentNotebookId || undefined, - skipRevalidation: true - }) - if (!newNote) return - setItems((prev) => { - const pinned = prev.filter(n => n.isPinned) - const unpinned = prev.filter(n => !n.isPinned) - return [...pinned, newNote, ...unpinned] - }) - setSelectedId(newNote.id) - onNoteCreated?.(newNote) - // NOTE: No refreshNotes(note.notebookId) here — the note is already added to items above. - // refreshNotes(note.notebookId) would call getAllNotes() which may return stale cache - // in production (skipRevalidation:true skips cache invalidation). - } catch { - toast.error(t('notes.createFailed') || 'Impossible de créer la note') - } - }) - } - - const handlePinToggle = async (note: Note) => { - const next = !note.isPinned - setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: next } : n)) - try { - await updateNote(note.id, { isPinned: next }, { skipRevalidation: true }) - refreshNotes(note.notebookId) - toast.success(next ? (t('notes.pinned') || 'Épinglée') : (t('notes.unpinned') || 'Désépinglée')) - } catch { - setItems((prev) => prev.map((n) => n.id === note.id ? { ...n, isPinned: note.isPinned } : n)) - toast.error(t('notes.updateFailed') || 'Mise à jour échouée') - } - } - - const handleArchive = async (note: Note) => { - try { - await toggleArchive(note.id, true) - setItems((prev) => prev.filter((n) => n.id !== note.id)) - setSelectedId((prev) => (prev === note.id ? null : prev)) - refreshNotes(note.notebookId) - toast.success(t('notes.archived') || 'Note archivée') - } catch { - toast.error(t('notes.archiveFailed') || 'Archivage échoué') - } - } - - return ( -
- {/* ── Left panel: note list ── */} -
- - {/* Header — always visible */} -
- {listOpen && ( -
- - {t('notes.title')} - - - {items.length} - -
- )} -
- {/* Sort / filter button */} - - - - - - - {t('notes.sortBy') || 'Trier par'} - - - setSortOrder(v as SortOrder)}> - - {t('notes.sortDateDesc') || 'Date (récent)'} - - - {t('notes.sortDateAsc') || 'Date (ancien)'} - - - {t('notes.sortTitleAsc') || 'Titre A → Z'} - - - {t('notes.sortTitleDesc') || 'Titre Z → A'} - - - - - - {/* New note button — dropdown to choose type */} - - - - - - handleCreateNote('richtext')}> - - {t('notes.newNote') || 'Note'} - - handleCreateNote('checklist')}> - - {t('notes.newChecklist') || 'Checklist'} - - - - {/* Collapse list panel */} - {listOpen && ( - - )} -
-
- - {/* Scrollable note list */} - {listOpen && ( -
- {items.length === 0 ? ( -
-
- -
-

{t('notes.emptyStateTabs') || 'Aucune note'}

-

{t('notes.createFirst') || 'Créez votre première note'}

-
- ) : ( - - n.id)} - strategy={verticalListSortingStrategy} - > -
- {sortedItems.map((note) => ( - setSelectedId(note.id)} - onDelete={() => setNoteToDelete(note)} - reorderLabel={t('notes.reorderTabs')} - deleteLabel={t('notes.delete')} - language={language} - untitledLabel={t('notes.untitled')} - /> - ))} -
-
-
- )} -
- )} - {/* Expand button shown in collapsed state */} - {!listOpen && ( -
- -
- )} -
- - {/* ── Right content panel ── */} - {selected ? ( -
- {/* Editor */} -
- { - setItems((prev) => - prev.map((n) => (n.id === noteId ? { ...n, ...fields } : n)) - ) - }} - onDelete={(noteId) => { - setItems((prev) => prev.filter((n) => n.id !== noteId)) - setSelectedId((prev) => (prev === noteId ? null : prev)) - }} - onArchive={(noteId) => { - setItems((prev) => prev.filter((n) => n.id !== noteId)) - setSelectedId((prev) => (prev === noteId ? null : prev)) - refreshNotes(selected?.notebookId) - }} - /> - {/* Toggle sidebar button — top-right of editor, always visible */} - -
- - {/* Meta sidebar — collapsible */} - {sidebarOpen && ( - { - try { - await updateNote(noteId, { reminder }) - setItems((prev) => - prev.map((n) => (n.id === noteId ? { ...n, reminder } : n)) - ) - if (reminder) { - toast.success(t('notes.reminderSet', { datetime: reminder.toLocaleString() })) - } else { - toast.info(t('reminder.removeReminder')) - } - refreshNotes(items.find(n => n.id === noteId)?.notebookId) - } catch { - toast.error(t('general.error')) - } - }} - /> - )} -
- ) : ( -
-
-
- -
-

- {items.length === 0 ? t('notes.emptyNotebook') : t('notes.noNoteSelected')} -

-

- {items.length === 0 - ? t('notes.emptyNotebookDesc') - : t('notes.selectOrCreateNote')} -

-
-
- )} - - {/* Delete Confirmation Dialog */} - setNoteToDelete(null)}> - - - {t('notes.confirmDeleteTitle') || t('notes.delete')} - - {t('notes.confirmDelete') || 'Are you sure you want to delete this note?'} - {noteToDelete && ( - - "{getNoteDisplayTitle(noteToDelete, t('notes.untitled'))}" - - )} - - - - - - - - -
- ) -} diff --git a/memento-note/context/editor-ui-context.tsx b/memento-note/context/editor-ui-context.tsx index d41949d..d8bf6e6 100644 --- a/memento-note/context/editor-ui-context.tsx +++ b/memento-note/context/editor-ui-context.tsx @@ -3,7 +3,6 @@ import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react' export type HomeUiControls = { - isTabsMode: boolean openNoteComposer: () => void } diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index 7620dff..d4b82a5 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -162,13 +162,6 @@ "unpinned": "Unpinned", "redoShortcut": "Redo (Ctrl+Y)", "undoShortcut": "Undo (Ctrl+Z)", - "viewCards": "Cards View", - "viewCardsTooltip": "Card grid with drag-and-drop reorder", - "viewList": "List", - "viewListTooltip": "Scannable list with preview, dates, and labels", - "viewTabs": "Tabs", - "viewTabsTooltip": "Tabs on top, note below — drag tabs to reorder", - "viewModeGroup": "Notes display mode", "reorderTabs": "Reorder tab", "modified": "Modified", "created": "Created", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index abdab15..e729ea8 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -162,13 +162,6 @@ "unpinned": "Désépinglées", "redoShortcut": "Rétablir (Ctrl+Y)", "undoShortcut": "Annuler (Ctrl+Z)", - "viewCards": "Vue par cartes", - "viewCardsTooltip": "Grille de cartes et réorganisation par glisser-déposer", - "viewList": "Liste", - "viewListTooltip": "Liste avec aperçu, dates et étiquettes (style magazine)", - "viewTabs": "Onglets", - "viewTabsTooltip": "Onglets en haut, contenu dessous — glisser les onglets pour réordonner", - "viewModeGroup": "Mode d'affichage des notes", "reorderTabs": "Réordonner l'onglet", "modified": "Modifiée", "created": "Créée", diff --git a/memento-note/prisma/schema.prisma b/memento-note/prisma/schema.prisma index a524649..b108898 100644 --- a/memento-note/prisma/schema.prisma +++ b/memento-note/prisma/schema.prisma @@ -279,8 +279,6 @@ model UserAISettings { fontSize String @default("medium") demoMode Boolean @default(false) showRecentNotes Boolean @default(true) - /// "masonry" = grille cartes Muuri ; "tabs" = onglets + panneau (type OneNote). Ancienne valeur "list" migrée vers "tabs" en lecture. - notesViewMode String @default("masonry") emailNotifications Boolean @default(false) desktopNotifications Boolean @default(false) anonymousAnalytics Boolean @default(false) diff --git a/memento-note/scripts/migrate-sqlite-to-postgres.ts b/memento-note/scripts/migrate-sqlite-to-postgres.ts index 6fc05be..52afa8a 100644 --- a/memento-note/scripts/migrate-sqlite-to-postgres.ts +++ b/memento-note/scripts/migrate-sqlite-to-postgres.ts @@ -312,7 +312,6 @@ async function main() { fontSize: s.fontSize || 'medium', demoMode: s.demoMode === 1 || s.demoMode === true, showRecentNotes: s.showRecentNotes === 1 || s.showRecentNotes === true, - notesViewMode: s.notesViewMode || 'masonry', emailNotifications: s.emailNotifications === 1 || s.emailNotifications === true, desktopNotifications: s.desktopNotifications === 1 || s.desktopNotifications === true, anonymousAnalytics: s.anonymousAnalytics === 1 || s.anonymousAnalytics === true,