// 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 ? (

) : (
{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 (
);
}
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 (
{!isFacilitator && (
You’re signed in, but only the facilitator can manage board actions and phases.
)}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();