/* @component-map * App — Main container, tab navigation [app.jsx] * HuntSetup — Organizer creates/manages scavenger hunt items [components/HuntSetup.jsx] * PlayerView — Players check off found items [components/PlayerView.jsx] * Leaderboard — Rankings and scores [components/Leaderboard.jsx] * @end-component-map */ import { useEffect, useMemo, useState } from 'react'; import { useIdentity, playSound } from '@deplixo/sdk'; import { HuntSetup } from './components/HuntSetup.jsx'; import { PlayerView } from './components/PlayerView.jsx'; import { Leaderboard } from './components/Leaderboard.jsx'; function App() { const { user, loading } = useIdentity(); const [role, setRole] = useState(null); const [activeTab, setActiveTab] = useState('play'); const [huntInvite, setHuntInvite] = useState(null); const [copyState, setCopyState] = useState('idle'); const normalizedInvite = useMemo(() => { if (!huntInvite) return null; return String(huntInvite).trim(); }, [huntInvite]); useEffect(() => { const params = new URLSearchParams(window.location.search); const inviteFromUrl = params.get('hunt') || params.get('invite') || params.get('context'); if (inviteFromUrl) { setHuntInvite(inviteFromUrl); setRole('player'); setActiveTab('play'); } }, []); useEffect(() => { if (!copyState || copyState === 'idle') return; const timer = window.setTimeout(() => setCopyState('idle'), 2200); return () => window.clearTimeout(timer); }, [copyState]); const inviteUrl = useMemo(() => { if (typeof window === 'undefined') return ''; const url = new URL(window.location.href); url.searchParams.set('hunt', normalizedInvite || `hunt-${user?.id || 'shared'}`); url.searchParams.set('role', 'player'); return url.toString(); }, [normalizedInvite, user]); const inviteText = useMemo(() => { const shareLabel = normalizedInvite ? `Join my scavenger hunt (${normalizedInvite})` : 'Join my scavenger hunt'; return `${shareLabel}: ${inviteUrl}`; }, [normalizedInvite, inviteUrl]); const handleCreateInvite = async () => { const base = normalizedInvite || `hunt-${user?.id || 'shared'}`; const shareToken = `invite-${base}-${Date.now().toString(36)}`; const url = new URL(window.location.href); url.searchParams.set('hunt', shareToken); url.searchParams.set('role', 'player'); setHuntInvite(shareToken); setRole('organizer'); setActiveTab('setup'); playSound('@ding'); return url.toString(); }; const handleCopyInvite = async () => { const inviteLink = await handleCreateInvite(); const text = `Join my scavenger hunt: ${inviteLink}`; try { await navigator.clipboard.writeText(text); setCopyState('copied'); playSound('@success'); } catch { setCopyState('error'); playSound('@error'); } }; if (loading) { return (

Loading Scavenger Hunt...

); } if (!role) { return (
🔍

Scavenger Hunt

Welcome, {user.name}! Choose your role to get started.

); } const tabs = role === 'organizer' ? [{ id: 'setup', label: '📋 Hunt Setup' }, { id: 'leaderboard', label: '🏆 Leaderboard' }] : [{ id: 'play', label: '🔍 Find Items' }, { id: 'leaderboard', label: '🏆 Leaderboard' }]; const defaultTab = role === 'organizer' ? 'setup' : 'play'; if (!tabs.find(t => t.id === activeTab)) setActiveTab(defaultTab); return (
🔍

Scavenger Hunt

{user.name}
{role === 'organizer' && (

Invite Players

Generate a shareable hunt link or copy an invitation message to send to players.

{normalizedInvite ? 'Active invite' : 'Ready to share'}

{inviteText}

{copyState === 'copied' &&

Invite copied to clipboard.

} {copyState === 'error' &&

Could not copy automatically. Please copy the link manually.

}
)} {activeTab === 'setup' && } {activeTab === 'play' && } {activeTab === 'leaderboard' && }
); } ReactDOM.createRoot(document.getElementById("root")).render();