Files
workshop-manager/src/components/gif-mood/GifMoodCard.tsx
Froidefond Julien ce2eef1b65
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m55s
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 <noreply@anthropic.com>
2026-03-25 17:14:26 +01:00

128 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { memo, useRef, useEffect, useState, useTransition } from 'react';
import { updateGifMoodItem, deleteGifMoodItem } from '@/actions/gif-mood';
import { IconClose } from '@/components/ui';
interface GifMoodCardProps {
sessionId: string;
item: {
id: string;
gifUrl: string;
note: string | null;
userId: string;
};
currentUserId: string;
canEdit: boolean;
}
export const GifMoodCard = memo(function GifMoodCard({
sessionId,
item,
currentUserId,
canEdit,
}: GifMoodCardProps) {
const [note, setNote] = useState(item.note || '');
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<HTMLTextAreaElement>(null);
useEffect(() => {
const el = textareaRef.current;
if (!el) return;
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}, [note]);
// Sync from server only when not focused — prevents SSE refresh from wiping in-progress edits
if (itemVersion !== item) {
setItemVersion(item);
if (!isFocused) {
setNote(item.note || '');
}
}
const isOwner = item.userId === currentUserId;
const canEditThis = canEdit && isOwner;
function handleNoteBlur() {
if (!canEditThis) return;
startTransition(async () => {
await updateGifMoodItem(sessionId, item.id, { note: note.trim() || undefined });
});
}
function handleDelete() {
if (!canEditThis) return;
startTransition(async () => {
await deleteGifMoodItem(sessionId, item.id);
});
}
return (
<div
className={`group relative rounded-2xl overflow-hidden bg-card shadow-sm hover:shadow-md transition-all duration-200 ${
isPending ? 'opacity-50 scale-95' : ''
}`}
>
{/* GIF */}
{imgError ? (
<div className="flex flex-col items-center justify-center gap-2 min-h-[120px] bg-card-hover">
<span className="text-3xl opacity-40">🖼</span>
<p className="text-xs text-muted">Image non disponible</p>
</div>
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.gifUrl}
alt="GIF"
className={`w-full block ${imgLoaded ? '' : 'min-h-[80px]'}`}
onLoad={() => setImgLoaded(true)}
onError={() => setImgError(true)}
/>
)}
{/* Gradient overlay on hover (for delete affordance) */}
{canEditThis && (
<div className="absolute inset-0 bg-gradient-to-b from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
)}
{/* Delete button — visible on hover */}
{canEditThis && (
<button
onClick={handleDelete}
disabled={isPending}
className="absolute top-2 right-2 p-1.5 rounded-full bg-black/50 text-white opacity-0 group-hover:opacity-100 hover:bg-black/70 transition-all backdrop-blur-sm"
title="Supprimer ce GIF"
>
<IconClose className="w-3 h-3" />
</button>
)}
{/* Note */}
{canEditThis ? (
<div className="px-3 bg-card flex items-center justify-center min-h-[2.75rem]">
<textarea
ref={textareaRef}
value={note}
onChange={(e) => setNote(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => { setIsFocused(false); handleNoteBlur(); }}
placeholder="Ajouter une note…"
rows={1}
className="w-full text-foreground/70 bg-transparent resize-none outline-none placeholder:text-muted/40 leading-snug text-center overflow-hidden"
style={{ fontFamily: 'var(--font-caveat)', fontSize: '1rem' }}
/>
</div>
) : note ? (
<div className="px-3 bg-card flex items-center justify-center min-h-[2.75rem]">
<p className="text-foreground/70 leading-snug text-center" style={{ fontFamily: 'var(--font-caveat)', fontSize: '1rem' }}>{note}</p>
</div>
) : null}
</div>
);
});