feat(gif-mood): hide/reveal GIFs from other participants
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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<GifMoodItem[]>([]);
|
||||
@@ -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 (
|
||||
<section key={user.id}>
|
||||
@@ -338,10 +342,60 @@ export function GifMoodBoard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hide/reveal toggle — current user only */}
|
||||
{isCurrentUser && canEdit && userItems.length > 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
startHiddenTransition(async () => {
|
||||
await setGifMoodUserHidden(sessionId, !isHidden);
|
||||
});
|
||||
}}
|
||||
className={`flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full border transition-all ${
|
||||
isHidden
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border text-muted hover:text-foreground hover:border-foreground/30'
|
||||
}`}
|
||||
title={isHidden ? 'Révéler mes GIFs' : 'Cacher mes GIFs'}
|
||||
>
|
||||
{isHidden ? (
|
||||
<>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
Révéler
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>
|
||||
Cacher
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hidden placeholder for other users */}
|
||||
{showHidden ? (
|
||||
<div className="flex items-center justify-center rounded-2xl border border-dashed border-border/60 py-10 gap-3">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted/50" aria-hidden>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<p className="text-sm text-muted/60">
|
||||
{userItems.length > 0
|
||||
? `${userItems.length} GIF${userItems.length !== 1 ? 's' : ''} caché${userItems.length !== 1 ? 's' : ''}`
|
||||
: 'GIFs cachés'}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Grid */}
|
||||
{isCurrentUser && canEdit ? (
|
||||
{!showHidden && isCurrentUser && canEdit ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@@ -372,7 +426,7 @@ export function GifMoodBoard({
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
) : !showHidden ? (
|
||||
<div className={`grid ${GRID_COLS[cols]} gap-4 items-start`}>
|
||||
{userItems.map((item) => (
|
||||
<GifMoodCard
|
||||
@@ -389,7 +443,7 @@ export function GifMoodBoard({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user