feat: consolidate to single Architectural Grid view and remove all notesViewMode logic
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m6s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m6s
This commit is contained in:
@@ -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 (
|
||||
<HomeClient
|
||||
initialNotes={allNotes}
|
||||
initialSettings={{
|
||||
showRecentNotes: settings?.showRecentNotes !== false,
|
||||
notesViewMode,
|
||||
noteHistory: settings?.noteHistory === true,
|
||||
noteHistoryMode: (settings?.noteHistoryMode ?? 'manual') as 'manual' | 'auto',
|
||||
aiAssistantEnabled: settings?.paragraphRefactor !== false,
|
||||
|
||||
@@ -14,7 +14,6 @@ export type UserAISettingsData = {
|
||||
preferredLanguage?: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
|
||||
demoMode?: boolean
|
||||
showRecentNotes?: boolean
|
||||
notesViewMode?: 'masonry' | 'tabs'
|
||||
emailNotifications?: boolean
|
||||
desktopNotifications?: boolean
|
||||
anonymousAnalytics?: boolean
|
||||
@@ -39,7 +38,6 @@ const USER_AI_SETTINGS_PRISMA_KEYS = [
|
||||
'fontSize',
|
||||
'demoMode',
|
||||
'showRecentNotes',
|
||||
'notesViewMode',
|
||||
'emailNotifications',
|
||||
'desktopNotifications',
|
||||
'anonymousAnalytics',
|
||||
@@ -61,13 +59,6 @@ function pickUserAISettingsForDb(input: UserAISettingsData): Partial<Record<User
|
||||
out[key] = v
|
||||
}
|
||||
}
|
||||
if (
|
||||
out.notesViewMode != null &&
|
||||
out.notesViewMode !== 'masonry' &&
|
||||
out.notesViewMode !== 'tabs'
|
||||
) {
|
||||
delete out.notesViewMode
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -148,7 +139,6 @@ const getCachedAISettings = unstable_cache(
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
@@ -163,13 +153,6 @@ const getCachedAISettings = unstable_cache(
|
||||
}
|
||||
}
|
||||
|
||||
const raw = settings.notesViewMode
|
||||
const viewMode =
|
||||
raw === 'masonry'
|
||||
? ('masonry' as const)
|
||||
: raw === 'tabs'
|
||||
? ('tabs' as const)
|
||||
: ('masonry' as const)
|
||||
|
||||
return {
|
||||
titleSuggestions: settings.titleSuggestions,
|
||||
@@ -181,7 +164,6 @@ const getCachedAISettings = unstable_cache(
|
||||
preferredLanguage: (settings.preferredLanguage || 'auto') as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
|
||||
demoMode: settings.demoMode,
|
||||
showRecentNotes: settings.showRecentNotes,
|
||||
notesViewMode: viewMode,
|
||||
emailNotifications: settings.emailNotifications,
|
||||
desktopNotifications: settings.desktopNotifications,
|
||||
anonymousAnalytics: settings.anonymousAnalytics,
|
||||
@@ -207,7 +189,6 @@ const getCachedAISettings = unstable_cache(
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
@@ -246,7 +227,6 @@ export async function getAISettings(userId?: string) {
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Note } from '@/lib/types'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById, createNote } from '@/app/actions/notes'
|
||||
import { NotesMainSection, type NotesViewMode } from '@/components/notes-main-section'
|
||||
import { NotesEditorialView } from '@/components/notes-editorial-view'
|
||||
|
||||
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
|
||||
@@ -46,7 +45,6 @@ const NotebookSummaryDialog = dynamic(
|
||||
|
||||
type InitialSettings = {
|
||||
showRecentNotes: boolean
|
||||
notesViewMode: 'masonry' | 'tabs'
|
||||
noteHistory: boolean
|
||||
noteHistoryMode: 'manual' | 'auto'
|
||||
aiAssistantEnabled: boolean
|
||||
@@ -66,7 +64,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
const [pinnedNotes, setPinnedNotes] = useState<Note[]>(
|
||||
initialNotes.filter(n => n.isPinned)
|
||||
)
|
||||
const [notesViewMode, setNotesViewMode] = useState<NotesViewMode>(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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full min-h-0 flex-1 flex-col',
|
||||
isTabs ? 'gap-3 py-1' : 'h-full'
|
||||
'flex w-full min-h-0 flex-1 flex-col gap-3 py-1'
|
||||
)}
|
||||
>
|
||||
{editingNote ? (
|
||||
@@ -413,10 +405,9 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto min-h-0 bg-memento-paper flex flex-col">
|
||||
<div className={cn(
|
||||
'px-12 pt-12 pb-8 flex flex-col gap-6',
|
||||
isEditorialMode ? 'sticky top-0 bg-memento-paper/90 backdrop-blur-md z-30' : ''
|
||||
)}>
|
||||
<div
|
||||
className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-memento-paper/90 backdrop-blur-md z-30"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
{currentNotebook && notebookPath.length > 0 && (
|
||||
@@ -571,18 +562,6 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
<div className="px-12 flex-1 pb-20">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">{t('general.loading')}</div>
|
||||
) : isTabs ? (
|
||||
<NotesMainSection
|
||||
viewMode={notesViewMode}
|
||||
notes={sortedNotes}
|
||||
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
|
||||
onSizeChange={handleSizeChange}
|
||||
currentNotebookId={searchParams.get('notebook')}
|
||||
noteHistoryMode={noteHistoryMode}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
onEnableHistory={handleEnableHistory}
|
||||
onNoteCreated={handleNoteCreated}
|
||||
/>
|
||||
) : (
|
||||
<div className="max-w-3xl space-y-16">
|
||||
{sortedPinnedNotes.length > 0 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="masonry-sortable-item"
|
||||
data-id={note.id}
|
||||
data-size={note.size}
|
||||
>
|
||||
<NoteCard
|
||||
note={note}
|
||||
onEdit={onEdit}
|
||||
onDragStart={onDragStartNote}
|
||||
onDragEnd={onDragEndNote}
|
||||
isDragging={isDragging}
|
||||
isTrashView={isTrashView}
|
||||
onSizeChange={(newSize) => onSizeChange(note.id, newSize)}
|
||||
noteHistoryEnabled={noteHistoryEnabled}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 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 (
|
||||
<SortableContext items={ids} strategy={rectSortingStrategy}>
|
||||
<div className="masonry-css-grid">
|
||||
{notes.map(note => (
|
||||
<SortableNoteItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={onEdit}
|
||||
onSizeChange={onSizeChange}
|
||||
onDragStartNote={onDragStartNote}
|
||||
onDragEndNote={onDragEndNote}
|
||||
isDragging={draggedNoteId === note.id}
|
||||
isTrashView={isTrashView}
|
||||
noteHistoryEnabled={noteHistoryEnabled}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 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<Note[]>(notes);
|
||||
const prevNotesRef = useRef<Note[]>(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<string | null>(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<Note[]>(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 (
|
||||
<DndContext
|
||||
id="masonry-dnd"
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="masonry-container" data-card-size-mode={cardSizeMode}>
|
||||
{pinnedNotes.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">
|
||||
{t('notes.pinned')}
|
||||
</h2>
|
||||
<SortableGridSection
|
||||
notes={pinnedNotes}
|
||||
onEdit={handleEdit}
|
||||
onSizeChange={handleSizeChange}
|
||||
draggedNoteId={draggedNoteId}
|
||||
onDragStartNote={startDrag}
|
||||
onDragEndNote={endDrag}
|
||||
isTrashView={isTrashView}
|
||||
noteHistoryEnabled={noteHistoryEnabled}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{othersNotes.length > 0 && (
|
||||
<div>
|
||||
{pinnedNotes.length > 0 && (
|
||||
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">
|
||||
{t('notes.others')}
|
||||
</h2>
|
||||
)}
|
||||
<SortableGridSection
|
||||
notes={othersNotes}
|
||||
onEdit={handleEdit}
|
||||
onSizeChange={handleSizeChange}
|
||||
draggedNoteId={draggedNoteId}
|
||||
onDragStartNote={startDrag}
|
||||
onDragEndNote={endDrag}
|
||||
isTrashView={isTrashView}
|
||||
noteHistoryEnabled={noteHistoryEnabled}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DragOverlay — montre une copie flottante pendant le drag */}
|
||||
<DragOverlay>
|
||||
{activeNote ? (
|
||||
<div className="masonry-sortable-item masonry-drag-overlay" data-size={activeNote.size}>
|
||||
<NoteCard
|
||||
note={activeNote}
|
||||
onEdit={handleEdit}
|
||||
isDragging={true}
|
||||
onSizeChange={(newSize) => handleSizeChange(activeNote.id, newSize)}
|
||||
noteHistoryEnabled={noteHistoryEnabled}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
|
||||
{editingNote && (
|
||||
<NoteEditor
|
||||
note={editingNote.note}
|
||||
readOnly={editingNote.readOnly}
|
||||
onClose={() => setEditingNote(null)}
|
||||
/>
|
||||
)}
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
@@ -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: () => (
|
||||
<div
|
||||
className="min-h-[200px] rounded-xl border border-dashed border-muted-foreground/20 bg-muted/30 animate-pulse"
|
||||
aria-hidden
|
||||
/>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
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<void>
|
||||
onNoteCreated?: (note: Note) => void
|
||||
}
|
||||
|
||||
export function NotesMainSection({
|
||||
notes,
|
||||
viewMode,
|
||||
onEdit,
|
||||
onSizeChange,
|
||||
currentNotebookId,
|
||||
noteHistoryMode = 'manual',
|
||||
onOpenHistory,
|
||||
onEnableHistory,
|
||||
onNoteCreated,
|
||||
}: NotesMainSectionProps) {
|
||||
if (viewMode === 'tabs') {
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col" data-testid="notes-grid-tabs-wrap">
|
||||
<NotesTabsView
|
||||
notes={notes}
|
||||
onEdit={onEdit}
|
||||
currentNotebookId={currentNotebookId}
|
||||
noteHistoryMode={noteHistoryMode}
|
||||
onOpenHistory={onOpenHistory}
|
||||
onEnableHistory={onEnableHistory}
|
||||
onNoteCreated={onNoteCreated}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="notes-grid">
|
||||
<MasonryGridLazy
|
||||
notes={notes}
|
||||
onEdit={onEdit}
|
||||
onSizeChange={onSizeChange}
|
||||
noteHistoryMode={noteHistoryMode}
|
||||
onOpenHistory={onOpenHistory}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
import { createContext, useContext, useState, useCallback, useMemo, type ReactNode } from 'react'
|
||||
|
||||
export type HomeUiControls = {
|
||||
isTabsMode: boolean
|
||||
openNoteComposer: () => void
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user