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)
|
session GifMoodSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
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())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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
|
// Sharing Actions
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
arrayMove,
|
arrayMove,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
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 { Avatar } from '@/components/ui/Avatar';
|
||||||
import { GifMoodCard } from './GifMoodCard';
|
import { GifMoodCard } from './GifMoodCard';
|
||||||
import { GifMoodAddForm } from './GifMoodAddForm';
|
import { GifMoodAddForm } from './GifMoodAddForm';
|
||||||
@@ -55,7 +55,7 @@ interface GifMoodBoardProps {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
ratings: { userId: string; rating: number }[];
|
ratings: { userId: string; rating: number | null; hidden: boolean }[];
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +218,7 @@ export function GifMoodBoard({
|
|||||||
}: GifMoodBoardProps) {
|
}: GifMoodBoardProps) {
|
||||||
const [cols, setCols] = useState(4);
|
const [cols, setCols] = useState(4);
|
||||||
const [, startReorderTransition] = useTransition();
|
const [, startReorderTransition] = useTransition();
|
||||||
|
const [, startHiddenTransition] = useTransition();
|
||||||
|
|
||||||
// Optimistic reorder state for the current user's items
|
// Optimistic reorder state for the current user's items
|
||||||
const [optimisticItems, setOptimisticItems] = useState<GifMoodItem[]>([]);
|
const [optimisticItems, setOptimisticItems] = useState<GifMoodItem[]>([]);
|
||||||
@@ -304,7 +305,10 @@ export function GifMoodBoard({
|
|||||||
isCurrentUser && optimisticItems.length > 0 ? optimisticItems : serverItems;
|
isCurrentUser && optimisticItems.length > 0 ? optimisticItems : serverItems;
|
||||||
const canAdd = canEdit && isCurrentUser && userItems.length < GIF_MOOD_MAX_ITEMS;
|
const canAdd = canEdit && isCurrentUser && userItems.length < GIF_MOOD_MAX_ITEMS;
|
||||||
const accentColor = SECTION_COLORS[index % SECTION_COLORS.length];
|
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 (
|
return (
|
||||||
<section key={user.id}>
|
<section key={user.id}>
|
||||||
@@ -338,10 +342,60 @@ export function GifMoodBoard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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 */}
|
{/* Grid */}
|
||||||
{isCurrentUser && canEdit ? (
|
{!showHidden && isCurrentUser && canEdit ? (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
@@ -372,7 +426,7 @@ export function GifMoodBoard({
|
|||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
) : (
|
) : !showHidden ? (
|
||||||
<div className={`grid ${GRID_COLS[cols]} gap-4 items-start`}>
|
<div className={`grid ${GRID_COLS[cols]} gap-4 items-start`}>
|
||||||
{userItems.map((item) => (
|
{userItems.map((item) => (
|
||||||
<GifMoodCard
|
<GifMoodCard
|
||||||
@@ -389,7 +443,7 @@ export function GifMoodBoard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const gifMoodByIdInclude = {
|
|||||||
orderBy: { order: 'asc' as const },
|
orderBy: { order: 'asc' as const },
|
||||||
},
|
},
|
||||||
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
|
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) {
|
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
|
// Session Sharing
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user