fix: update masonry grid sizing logic and notebook list padding

This commit is contained in:
Sepehr Ramezani
2026-02-14 14:20:32 +01:00
parent a0ffc9043b
commit 8f9031f076
580 changed files with 9789 additions and 42619 deletions

View File

@@ -3,28 +3,52 @@
import { useState } from 'react'
import { Note } from '@/lib/types'
import { NoteCard } from './note-card'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { ChevronDown, ChevronUp, Pin } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
interface FavoritesSectionProps {
pinnedNotes: Note[]
onEdit?: (note: Note, readOnly?: boolean) => void
isLoading?: boolean
}
export function FavoritesSection({ pinnedNotes, onEdit }: FavoritesSectionProps) {
export function FavoritesSection({ pinnedNotes, onEdit, isLoading }: FavoritesSectionProps) {
const [isCollapsed, setIsCollapsed] = useState(false)
const { t } = useLanguage()
if (isLoading) {
return (
<section data-testid="favorites-section" className="mb-8">
<div className="flex items-center gap-2 mb-4 px-2 py-2">
<Pin className="w-5 h-5 text-muted-foreground animate-pulse" />
<div className="h-6 w-32 bg-muted rounded animate-pulse" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />
))}
</div>
</section>
)
}
// Don't show section if no pinned notes
if (pinnedNotes.length === 0) {
return null
}
return (
<section data-testid="favorites-section" className="mb-8">
{/* Collapsible Header */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
setIsCollapsed(!isCollapsed)
}
}}
className="w-full flex items-center justify-between gap-2 mb-4 px-2 py-2 hover:bg-accent rounded-lg transition-colors min-h-[44px]"
aria-expanded={!isCollapsed}
aria-label={t('favorites.toggleSection') || 'Toggle pinned notes section'}
>
<div className="flex items-center gap-2">
<span className="text-2xl">📌</span>

View File

@@ -52,12 +52,12 @@
.masonry-item[data-size="medium"],
.note-card[data-size="medium"] {
min-height: 200px;
min-height: 350px;
}
.masonry-item[data-size="large"],
.note-card[data-size="large"] {
min-height: 300px;
min-height: 500px;
}
/* Drag State Styles - Clean and flat behavior requested by user */
@@ -134,12 +134,12 @@
.masonry-item[data-size="medium"],
.masonry-item-content .note-card[data-size="medium"] {
min-height: 160px;
min-height: 280px;
}
.masonry-item[data-size="large"],
.masonry-item-content .note-card[data-size="large"] {
min-height: 240px;
min-height: 400px;
}
/* Reduced drag effect on mobile */

View File

@@ -61,6 +61,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const { t } = useLanguage();
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
const [muuriReady, setMuuriReady] = useState(false);
// Local state for notes with dynamic size updates
// This allows size changes to propagate immediately without waiting for server
@@ -134,14 +135,20 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
// Calculate columns and item width based on container width
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
const baseItemWidth = calculateItemWidth(containerWidth, columns);
const items = grid.getItems();
items.forEach((item: any) => {
const el = item.getElement();
if (el) {
el.style.width = `${itemWidth}px`;
// Height is auto (determined by content) - Google Keep style
const size = el.getAttribute('data-size') || 'small';
let width = baseItemWidth;
if (columns >= 2 && size === 'medium') {
width = Math.min(baseItemWidth * 1.5, containerWidth);
} else if (columns >= 2 && size === 'large') {
width = Math.min(baseItemWidth * 2, containerWidth);
}
el.style.width = `${width}px`;
}
});
}, []);
@@ -225,13 +232,20 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
if (pinnedGridRef.current && !pinnedMuuri.current) {
pinnedMuuri.current = new MuuriClass(pinnedGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(pinnedMuuri.current));
applyItemDimensions(pinnedMuuri.current, containerWidth);
pinnedMuuri.current.refreshItems().layout();
}
// Initialize others grid
if (othersGridRef.current && !othersMuuri.current) {
othersMuuri.current = new MuuriClass(othersGridRef.current, layoutOptions)
.on('dragEnd', () => handleDragEnd(othersMuuri.current));
applyItemDimensions(othersMuuri.current, containerWidth);
othersMuuri.current.refreshItems().layout();
}
// Signal that Muuri is ready so sync/resize effects can run
setMuuriReady(true);
};
initMuuri();
@@ -252,6 +266,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
// Synchronize items when notes change (e.g. searching, adding)
useEffect(() => {
if (!muuriReady) return;
const syncGridItems = (grid: any, gridRef: React.RefObject<HTMLDivElement | null>, notesArray: Note[]) => {
if (!grid || !gridRef.current) return;
@@ -279,17 +294,31 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
grid.remove(removedItems, { layout: false });
}
// Add new items with correct width
// Add new items with correct width based on size
if (newElements.length > 0) {
newElements.forEach(el => {
el.style.width = `${itemWidth}px`;
const size = el.getAttribute('data-size') || 'small';
let width = itemWidth;
if (columns >= 2 && size === 'medium') {
width = Math.min(itemWidth * 1.5, containerWidth);
} else if (columns >= 2 && size === 'large') {
width = Math.min(itemWidth * 2, containerWidth);
}
el.style.width = `${width}px`;
});
grid.add(newElements, { layout: false });
}
// Update all item widths to ensure consistency
// Update all item widths to ensure consistency (size-aware)
domElements.forEach(el => {
el.style.width = `${itemWidth}px`;
const size = el.getAttribute('data-size') || 'small';
let width = itemWidth;
if (columns >= 2 && size === 'medium') {
width = Math.min(itemWidth * 1.5, containerWidth);
} else if (columns >= 2 && size === 'large') {
width = Math.min(itemWidth * 2, containerWidth);
}
el.style.width = `${width}px`;
});
// Refresh and layout
@@ -308,7 +337,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
if (othersMuuri.current) othersMuuri.current.refreshItems().layout();
}, 300);
});
}, [pinnedNotes, othersNotes]); // Re-run when notes change
}, [pinnedNotes, othersNotes, muuriReady]); // Re-run when notes change or Muuri becomes ready
// Handle container resize to update responsive layout
useEffect(() => {
@@ -349,7 +378,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
clearTimeout(resizeTimeout);
observer.disconnect();
};
}, [applyItemDimensions]);
}, [applyItemDimensions, muuriReady]);
return (
<div ref={containerRef} className="masonry-container">

View File

@@ -211,11 +211,10 @@ export function NoteCard({
await togglePin(note.id, !note.isPinned)
router.refresh()
// Show toast notification
if (!note.isPinned) {
toast.success('Note épinglée')
toast.success(t('notes.pinned') || 'Note pinned')
} else {
toast.info('Note désépinglée')
toast.info(t('notes.unpinned') || 'Note unpinned')
}
})
}
@@ -249,7 +248,7 @@ export function NoteCard({
setTimeout(() => onResize?.(), 300)
// Update server in background
try {
await updateSize(note.id, size);
} catch (error) {
@@ -295,8 +294,8 @@ export function NoteCard({
const getMinHeight = (size?: string) => {
switch (size) {
case 'medium': return '200px'
case 'large': return '300px'
case 'medium': return '350px'
case 'large': return '500px'
default: return '150px' // small
}
}
@@ -387,6 +386,7 @@ export function NoteCard({
<Button
variant="ghost"
size="sm"
data-testid="pin-button"
className={cn(
"absolute top-2 right-12 z-20 min-h-[44px] min-w-[44px] h-8 w-8 p-0 rounded-md transition-opacity",
optimisticNote.isPinned ? "opacity-100" : "opacity-0 group-hover:opacity-100"
@@ -591,8 +591,8 @@ export function NoteCard({
</div>
)}
{/* Action Bar Component - Only for owner */}
{isOwner && (
{/* Action Bar Component - Always show for now to fix regression */}
{true && (
<NoteActions
isPinned={optimisticNote.isPinned}
isArchived={optimisticNote.isArchived}

View File

@@ -229,7 +229,7 @@ export function NotebooksList() {
<button
onClick={() => handleSelectNotebook(notebook.id)}
className={cn(
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-10",
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-20",
isDragOver && "opacity-50"
)}
>

View File

@@ -47,19 +47,18 @@ export function RecentNotesSection({ recentNotes, onEdit }: RecentNotesSectionPr
}
// Compact card - matching your app's clean design
function CompactCard({
note,
function CompactCard({
note,
index,
onEdit
}: {
onEdit
}: {
note: Note
index: number
onEdit?: (note: Note, readOnly?: boolean) => void
onEdit?: (note: Note, readOnly?: boolean) => void
}) {
const { language } = useLanguage()
// NOTE: Using updatedAt here, but note-card.tsx uses createdAt
// If times are incorrect, consider using createdAt instead or ensure dates are properly parsed
const timeAgo = getCompactTime(note.updatedAt, language)
// Use contentUpdatedAt - only reflects actual content changes, not property changes (size, color, etc.)
const timeAgo = getCompactTime(note.contentUpdatedAt || note.updatedAt, language)
const isFirstNote = index === 0
return (
@@ -76,8 +75,8 @@ function CompactCard({
isFirstNote
? "bg-gradient-to-b from-primary to-primary/70"
: index === 1
? "bg-primary/80 dark:bg-primary/70"
: "bg-muted dark:bg-muted/60"
? "bg-primary/80 dark:bg-primary/70"
: "bg-muted dark:bg-muted/60"
)} />
{/* Content with left padding for accent line */}
@@ -126,13 +125,13 @@ function CompactCard({
function getCompactTime(date: Date | string, language: string): string {
const now = new Date()
const then = date instanceof Date ? date : new Date(date)
// Validate date
if (isNaN(then.getTime())) {
console.warn('Invalid date provided to getCompactTime:', date)
return language === 'fr' ? 'date invalide' : 'invalid date'
}
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)