/* @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 (
{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();