// DEPLOY_CONFIG: {"cron": [{"name": "weekly_board_reset", "schedule": "0 0 * * 0", "action": "event", "config": {"event_type": "reset_board"}}}]
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { SignupBoard } from './components/SignupBoard.jsx';
import { useCollection, useIdentity, useReactions, playSound, share } from '@deplixo/sdk';
function ReactionBar({ targetId, onReactionSound }) {
const { counts, toggle, loading } = useReactions(targetId);
const emojis = ['๐', 'โค๏ธ', '๐', '๐ฅ', '๐'];
const totalReactions = useMemo(
() => emojis.reduce((sum, emoji) => sum + (counts[emoji] || 0), 0),
[counts]
);
const lastTotalRef = useRef(totalReactions);
useEffect(() => {
if (loading) return;
const previousTotal = lastTotalRef.current;
if (totalReactions > previousTotal) {
onReactionSound?.(totalReactions, previousTotal);
}
lastTotalRef.current = totalReactions;
}, [totalReactions, loading, onReactionSound]);
if (loading) return null;
return (
{emojis.map((emoji) => (
))}
);
}
function App() {
const [activeTab, setActiveTab] = useState('lineup');
const [presenceUsers, setPresenceUsers] = useState([]);
const [presenceId] = useState(() => `viewer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
const [shareStatus, setShareStatus] = useState('');
const { user: identityUser } = useIdentity();
const { items: performances = [] } = useCollection('openmic-performances', { personal: true });
const { items: performerProfiles = [] } = useCollection('openmic-performer-identities', { personal: true });
const { items: eventHistory = [], add: addEventRecord, update: updateEventRecord } = useCollection('openmic-event-history', { personal: true });
const { items: presenceItems = [] } = useCollection('deplixo-openmic-presence', { personal: true });
const { add: addPresence, update: updatePresence, remove: removePresence } = useCollection('deplixo-openmic-presence', { personal: true });
const lastNowPlayingIdRef = useRef(null);
const applauseCooldownRef = useRef(0);
const nextMusicCooldownRef = useRef(0);
const lastSyncedEventKeyRef = useRef('');
const lastSyncedPerformersRef = useRef('');
const normalizeText = (value) => (typeof value === 'string' ? value.trim() : '');
const performerIdentity = useMemo(() => {
const identityName = normalizeText(identityUser?.name) || 'Performer';
const identityAvatar = normalizeText(identityUser?.avatar) || '';
const identityId = normalizeText(identityUser?.id) || '';
return {
id: identityId,
name: identityName,
avatar: identityAvatar,
};
}, [identityUser?.id, identityUser?.name, identityUser?.avatar]);
const performersByIdentity = useMemo(() => {
const map = new Map();
performerProfiles.forEach((profile) => {
if (!profile) return;
const identityId = normalizeText(profile.identityId || profile.userId || profile.id);
if (!identityId) return;
map.set(identityId, profile);
});
return map;
}, [performerProfiles]);
const currentPerformances = useMemo(() => {
return performances
.filter((performance) => performance && typeof performance === 'object')
.map((performance) => {
const performerId = normalizeText(performance.performerId || performance.identityId || performance.userId || performance.id || '');
const storedProfile = performersByIdentity.get(performerId);
const performerName = normalizeText(performance.performerName || storedProfile?.name || performance.title || 'Performer') || 'Performer';
const performerAvatar = normalizeText(performance.performerAvatar || storedProfile?.avatar || '') || '';
return {
...performance,
performerId,
performerName,
performerAvatar,
};
});
}, [performances, performersByIdentity]);
const currentEvent = useMemo(() => {
const nowPlaying = currentPerformances.find((performance) => performance?.status === 'performing') || currentPerformances[0] || null;
return nowPlaying;
}, [currentPerformances]);
const shareUrl = useMemo(() => {
if (typeof window === 'undefined' || !window.location) return '';
const url = new URL(window.location.href);
const eventId = currentEvent?.id ? String(currentEvent.id) : '';
if (eventId) {
url.searchParams.set('event', eventId);
} else {
url.searchParams.delete('event');
}
url.searchParams.set('tab', 'lineup');
return url.toString();
}, [currentEvent?.id]);
const copyShareLink = async () => {
if (!shareUrl) return;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(shareUrl);
} else {
const tempInput = document.createElement('input');
tempInput.value = shareUrl;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
}
setShareStatus('Link copied');
window.setTimeout(() => setShareStatus(''), 2000);
} catch {
setShareStatus('Copy failed');
window.setTimeout(() => setShareStatus(''), 2000);
}
};
const shareEventLink = async () => {
if (!shareUrl) return;
try {
if (navigator?.share) {
await navigator.share({
title: 'Open Mic Night',
text: currentEvent ? `Join the open mic: ${currentEvent.performerName}` : 'Join the open mic night',
url: shareUrl,
});
setShareStatus('Shared');
window.setTimeout(() => setShareStatus(''), 2000);
} else {
await copyShareLink();
}
} catch {
setShareStatus('Share canceled');
window.setTimeout(() => setShareStatus(''), 2000);
}
};
const syncPresence = useCallback(() => {
const now = Date.now();
const viewer = {
id: presenceId,
name: 'You',
status: 'viewing',
tab: activeTab,
lastSeen: now,
};
const loadPresence = () => {
try {
const alive = presenceItems
.filter((user) => user && typeof user.lastSeen === 'number' && now - user.lastSeen < 15000)
.filter((user) => user.id !== presenceId);
const next = [viewer, ...alive];
const existingIndex = presenceItems.findIndex((item) => item.id === presenceId);
if (existingIndex >= 0) {
updatePresence(presenceItems[existingIndex].id, next);
} else {
addPresence(next);
}
setPresenceUsers(next);
} catch {
setPresenceUsers([viewer]);
}
};
loadPresence();
}, [activeTab, presenceId, presenceItems, addPresence, updatePresence]);
useEffect(() => {
syncPresence();
const interval = setInterval(syncPresence, 5000);
const handleVisibility = () => {
if (document.visibilityState === 'visible') syncPresence();
};
window.addEventListener('focus', syncPresence);
window.addEventListener('visibilitychange', handleVisibility);
return () => {
clearInterval(interval);
window.removeEventListener('focus', syncPresence);
window.removeEventListener('visibilitychange', handleVisibility);
try {
const next = presenceItems.filter((user) => user && user.id !== presenceId);
const existingIndex = presenceItems.findIndex((item) => item.id === presenceId);
if (existingIndex >= 0) {
updatePresence(presenceItems[existingIndex].id, next);
}
} catch {}
};
}, [syncPresence, presenceId, presenceItems, updatePresence]);
useEffect(() => {
const now = Date.now();
try {
const alive = presenceItems
.filter((user) => user && typeof user.lastSeen === 'number' && now - user.lastSeen < 15000);
setPresenceUsers(alive);
} catch {
setPresenceUsers([]);
}
}, [presenceItems]);
useEffect(() => {
const nowPlaying = currentPerformances.find((performance) => performance?.status === 'performing') || currentPerformances[0] || null;
const nowPlayingId = nowPlaying?.id || null;
if (nowPlayingId && lastNowPlayingIdRef.current && lastNowPlayingIdRef.current !== nowPlayingId) {
const now = Date.now();
if (now - nextMusicCooldownRef.current > 1500) {
playSound('@whoosh');
nextMusicCooldownRef.current = now;
}
}
lastNowPlayingIdRef.current = nowPlayingId;
}, [currentPerformances]);
useEffect(() => {
const performerId = performerIdentity.id;
if (!performerId) return;
const profileKey = JSON.stringify({
id: performerIdentity.id,
name: performerIdentity.name,
avatar: performerIdentity.avatar,
});
if (lastSyncedPerformersRef.current !== profileKey) {
lastSyncedPerformersRef.current = profileKey;
const existing = performerProfiles.find(
(profile) => profile && (profile.identityId === performerId || profile.userId === performerId || profile.id === performerId)
);
const nextProfile = {
...(existing || {}),
identityId: performerId,
userId: performerId,
name: performerIdentity.name,
avatar: performerIdentity.avatar,
updatedAt: Date.now(),
};
if (existing?.id) {
updateEventRecord?.(existing.id, nextProfile).catch(() => {});
}
}
}, [performerIdentity.id, performerIdentity.name, performerIdentity.avatar, performerProfiles, updateEventRecord]);
useEffect(() => {
const event = currentEvent;
if (!event) return;
const eventKey = JSON.stringify({
eventId: event.id || '',
performerId: event.performerId || '',
performerName: event.performerName || '',
status: event.status || '',
});
if (lastSyncedEventKeyRef.current === eventKey) return;
lastSyncedEventKeyRef.current = eventKey;
const existingRecord = eventHistory.find(
(record) => record && record.eventId === event.id && record.performerId === (event.performerId || '')
);
const performerProfile = event.performerId ? performersByIdentity.get(event.performerId) : null;
const payload = {
eventId: event.id || '',
performerId: event.performerId || '',
performerName: event.performerName || performerProfile?.name || 'Performer',
performerAvatar: event.performerAvatar || performerProfile?.avatar || '',
status: event.status || 'scheduled',
title: event.title || event.name || 'Open Mic Set',
startedAt: event.startedAt || Date.now(),
updatedAt: Date.now(),
};
if (existingRecord?.id) {
updateEventRecord(existingRecord.id, payload).catch(() => {});
} else {
addEventRecord(payload).catch(() => {});
}
}, [currentEvent, eventHistory, performersByIdentity, addEventRecord, updateEventRecord]);
const audienceCount = presenceUsers.length;
const audienceList = useMemo(() => presenceUsers.slice(0, 6), [presenceUsers]);
const nowPlaying = currentPerformances.find((performance) => performance?.status === 'performing') || currentPerformances[0] || null;
const reactionTargetId = nowPlaying?.id ? `performance-${nowPlaying.id}` : null;
const performerHistory = useMemo(() => {
return eventHistory
.filter((record) => record && record.performerId)
.map((record) => ({
...record,
performerName: normalizeText(record.performerName) || 'Performer',
}))
.slice(0, 8);
}, [eventHistory]);
const handleReactionSound = (totalReactions, previousTotal) => {
const now = Date.now();
const crossedThreshold = previousTotal < 5 && totalReactions >= 5;
const newReactionReceived = totalReactions > previousTotal;
if (newReactionReceived && (crossedThreshold || now - applauseCooldownRef.current > 1500)) {
playSound('@success');
applauseCooldownRef.current = now;
}
};
return (
๐ค
Open Mic Night
Sign up, step up, shine
Audience Live
People currently viewing this open mic night
{audienceCount} live
{audienceList.length > 0 ? (
audienceList.map((user) => (
{user.name?.[0]?.toUpperCase() || 'A'}
))
) : (
No active viewers detected yet.
)}
{audienceList.length > 0 && (
{audienceList.map((user) => (
-
{user.name}
{user.id === presenceId ? ' (you)' : ''}
{user.tab === 'signup' ? 'Sign Up' : 'Lineup'}
))}
)}
Performer History
Identity and event records persisted across sessions
{performerHistory.length} records
{performerHistory.length > 0 ? (
) : (
No performer history yet. Start or switch a set to create records.
)}
{currentEvent && (
Share this event
Copy or share a link to invite others to the current open mic.
Live link
{shareStatus && {shareStatus}
}
)}
{activeTab === 'lineup' && reactionTargetId && (
Cheer the performer
Tap an emoji to react in real time
Live
)}
);
}
ReactDOM.createRoot(document.getElementById('root')).render();