From ce2eef1b65cc3fdf87dfea4d31e5e146a91be241 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Wed, 25 Mar 2026 17:14:26 +0100 Subject: [PATCH] feat(gif-mood): masonry wall mode with DnD + flex columns - Wall mode: flex columns (no empty bottoms), all GIFs mixed - DnD in wall: SortableContext on current user items only, others are fixed - Own GIFs show drag handle, others show avatar badge - Image min-h skeleton prevents 0-height layout slots while loading Co-Authored-By: Claude Sonnet 4.6 --- src/components/gif-mood/GifMoodBoard.tsx | 257 ++++++++++++++++++----- src/components/gif-mood/GifMoodCard.tsx | 4 +- 2 files changed, 208 insertions(+), 53 deletions(-) diff --git a/src/components/gif-mood/GifMoodBoard.tsx b/src/components/gif-mood/GifMoodBoard.tsx index ce88ad2..7238525 100644 --- a/src/components/gif-mood/GifMoodBoard.tsx +++ b/src/components/gif-mood/GifMoodBoard.tsx @@ -149,6 +149,38 @@ function DragHandle(props: React.HTMLAttributes) { ); } +function SortableWallItem({ + sessionId, + item, + currentUserId, + canEdit, +}: { + sessionId: string; + item: GifMoodItem; + currentUserId: string; + canEdit: boolean; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: item.id, + }); + + return ( +
+ + +
+ ); +} + function SortableGifMoodCard({ sessionId, item, @@ -190,6 +222,7 @@ const GRID_COLS: Record = { 6: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6', }; + function GridIcon({ cols }: { cols: number }) { return ( @@ -207,6 +240,21 @@ function GridIcon({ cols }: { cols: number }) { ); } +function MasonryIcon() { + return ( + + + + + + + + + + + ); +} + export function GifMoodBoard({ sessionId, currentUserId, @@ -217,6 +265,7 @@ export function GifMoodBoard({ canEdit, }: GifMoodBoardProps) { const [cols, setCols] = useState(4); + const [masonry, setMasonry] = useState(false); const [, startReorderTransition] = useTransition(); const [, startHiddenTransition] = useTransition(); @@ -259,6 +308,24 @@ export function GifMoodBoard({ }); }, [allUsers, currentUserId, owner.id]); + function handleWallDragEnd(event: DragEndEvent) { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const currentItems = + optimisticItems.length > 0 ? optimisticItems : (itemsByUser.get(currentUserId) ?? []); + const oldIndex = currentItems.findIndex((i) => i.id === active.id); + const newIndex = currentItems.findIndex((i) => i.id === over.id); + if (oldIndex === -1 || newIndex === -1) return; + + const reordered = arrayMove(currentItems, oldIndex, newIndex); + setOptimisticItems(reordered); + + startReorderTransition(async () => { + await reorderGifMoodItems(sessionId, reordered.map((i) => i.id)); + }); + } + function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; @@ -277,9 +344,31 @@ export function GifMoodBoard({ }); } + const currentUserItems = optimisticItems.length > 0 + ? optimisticItems + : (itemsByUser.get(currentUserId) ?? []); + + const wallItems = useMemo(() => { + return sortedUsers.flatMap((user) => { + const isCurrentUser = user.id === currentUserId; + const serverItems = itemsByUser.get(user.id) ?? []; + const uItems = isCurrentUser && optimisticItems.length > 0 ? optimisticItems : serverItems; + const userRatingEntry = ratings.find((r) => r.userId === user.id); + if (!isCurrentUser && (uItems.length === 0 || userRatingEntry?.hidden)) return []; + return uItems; + }); + }, [sortedUsers, itemsByUser, currentUserId, optimisticItems, ratings]); + + // Distribute items into N columns round-robin so each column only takes its items' height + const wallColumns = useMemo(() => { + const columns: GifMoodItem[][] = Array.from({ length: cols }, () => []); + wallItems.forEach((item, i) => columns[i % cols].push(item)); + return columns; + }, [wallItems, cols]); + return (
- {/* Column size control */} + {/* Layout controls */}
{[4, 5, 6].map((n) => ( @@ -287,7 +376,7 @@ export function GifMoodBoard({ key={n} onClick={() => setCols(n)} className={`flex items-center justify-center rounded-lg px-2.5 py-1.5 transition-all ${ - cols === n + !masonry && cols === n ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted hover:text-foreground hover:bg-card-hover' }`} @@ -296,9 +385,70 @@ export function GifMoodBoard({ ))} +
+
- {sortedUsers.map((user, index) => { + + {/* Wall mode: flex columns so each column only takes its items' height */} + {masonry && ( + + i.id)} + strategy={rectSortingStrategy} + > +
+ {wallColumns.map((colItems, colIdx) => ( +
+ {colItems.map((item) => + item.userId === currentUserId && canEdit ? ( + + ) : ( +
+
+ +
+ +
+ ) + )} + {colIdx === 0 && canEdit && currentUserItems.length < GIF_MOOD_MAX_ITEMS && ( + + )} +
+ ))} +
+
+
+ )} + + {/* Grid mode: per-user sections */} + {!masonry && sortedUsers.map((user, index) => { const isCurrentUser = user.id === currentUserId; const serverItems = itemsByUser.get(user.id) ?? []; const userItems = @@ -397,59 +547,62 @@ export function GifMoodBoard({
) : null} - {/* Grid */} - {!showHidden && isCurrentUser && canEdit ? ( - - i.id)} - strategy={rectSortingStrategy} + {/* Items */} + {!showHidden && ( + isCurrentUser && canEdit ? ( + -
- {userItems.map((item) => ( - - ))} - {canAdd && ( - - )} - {!canAdd && userItems.length === 0 && ( -
-

Aucun GIF pour le moment

-
- )} -
-
-
- ) : !showHidden ? ( -
- {userItems.map((item) => ( - - ))} - {!canAdd && userItems.length === 0 && ( -
-

Aucun GIF pour le moment

-
- )} -
- ) : null} + i.id)} + strategy={rectSortingStrategy} + > +
+ {userItems.map((item) => ( + + ))} + {canAdd && ( + + )} + {!canAdd && userItems.length === 0 && ( +
+

Aucun GIF pour le moment

+
+ )} +
+
+ + ) : ( +
+ {userItems.map((item) => ( + + ))} + {!canAdd && userItems.length === 0 && ( +
+

Aucun GIF pour le moment

+
+ )} +
+ ) + )} ); })}
); } + diff --git a/src/components/gif-mood/GifMoodCard.tsx b/src/components/gif-mood/GifMoodCard.tsx index ca3ead3..523c7b0 100644 --- a/src/components/gif-mood/GifMoodCard.tsx +++ b/src/components/gif-mood/GifMoodCard.tsx @@ -26,6 +26,7 @@ export const GifMoodCard = memo(function GifMoodCard({ const [itemVersion, setItemVersion] = useState(item); const [isPending, startTransition] = useTransition(); const [imgError, setImgError] = useState(false); + const [imgLoaded, setImgLoaded] = useState(false); const [isFocused, setIsFocused] = useState(false); const textareaRef = useRef(null); @@ -78,7 +79,8 @@ export const GifMoodCard = memo(function GifMoodCard({ GIF setImgLoaded(true)} onError={() => setImgError(true)} /> )}