From e7ce98320df271d8f22a8f3dcafb9fa63e0d2554 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Wed, 25 Mar 2026 16:35:43 +0100 Subject: [PATCH] feat(gif-mood): hide/reveal GIFs from other participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hidden field to GifMoodUserRating (schema + migration) - Add setGifMoodUserHidden service + action with SSE broadcast - Current user sees Cacher/Révéler toggle in their section header - Other users see a locked placeholder with item count when hidden Co-Authored-By: Claude Sonnet 4.6 --- .../migration.sql | 21 ++++++ prisma/schema.prisma | 3 +- src/actions/gif-mood.ts | 39 +++++++++++ src/components/gif-mood/GifMoodBoard.tsx | 66 +++++++++++++++++-- src/services/gif-mood.ts | 14 +++- 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20260325153345_gif_mood_hidden/migration.sql diff --git a/prisma/migrations/20260325153345_gif_mood_hidden/migration.sql b/prisma/migrations/20260325153345_gif_mood_hidden/migration.sql new file mode 100644 index 0000000..1c266f5 --- /dev/null +++ b/prisma/migrations/20260325153345_gif_mood_hidden/migration.sql @@ -0,0 +1,21 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_GifMoodUserRating" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "rating" INTEGER, + "hidden" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "GifMoodUserRating_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "GifMoodSession" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "GifMoodUserRating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_GifMoodUserRating" ("createdAt", "id", "rating", "sessionId", "updatedAt", "userId") SELECT "createdAt", "id", "rating", "sessionId", "updatedAt", "userId" FROM "GifMoodUserRating"; +DROP TABLE "GifMoodUserRating"; +ALTER TABLE "new_GifMoodUserRating" RENAME TO "GifMoodUserRating"; +CREATE INDEX "GifMoodUserRating_sessionId_idx" ON "GifMoodUserRating"("sessionId"); +CREATE UNIQUE INDEX "GifMoodUserRating_sessionId_userId_key" ON "GifMoodUserRating"("sessionId", "userId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c8c1c1d..e7910d2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -561,7 +561,8 @@ model GifMoodUserRating { session GifMoodSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) - rating Int // 1-5 + rating Int? // 1-5 + hidden Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/src/actions/gif-mood.ts b/src/actions/gif-mood.ts index 4028f6d..1db09b4 100644 --- a/src/actions/gif-mood.ts +++ b/src/actions/gif-mood.ts @@ -310,6 +310,45 @@ export async function setGifMoodUserRating(sessionId: string, rating: number) { } } +export async function setGifMoodUserHidden(sessionId: string, hidden: boolean) { + const authSession = await auth(); + if (!authSession?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + const canEdit = await gifMoodService.canEditGifMoodSession(sessionId, authSession.user.id); + if (!canEdit) { + return { success: false, error: 'Permission refusée' }; + } + + try { + await gifMoodService.setGifMoodUserHidden(sessionId, authSession.user.id, hidden); + + const user = await getUserById(authSession.user.id); + if (user) { + const event = await gifMoodService.createGifMoodSessionEvent( + sessionId, + authSession.user.id, + 'SESSION_UPDATED', + { hidden, userId: authSession.user.id } + ); + broadcastToGifMoodSession(sessionId, { + type: 'SESSION_UPDATED', + payload: { hidden, userId: authSession.user.id }, + userId: authSession.user.id, + user: { id: user.id, name: user.name, email: user.email }, + timestamp: event.createdAt, + }); + } + + revalidatePath(`/gif-mood/${sessionId}`); + return { success: true }; + } catch (error) { + console.error('Error setting gif mood user hidden:', error); + return { success: false, error: 'Erreur lors de la mise à jour' }; + } +} + // ============================================ // Sharing Actions // ============================================ diff --git a/src/components/gif-mood/GifMoodBoard.tsx b/src/components/gif-mood/GifMoodBoard.tsx index 471e069..9da13be 100644 --- a/src/components/gif-mood/GifMoodBoard.tsx +++ b/src/components/gif-mood/GifMoodBoard.tsx @@ -16,7 +16,7 @@ import { arrayMove, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { setGifMoodUserRating, reorderGifMoodItems } from '@/actions/gif-mood'; +import { setGifMoodUserRating, reorderGifMoodItems, setGifMoodUserHidden } from '@/actions/gif-mood'; import { Avatar } from '@/components/ui/Avatar'; import { GifMoodCard } from './GifMoodCard'; import { GifMoodAddForm } from './GifMoodAddForm'; @@ -55,7 +55,7 @@ interface GifMoodBoardProps { name: string | null; email: string; }; - ratings: { userId: string; rating: number }[]; + ratings: { userId: string; rating: number | null; hidden: boolean }[]; canEdit: boolean; } @@ -218,6 +218,7 @@ export function GifMoodBoard({ }: GifMoodBoardProps) { const [cols, setCols] = useState(4); const [, startReorderTransition] = useTransition(); + const [, startHiddenTransition] = useTransition(); // Optimistic reorder state for the current user's items const [optimisticItems, setOptimisticItems] = useState([]); @@ -304,7 +305,10 @@ export function GifMoodBoard({ isCurrentUser && optimisticItems.length > 0 ? optimisticItems : serverItems; const canAdd = canEdit && isCurrentUser && userItems.length < GIF_MOOD_MAX_ITEMS; const accentColor = SECTION_COLORS[index % SECTION_COLORS.length]; - const userRating = ratings.find((r) => r.userId === user.id)?.rating ?? null; + const userRatingEntry = ratings.find((r) => r.userId === user.id); + const userRating = userRatingEntry?.rating ?? null; + const isHidden = userRatingEntry?.hidden ?? false; + const showHidden = !isCurrentUser && isHidden; return (
@@ -338,10 +342,60 @@ export function GifMoodBoard({ /> + + {/* Hide/reveal toggle — current user only */} + {isCurrentUser && canEdit && userItems.length > 0 && ( + + )} + {/* Hidden placeholder for other users */} + {showHidden ? ( +
+ + + + +

+ {userItems.length > 0 + ? `${userItems.length} GIF${userItems.length !== 1 ? 's' : ''} caché${userItems.length !== 1 ? 's' : ''}` + : 'GIFs cachés'} +

+
+ ) : null} + {/* Grid */} - {isCurrentUser && canEdit ? ( + {!showHidden && isCurrentUser && canEdit ? ( - ) : ( + ) : !showHidden ? (
{userItems.map((item) => ( )}
- )} + ) : null}
); })} diff --git a/src/services/gif-mood.ts b/src/services/gif-mood.ts index 85a2e9b..ba91ccb 100644 --- a/src/services/gif-mood.ts +++ b/src/services/gif-mood.ts @@ -69,7 +69,7 @@ const gifMoodByIdInclude = { orderBy: { order: 'asc' as const }, }, shares: { include: { user: { select: { id: true, name: true, email: true } } } }, - ratings: { select: { userId: true, rating: true } }, + ratings: { select: { userId: true, rating: true, hidden: true } }, }; export async function getGifMoodSessionById(sessionId: string, userId: string) { @@ -213,6 +213,18 @@ export async function upsertGifMoodUserRating( }); } +export async function setGifMoodUserHidden( + sessionId: string, + userId: string, + hidden: boolean +) { + return prisma.gifMoodUserRating.upsert({ + where: { sessionId_userId: { sessionId, userId } }, + update: { hidden }, + create: { sessionId, userId, hidden }, + }); +} + // ============================================ // Session Sharing // ============================================