// DEPLOY_CONFIG: {"triggers": [{"name": "email_finalized_retro_summary", "on": "collection.add", "collection": "retro_summaries", "actions": [{"type": "email", "to": "{{participants_emails}}", "subject": "Your finalized retro summary is ready", "body": "Hi everyone,\n\nThe retro summary has been finalized and is ready to review.\n\nSummary:\n{{summary}}\n\nStructured themes:\n{{structured_themes}}\n\nSuggested actions:\n{{suggested_actions}}\n\nBest,\nDeplixo"}]}]} import { useMemo, useState, useEffect, useCallback } from 'react'; import { useAuth, useIdentity, usePresence, playSound } from '@deplixo/sdk'; import { RetroBoard } from './components/RetroBoard.jsx'; import { Timer } from './components/Timer.jsx'; const FACILITATOR_ALLOWED_EMAILS = [ 'facilitator@deplixo.com' ]; function PresenceBar({ users = [], currentUserId }) { const activeUsers = useMemo(() => { const seen = new Map(); for (const user of users || []) { const id = user?.id || user?.userId || user?.uid || user?.email || user?.name; if (!id) continue; if (!seen.has(id)) { seen.set(id, { id, name: user?.name || user?.displayName || user?.email || 'Participant', avatar: user?.avatar || user?.photoURL || '', color: user?.color || '#8D6E63' }); } } return Array.from(seen.values()); }, [users]); if (!activeUsers.length) return null; return (
Live now
{activeUsers.slice(0, 6).map((participant) => { const isMe = currentUserId && participant.id === currentUserId; const initials = participant.name .split(' ') .filter(Boolean) .slice(0, 2) .map(part => part[0]) .join('') .toUpperCase(); return (
{participant.avatar ? ( {participant.name} ) : ( {initials || 'P'} )}
); })} {activeUsers.length > 6 && ( +{activeUsers.length - 6} )}
); } function App() { const { user, loading, logout } = useAuth(); const identity = useIdentity(); const [phase, setPhase] = useState('brainstorm'); const [cardGroups, setCardGroups] = useState({}); const [draggedCard, setDraggedCard] = useState(null); const [dragOverGroupId, setDragOverGroupId] = useState(null); const [phaseEndSoundPlayedFor, setPhaseEndSoundPlayedFor] = useState(null); const phases = [ { id: 'brainstorm', label: '💡 Brainstorm', duration: 300 }, { id: 'discuss', label: '💬 Discuss', duration: 300 }, { id: 'action', label: '🎯 Action Items', duration: 300 } ]; const currentPhase = phases.find(p => p.id === phase); const isFacilitator = Boolean( user && ( FACILITATOR_ALLOWED_EMAILS.includes((user.email || '').toLowerCase()) || (identity?.user?.id && user.id === identity.user.id) ) ); const presencePayload = useMemo(() => ({ id: user?.id || identity?.user?.id, name: user?.name || user?.displayName || user?.email || 'Participant', email: user?.email || '', avatar: user?.photoURL || user?.avatar || '', role: isFacilitator ? 'facilitator' : 'viewer' }), [user, identity?.user?.id, isFacilitator]); const { users: presenceUsers, update: updatePresence } = usePresence(presencePayload); useEffect(() => { if (!user || !updatePresence) return; updatePresence(presencePayload); }, [user, updatePresence, presencePayload]); useEffect(() => { const handleBeforeUnload = () => { if (updatePresence) updatePresence({ ...presencePayload, away: true }); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, [updatePresence, presencePayload]); useEffect(() => { if (!currentPhase) return; const timerHasEnded = currentPhase.remaining <= 0 || currentPhase.expired || currentPhase.status === 'expired'; if (timerHasEnded && phaseEndSoundPlayedFor !== currentPhase.id) { playSound('@ding'); setPhaseEndSoundPlayedFor(currentPhase.id); } if (!timerHasEnded && phaseEndSoundPlayedFor === currentPhase.id) { setPhaseEndSoundPlayedFor(null); } }, [currentPhase, phaseEndSoundPlayedFor]); const createGroup = (cardId) => { const groupId = `group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; setCardGroups(prev => ({ ...prev, [groupId]: [cardId] })); return groupId; }; const addCardToGroup = (cardId, groupId) => { if (!groupId) return; setCardGroups(prev => { const next = { ...prev }; const existingGroupId = Object.keys(next).find(id => next[id].includes(cardId)); if (existingGroupId && existingGroupId !== groupId) { next[existingGroupId] = next[existingGroupId].filter(id => id !== cardId); if (next[existingGroupId].length === 0) delete next[existingGroupId]; } const currentMembers = next[groupId] || []; if (!currentMembers.includes(cardId)) { next[groupId] = [...currentMembers, cardId]; } return next; }); }; const handleCardDragStart = (card) => { setDraggedCard(card); }; const handleCardDropOnCard = (targetCard) => { if (!draggedCard || draggedCard.id === targetCard.id) return; const sourceGroupId = Object.keys(cardGroups).find(id => cardGroups[id].includes(draggedCard.id)); const targetGroupId = Object.keys(cardGroups).find(id => cardGroups[id].includes(targetCard.id)); if (targetGroupId) { addCardToGroup(draggedCard.id, targetGroupId); } else if (sourceGroupId) { addCardToGroup(targetCard.id, sourceGroupId); } else { const newGroupId = createGroup(targetCard.id); addCardToGroup(draggedCard.id, newGroupId); } setDraggedCard(null); }; const handleDropOnGroup = (groupId) => { if (!draggedCard || !groupId) return; addCardToGroup(draggedCard.id, groupId); setDraggedCard(null); setDragOverGroupId(null); }; const handleUngroupCard = (cardId) => { setCardGroups(prev => { const next = {}; for (const [groupId, members] of Object.entries(prev)) { const filtered = members.filter(id => id !== cardId); if (filtered.length > 1) { next[groupId] = filtered; } else if (filtered.length === 1) { next[groupId] = filtered; } } return next; }); }; const boardGroupingProps = useMemo(() => ({ cardGroups, isGroupingEnabled: isFacilitator, onCardDragStart: handleCardDragStart, onCardDropOnCard: handleCardDropOnCard, onDropOnGroup: handleDropOnGroup, onGroupDragEnter: setDragOverGroupId, onGroupDragLeave: () => setDragOverGroupId(null), dragOverGroupId, onUngroupCard: handleUngroupCard }), [cardGroups, isFacilitator, dragOverGroupId]); if (loading) { return (

Brewing your retro...

); } if (!user) { return (

☕ Retro Brew

Sign in with Google to manage the retrospective board.

Facilitator access is required to change phases and manage board controls.

Continue with Google
); } return (

☕ Retro Brew

Sprint Retrospective Board
{isFacilitator ? 'Facilitator' : 'Viewer'}
{!isFacilitator && (
You’re signed in, but only the facilitator can manage board actions and phases.
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();