fix: update masonry grid sizing logic and notebook list padding
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user