feat: add GIF Mood Board workshop
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m5s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m5s
- New workshop where each team member shares up to 5 GIFs with notes to express their weekly mood - Per-user week rating (1-5 stars) visible next to each member's section - Masonry-style grid with adjustable column count (3/4/5) toggle - Handwriting font (Caveat) for GIF notes - Full real-time collaboration via SSE - Clean migration (add_gif_mood_workshop) safe for production deploy - DB backup via cp before each migration in docker-entrypoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
126
src/components/gif-mood/GifMoodAddForm.tsx
Normal file
126
src/components/gif-mood/GifMoodAddForm.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { Button, Input } from '@/components/ui';
|
||||
import { addGifMoodItem } from '@/actions/gif-mood';
|
||||
import { GIF_MOOD_MAX_ITEMS } from '@/lib/types';
|
||||
|
||||
interface GifMoodAddFormProps {
|
||||
sessionId: string;
|
||||
currentCount: number;
|
||||
}
|
||||
|
||||
export function GifMoodAddForm({ sessionId, currentCount }: GifMoodAddFormProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [gifUrl, setGifUrl] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const remaining = GIF_MOOD_MAX_ITEMS - currentCount;
|
||||
|
||||
function handleUrlBlur() {
|
||||
const trimmed = gifUrl.trim();
|
||||
if (!trimmed) { setPreviewUrl(''); return; }
|
||||
try { new URL(trimmed); setPreviewUrl(trimmed); setError(null); }
|
||||
catch { setPreviewUrl(''); }
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const trimmed = gifUrl.trim();
|
||||
if (!trimmed) { setError("L'URL est requise"); return; }
|
||||
try { new URL(trimmed); } catch { setError('URL invalide'); return; }
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await addGifMoodItem(sessionId, {
|
||||
gifUrl: trimmed,
|
||||
note: note.trim() || undefined,
|
||||
});
|
||||
if (result.success) {
|
||||
setGifUrl(''); setNote(''); setPreviewUrl(''); setOpen(false);
|
||||
} else {
|
||||
setError(result.error || "Erreur lors de l'ajout");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collapsed state — placeholder card
|
||||
if (!open) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="group flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/50 hover:border-primary/40 hover:bg-primary/5 min-h-[120px] transition-all duration-200 text-muted hover:text-primary w-full"
|
||||
>
|
||||
<span className="text-2xl opacity-40 group-hover:opacity-70 transition-opacity">+</span>
|
||||
<span className="text-xs font-medium">{remaining} slot{remaining !== 1 ? 's' : ''} restant{remaining !== 1 ? 's' : ''}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded form
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="rounded-2xl border border-border bg-card shadow-sm p-4 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-foreground">Ajouter un GIF</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setOpen(false); setError(null); setPreviewUrl(''); }}
|
||||
className="text-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="URL du GIF"
|
||||
value={gifUrl}
|
||||
onChange={(e) => setGifUrl(e.target.value)}
|
||||
onBlur={handleUrlBlur}
|
||||
placeholder="https://media.giphy.com/…"
|
||||
disabled={isPending}
|
||||
/>
|
||||
|
||||
{previewUrl && (
|
||||
<div className="rounded-xl overflow-hidden border border-border/50">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Aperçu"
|
||||
className="w-full object-contain max-h-40"
|
||||
onError={() => setPreviewUrl('')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted mb-1">
|
||||
Note <span className="font-normal opacity-60">(optionnelle)</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="Ce que ce GIF exprime…"
|
||||
rows={2}
|
||||
disabled={isPending}
|
||||
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" loading={isPending} className="w-full" size="sm">
|
||||
Ajouter
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
273
src/components/gif-mood/GifMoodBoard.tsx
Normal file
273
src/components/gif-mood/GifMoodBoard.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState, useTransition } from 'react';
|
||||
import { setGifMoodUserRating } from '@/actions/gif-mood';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { GifMoodCard } from './GifMoodCard';
|
||||
import { GifMoodAddForm } from './GifMoodAddForm';
|
||||
import { GIF_MOOD_MAX_ITEMS } from '@/lib/types';
|
||||
|
||||
interface GifMoodItem {
|
||||
id: string;
|
||||
gifUrl: string;
|
||||
note: string | null;
|
||||
order: number;
|
||||
userId: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Share {
|
||||
id: string;
|
||||
userId: string;
|
||||
user: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GifMoodBoardProps {
|
||||
sessionId: string;
|
||||
currentUserId: string;
|
||||
items: GifMoodItem[];
|
||||
shares: Share[];
|
||||
owner: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
ratings: { userId: string; rating: number }[];
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
function WeekRating({
|
||||
sessionId,
|
||||
isCurrentUser,
|
||||
canEdit,
|
||||
initialRating,
|
||||
}: {
|
||||
sessionId: string;
|
||||
isCurrentUser: boolean;
|
||||
canEdit: boolean;
|
||||
initialRating: number | null;
|
||||
}) {
|
||||
const [prevInitialRating, setPrevInitialRating] = useState(initialRating);
|
||||
const [rating, setRating] = useState<number | null>(initialRating);
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
if (prevInitialRating !== initialRating) {
|
||||
setPrevInitialRating(initialRating);
|
||||
setRating(initialRating);
|
||||
}
|
||||
|
||||
const interactive = isCurrentUser && canEdit;
|
||||
const display = hovered ?? rating;
|
||||
|
||||
function handleClick(n: number) {
|
||||
if (!interactive) return;
|
||||
setRating(n);
|
||||
startTransition(async () => {
|
||||
await setGifMoodUserRating(sessionId, n);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-0.5"
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((n) => {
|
||||
const filled = display !== null && n <= display;
|
||||
return (
|
||||
<button
|
||||
key={n}
|
||||
type="button"
|
||||
onClick={() => handleClick(n)}
|
||||
onMouseEnter={() => interactive && setHovered(n)}
|
||||
disabled={!interactive || isPending}
|
||||
className={`transition-all duration-100 ${interactive ? 'cursor-pointer hover:scale-125' : 'cursor-default'}`}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
className={`transition-colors duration-100 ${
|
||||
filled ? 'text-amber-400' : 'text-border'
|
||||
}`}
|
||||
>
|
||||
<path
|
||||
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Subtle accent colors for each user section
|
||||
const SECTION_COLORS = ['#ec4899', '#8b5cf6', '#3b82f6', '#10b981', '#f59e0b', '#ef4444'];
|
||||
|
||||
const GRID_COLS: Record<number, string> = {
|
||||
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
5: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5',
|
||||
};
|
||||
|
||||
function GridIcon({ cols }: { cols: number }) {
|
||||
return (
|
||||
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" aria-hidden>
|
||||
{Array.from({ length: cols }).map((_, i) => {
|
||||
const w = (20 - (cols - 1) * 2) / cols;
|
||||
const x = i * (w + 2);
|
||||
return (
|
||||
<g key={i}>
|
||||
<rect x={x} y={0} width={w} height={6} rx={1} fill="currentColor" opacity={0.7} />
|
||||
<rect x={x} y={8} width={w} height={6} rx={1} fill="currentColor" opacity={0.4} />
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GifMoodBoard({
|
||||
sessionId,
|
||||
currentUserId,
|
||||
items,
|
||||
shares,
|
||||
owner,
|
||||
ratings,
|
||||
canEdit,
|
||||
}: GifMoodBoardProps) {
|
||||
const [cols, setCols] = useState(5);
|
||||
|
||||
const allUsers = useMemo(() => {
|
||||
const map = new Map<string, { id: string; name: string | null; email: string }>();
|
||||
map.set(owner.id, owner);
|
||||
shares.forEach((s) => map.set(s.userId, s.user));
|
||||
return Array.from(map.values());
|
||||
}, [owner, shares]);
|
||||
|
||||
const itemsByUser = useMemo(() => {
|
||||
const map = new Map<string, GifMoodItem[]>();
|
||||
items.forEach((item) => {
|
||||
const existing = map.get(item.userId) ?? [];
|
||||
existing.push(item);
|
||||
map.set(item.userId, existing);
|
||||
});
|
||||
return map;
|
||||
}, [items]);
|
||||
|
||||
const sortedUsers = useMemo(() => {
|
||||
return [...allUsers].sort((a, b) => {
|
||||
if (a.id === currentUserId) return -1;
|
||||
if (b.id === currentUserId) return 1;
|
||||
if (a.id === owner.id) return -1;
|
||||
if (b.id === owner.id) return 1;
|
||||
return (a.name || a.email).localeCompare(b.name || b.email, 'fr');
|
||||
});
|
||||
}, [allUsers, currentUserId, owner.id]);
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
{/* Column size control */}
|
||||
<div className="flex justify-end">
|
||||
<div className="inline-flex items-center gap-1 rounded-xl border border-border bg-card p-1">
|
||||
{[3, 4, 5].map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setCols(n)}
|
||||
className={`flex items-center justify-center rounded-lg px-2.5 py-1.5 transition-all ${
|
||||
cols === n
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted hover:text-foreground hover:bg-card-hover'
|
||||
}`}
|
||||
title={`${n} colonnes`}
|
||||
>
|
||||
<GridIcon cols={n} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{sortedUsers.map((user, index) => {
|
||||
const userItems = itemsByUser.get(user.id) ?? [];
|
||||
const isCurrentUser = user.id === currentUserId;
|
||||
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;
|
||||
|
||||
return (
|
||||
<section key={user.id}>
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-4 mb-5">
|
||||
{/* Colored accent bar */}
|
||||
<div className="w-1 h-8 rounded-full shrink-0" style={{ backgroundColor: accentColor }} />
|
||||
|
||||
<Avatar email={user.email} name={user.name} size={36} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-foreground truncate">
|
||||
{user.name || user.email}
|
||||
</span>
|
||||
{isCurrentUser && (
|
||||
<span className="text-xs text-muted bg-card-hover px-2 py-0.5 rounded-full border border-border">
|
||||
vous
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<p className="text-xs text-muted">
|
||||
{userItems.length} / {GIF_MOOD_MAX_ITEMS} GIF{userItems.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<WeekRating
|
||||
sessionId={sessionId}
|
||||
isCurrentUser={isCurrentUser}
|
||||
canEdit={canEdit}
|
||||
initialRating={userRating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className={`grid ${GRID_COLS[cols]} gap-4 items-start`}>
|
||||
{userItems.map((item) => (
|
||||
<GifMoodCard
|
||||
key={item.id}
|
||||
sessionId={sessionId}
|
||||
item={item}
|
||||
currentUserId={currentUserId}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Add form slot */}
|
||||
{canAdd && (
|
||||
<GifMoodAddForm sessionId={sessionId} currentCount={userItems.length} />
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!canAdd && userItems.length === 0 && (
|
||||
<div className="col-span-full flex items-center justify-center rounded-2xl border border-dashed border-border/60 py-10">
|
||||
<p className="text-sm text-muted/60">Aucun GIF pour le moment</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/components/gif-mood/GifMoodCard.tsx
Normal file
112
src/components/gif-mood/GifMoodCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
|
||||
import { memo, useState, useTransition } from 'react';
|
||||
import { updateGifMoodItem, deleteGifMoodItem } from '@/actions/gif-mood';
|
||||
|
||||
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);
|
||||
|
||||
if (itemVersion !== item) {
|
||||
setItemVersion(item);
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Note */}
|
||||
{canEditThis ? (
|
||||
<div className="px-3 pt-2 pb-3 bg-card">
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
onBlur={handleNoteBlur}
|
||||
placeholder="Ajouter une note…"
|
||||
rows={1}
|
||||
className="w-full text-foreground/70 bg-transparent resize-none outline-none placeholder:text-muted/40 leading-relaxed text-center"
|
||||
style={{ fontFamily: 'var(--font-caveat)', fontSize: '1.2rem' }}
|
||||
/>
|
||||
</div>
|
||||
) : note ? (
|
||||
<div className="px-3 py-2.5 bg-card">
|
||||
<p className="text-foreground/70 leading-relaxed text-center" style={{ fontFamily: 'var(--font-caveat)', fontSize: '1.2rem' }}>{note}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
62
src/components/gif-mood/GifMoodLiveWrapper.tsx
Normal file
62
src/components/gif-mood/GifMoodLiveWrapper.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { BaseSessionLiveWrapper } from '@/components/collaboration/BaseSessionLiveWrapper';
|
||||
import {
|
||||
shareGifMoodSession,
|
||||
shareGifMoodSessionToTeam,
|
||||
removeGifMoodShare,
|
||||
} from '@/actions/gif-mood';
|
||||
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||
|
||||
interface GifMoodLiveWrapperProps {
|
||||
sessionId: string;
|
||||
sessionTitle: string;
|
||||
currentUserId: string;
|
||||
shares: Share[];
|
||||
isOwner: boolean;
|
||||
canEdit: boolean;
|
||||
userTeams?: TeamWithMembers[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function GifMoodLiveWrapper({
|
||||
sessionId,
|
||||
sessionTitle,
|
||||
currentUserId,
|
||||
shares,
|
||||
isOwner,
|
||||
canEdit,
|
||||
userTeams = [],
|
||||
children,
|
||||
}: GifMoodLiveWrapperProps) {
|
||||
return (
|
||||
<BaseSessionLiveWrapper
|
||||
sessionId={sessionId}
|
||||
sessionTitle={sessionTitle}
|
||||
currentUserId={currentUserId}
|
||||
shares={shares}
|
||||
isOwner={isOwner}
|
||||
canEdit={canEdit}
|
||||
userTeams={userTeams}
|
||||
config={{
|
||||
apiPath: 'gif-mood',
|
||||
shareModal: {
|
||||
title: 'Partager le GIF Mood Board',
|
||||
sessionSubtitle: 'GIF Mood Board',
|
||||
helpText: (
|
||||
<>
|
||||
<strong>Éditeur</strong> : peut ajouter ses GIFs et voir ceux des autres
|
||||
<br />
|
||||
<strong>Lecteur</strong> : peut uniquement consulter
|
||||
</>
|
||||
),
|
||||
},
|
||||
onShareWithEmail: (email, role) => shareGifMoodSession(sessionId, email, role),
|
||||
onShareWithTeam: (teamId, role) => shareGifMoodSessionToTeam(sessionId, teamId, role),
|
||||
onRemoveShare: (userId) => removeGifMoodShare(sessionId, userId),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseSessionLiveWrapper>
|
||||
);
|
||||
}
|
||||
4
src/components/gif-mood/index.ts
Normal file
4
src/components/gif-mood/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { GifMoodBoard } from './GifMoodBoard';
|
||||
export { GifMoodCard } from './GifMoodCard';
|
||||
export { GifMoodAddForm } from './GifMoodAddForm';
|
||||
export { GifMoodLiveWrapper } from './GifMoodLiveWrapper';
|
||||
Reference in New Issue
Block a user