import { useIdentity, useCollection, useBroadcast, usePresence } from '@deplixo/sdk'; /* @component-map * App — Main container, tab navigation [app.jsx] * TypingTest — Core typing test interface with real-time WPM/accuracy [components/TypingTest.jsx] * Results — Post-test results screen with detailed stats [components/Results.jsx] * PersonalBests — Historical best records list [components/PersonalBests.jsx] * @end-component-map */ import { useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { TypingTest } from './components/TypingTest.jsx'; import { Results } from './components/Results.jsx'; import { PersonalBests } from './components/PersonalBests.jsx'; import { renderChart } from '@deplixo/sdk'; const DEFAULT_RACE_STATE = { status: 'idle', startedAt: null, countdownEndsAt: null, endedAt: null, }; function HistoryChart({ records, currentUserId }) { const canvasRef = useRef(null); const chartData = useMemo(() => { const items = Array.isArray(records) ? records : []; const sorted = [...items] .filter((record) => record && typeof record.wpm === 'number') .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0)); const mine = currentUserId ? sorted.filter((record) => record.userId === currentUserId) : sorted; const data = (mine.length ? mine : sorted).slice(-12); return { labels: data.map((record) => { const d = new Date(record.createdAt || Date.now()); return `${d.getMonth() + 1}/${d.getDate()}`; }), values: data.map((record) => Number(record.wpm || 0)), accuracy: data.map((record) => Number(record.accuracy || 0)), count: data.length, }; }, [records, currentUserId]); useEffect(() => { if (!canvasRef.current || !chartData.count) return; renderChart(canvasRef.current, { type: 'line', data: { labels: chartData.labels, datasets: [ { label: 'WPM', data: chartData.values, }, { label: 'Accuracy %', data: chartData.accuracy, }, ], }, }); }, [chartData]); if (!chartData.count) { return
No history yet. Complete more tests to see your WPM trend.
; } return (

Progress over time

Last {chartData.count} runs
); } function App() { const [view, setView] = useState('test'); const [lastResult, setLastResult] = useState(null); const [activeTab, setActiveTab] = useState('test'); const { user: identityUser, loading: identityLoading } = useIdentity(); const [localUserId, setLocalUserId] = useState(''); const [selectedShareRecordId, setSelectedShareRecordId] = useState(''); const [shareFeedback, setShareFeedback] = useState(''); const { items: leaderboardItems, loading: leaderboardLoading, add: addLeaderboardRecord } = useCollection('typepulse_leaderboard', { personal: true }); const { items: raceRoomItems, loading: raceRoomsLoading, add: addRaceRoom, update: updateRaceRoom, remove: removeRaceRoom } = useCollection('typepulse_race_rooms', { personal: true }); const { items: raceParticipantItems, loading: raceParticipantsLoading, add: addRaceParticipant, update: updateRaceParticipant, remove: removeRaceParticipant } = useCollection('typepulse_race_participants', { personal: true }); const currentUserId = localUserId || (identityUser ? identityUser.id : ''); const currentUsername = identityUser?.name || identityUser?.username || identityUser?.email || 'Anonymous'; const presenceRoomId = 'typepulse-lobby'; const { users: lobbyUsers, update: updateLobbyPresence } = usePresence({ userId: currentUserId, username: currentUsername, roomId: presenceRoomId, status: 'online' }); useEffect(() => { if (identityUser && identityUser.id && !localUserId) { setLocalUserId(identityUser.id); } }, [identityUser, localUserId]); useEffect(() => { if (!currentUserId) return; updateLobbyPresence({ userId: currentUserId, username: currentUsername, roomId: presenceRoomId, status: 'online' }); }, [currentUserId, currentUsername, presenceRoomId, updateLobbyPresence]); const leaderboard = useMemo(() => { const items = Array.isArray(leaderboardItems) ? leaderboardItems : []; return [...items] .filter((record) => record && typeof record.wpm === 'number') .sort((a, b) => { const wpmDiff = (b.wpm || 0) - (a.wpm || 0); if (wpmDiff !== 0) return wpmDiff; const accDiff = (b.accuracy || 0) - (a.accuracy || 0); if (accDiff !== 0) return accDiff; return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); }) .slice(0, 10); }, [leaderboardItems]); const personalBest = useMemo(() => { if (!currentUserId) return null; const mine = leaderboard.filter((record) => record.userId === currentUserId); return mine[0] || null; }, [leaderboard, currentUserId]); const shareableRecords = useMemo(() => { const items = Array.isArray(leaderboardItems) ? leaderboardItems : []; return items .filter((record) => record && record.userId === currentUserId && typeof record.wpm === 'number') .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)); }, [leaderboardItems, currentUserId]); const selectedShareRecord = useMemo(() => { if (!selectedShareRecordId) return shareableRecords[0] || null; return shareableRecords.find((record) => record.id === selectedShareRecordId) || shareableRecords[0] || null; }, [selectedShareRecordId, shareableRecords]); const buildShareData = useCallback((record) => { if (!record) return null; const title = `TypePulse PB: ${Number(record.wpm || 0)} WPM`; const text = `I just hit ${Number(record.wpm || 0)} WPM with ${Number(record.accuracy || 0)}% accuracy on TypePulse!`; const url = typeof window !== 'undefined' ? window.location.href : ''; return { title, text, url, shareText: `${text}${url ? `\n${url}` : ''}`, }; }, []); const copyToClipboard = useCallback(async (text) => { if (!text) return false; try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', 'true'); textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); return true; } catch { return false; } }, []); const handleShareRecord = useCallback(async (record) => { const shareData = buildShareData(record); if (!shareData) return; const copied = await copyToClipboard(`${shareData.text}${shareData.url ? `\n${shareData.url}` : ''}`); if (typeof navigator !== 'undefined' && navigator.share) { try { await navigator.share({ title: shareData.title, text: shareData.text, url: shareData.url || undefined, }); setShareFeedback('Shared successfully.'); return; } catch { // User may cancel native share; fall back to copied message. } } setShareFeedback(copied ? 'Share text copied to clipboard.' : 'Could not copy share text.'); }, [buildShareData, copyToClipboard]); const handleCopyShareLink = useCallback(async (record) => { const shareData = buildShareData(record); if (!shareData) return; const copied = await copyToClipboard(`${shareData.text}${shareData.url ? `\n${shareData.url}` : ''}`); setShareFeedback(copied ? 'Share text copied to clipboard.' : 'Could not copy share text.'); }, [buildShareData, copyToClipboard]); const rooms = useMemo(() => { const items = Array.isArray(raceRoomItems) ? raceRoomItems : []; return [...items].sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0)); }, [raceRoomItems]); const participants = useMemo(() => Array.isArray(raceParticipantItems) ? raceParticipantItems : [], [raceParticipantItems]); const myRoom = useMemo(() => rooms.find((room) => room?.ownerId === currentUserId || room?.players?.some?.((p) => p.userId === currentUserId)) || null, [rooms, currentUserId]); const myParticipant = useMemo(() => participants.find((p) => p?.userId === currentUserId && (!myRoom || p.roomId === myRoom.id)) || null, [participants, currentUserId, myRoom]); const [roomCodeInput, setRoomCodeInput] = useState(''); const [raceDraft, setRaceDraft] = useState({ roomId: '', roomCode: '', status: 'idle', progress: 0, wpm: 0, accuracy: 100, finished: false, opponentProgress: 0, opponentName: '', opponentConnected: false, countdownEndsAt: null, startedAt: null, endedAt: null, message: '', }); const broadcastKey = useMemo(() => `typepulse-race-${raceDraft.roomCode || 'lobby'}`, [raceDraft.roomCode]); const handleBroadcast = useCallback((data) => { if (!data || data.roomId !== raceDraft.roomId) return; if (data.type === 'sync') { setRaceDraft((prev) => ({ ...prev, status: data.status || prev.status, countdownEndsAt: data.countdownEndsAt ?? prev.countdownEndsAt, startedAt: data.startedAt ?? prev.startedAt, endedAt: data.endedAt ?? prev.endedAt, message: data.message || prev.message, })); } if (data.type === 'progress' && data.userId !== currentUserId) { setRaceDraft((prev) => ({ ...prev, opponentProgress: Number(data.progress || 0), opponentName: data.username || prev.opponentName, opponentConnected: true, })); } if (data.type === 'finished' && data.userId !== currentUserId) { setRaceDraft((prev) => ({ ...prev, opponentProgress: 100, opponentConnected: true, message: data.message || prev.message, })); } if (data.type === 'leave' && data.userId !== currentUserId) { setRaceDraft((prev) => ({ ...prev, opponentConnected: false, message: 'Opponent disconnected.', })); } }, [currentUserId, raceDraft.roomId]); const { send: sendRaceBroadcast } = useBroadcast(broadcastKey, handleBroadcast); const { users: presenceUsers, update: updatePresence } = usePresence({ userId: currentUserId, username: currentUsername, roomId: raceDraft.roomId }); const activeOpponent = useMemo(() => { const other = participants.find((p) => p?.roomId === raceDraft.roomId && p.userId !== currentUserId) || null; return other; }, [participants, raceDraft.roomId, currentUserId]); const lobbyAvailableRooms = useMemo(() => { const roomMap = new Map(); rooms.forEach((room) => { if (!room?.id) return; const roomParticipants = participants.filter((p) => p?.roomId === room.id); roomMap.set(room.id, { ...room, onlinePlayers: roomParticipants.filter((p) => p?.connected !== false).length, totalPlayers: roomParticipants.length || (Array.isArray(room.players) ? room.players.length : 0), }); }); return Array.from(roomMap.values()); }, [rooms, participants]); const lobbyOnlineUsers = useMemo(() => { const users = Array.isArray(lobbyUsers) ? lobbyUsers : []; const unique = new Map(); users.forEach((u) => { if (!u?.userId) return; unique.set(u.userId, u); }); return Array.from(unique.values()); }, [lobbyUsers]); const currentRoomOccupancy = useMemo(() => { if (!raceDraft.roomId) return null; const room = rooms.find((r) => r.id === raceDraft.roomId) || myRoom; const roomParticipants = participants.filter((p) => p?.roomId === raceDraft.roomId); return { online: roomParticipants.filter((p) => p?.connected !== false).length, total: roomParticipants.length || (Array.isArray(room?.players) ? room.players.length : 0), }; }, [raceDraft.roomId, rooms, participants, myRoom]); useEffect(() => { if (myRoom) { setRaceDraft((prev) => ({ ...prev, roomId: myRoom.id || '', roomCode: myRoom.code || '', status: myRoom.status || 'idle', countdownEndsAt: myRoom.countdownEndsAt || null, startedAt: myRoom.startedAt || null, endedAt: myRoom.endedAt || null, })); } }, [myRoom]); useEffect(() => { if (!currentUserId || !raceDraft.roomId) return; updatePresence({ userId: currentUserId, username: currentUsername, roomId: raceDraft.roomId }); }, [currentUserId, currentUsername, raceDraft.roomId, updatePresence]); useEffect(() => { if (!raceDraft.roomId || !currentUserId) return; const opponentPresent = (presenceUsers || []).some((u) => u?.userId && u.userId !== currentUserId && u?.roomId === raceDraft.roomId); setRaceDraft((prev) => ({ ...prev, opponentConnected: opponentPresent })); }, [presenceUsers, raceDraft.roomId, currentUserId]); useEffect(() => { const syncTimer = setInterval(() => { if (!raceDraft.countdownEndsAt || raceDraft.status !== 'countdown') return; const remaining = new Date(raceDraft.countdownEndsAt).getTime() - Date.now(); if (remaining <= 0) { setRaceDraft((prev) => ({ ...prev, status: 'racing', message: 'Go!' })); } }, 250); return () => clearInterval(syncTimer); }, [raceDraft.countdownEndsAt, raceDraft.status]); const createRaceRoom = async () => { if (!currentUserId) return; const code = Math.random().toString(36).slice(2, 7).toUpperCase(); const room = { code, ownerId: currentUserId, ownerName: currentUsername, status: 'waiting', players: [{ userId: currentUserId, username: currentUsername }], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...DEFAULT_RACE_STATE, }; const created = await addRaceRoom(room); if (created?.id) { await addRaceParticipant({ roomId: created.id, userId: currentUserId, username: currentUsername, progress: 0, wpm: 0, accuracy: 100, connected: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); setRaceDraft((prev) => ({ ...prev, roomId: created.id, roomCode: code, status: 'waiting', message: 'Room created. Share the code to invite your opponent.' })); } }; const joinRaceRoom = async () => { if (!currentUserId || !roomCodeInput.trim()) return; const code = roomCodeInput.trim().toUpperCase(); const room = rooms.find((r) => String(r.code || '').toUpperCase() === code); if (!room) { setRaceDraft((prev) => ({ ...prev, message: 'Room not found.' })); return; } const players = Array.isArray(room.players) ? room.players : []; const nextRoom = { ...room, status: 'waiting', players: players.some((p) => p.userId === currentUserId) ? players : [...players, { userId: currentUserId, username: currentUsername }], updatedAt: new Date().toISOString(), }; await updateRaceRoom(room.id, nextRoom); await addRaceParticipant({ roomId: room.id, userId: currentUserId, username: currentUsername, progress: 0, wpm: 0, accuracy: 100, connected: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); setRaceDraft((prev) => ({ ...prev, roomId: room.id, roomCode: code, status: 'waiting', message: 'Joined room. Wait for the host to start.' })); }; const startRace = async () => { if (!myRoom) return; const countdownEndsAt = new Date(Date.now() + 3000).toISOString(); const startedAt = new Date(Date.now() + 3000).toISOString(); const nextRoom = { ...myRoom, status: 'countdown', countdownEndsAt, startedAt, updatedAt: new Date().toISOString() }; await updateRaceRoom(myRoom.id, nextRoom); sendRaceBroadcast({ type: 'sync', roomId: myRoom.id, status: 'countdown', countdownEndsAt, startedAt, message: 'Race starting soon...' }); setRaceDraft((prev) => ({ ...prev, status: 'countdown', countdownEndsAt, startedAt, message: 'Race starting soon...' })); }; const leaveRoom = async () => { if (!raceDraft.roomId) return; sendRaceBroadcast({ type: 'leave', roomId: raceDraft.roomId, userId: currentUserId }); await removeRaceParticipant?.(myParticipant?.id); if (myRoom?.ownerId === currentUserId) { await removeRaceRoom?.(myRoom.id); } setRaceDraft({ roomId: '', roomCode: '', status: 'idle', progress: 0, wpm: 0, accuracy: 100, finished: false, opponentProgress: 0, opponentName: '', opponentConnected: false, countdownEndsAt: null, startedAt: null, endedAt: null, message: '' }); }; const handleRaceProgress = useCallback((payload) => { if (!raceDraft.roomId) return; const next = { ...payload, roomId: raceDraft.roomId, userId: currentUserId, username: currentUsername, type: 'progress', }; sendRaceBroadcast(next); updateRaceParticipant?.(myParticipant?.id, { ...myParticipant, ...payload, connected: true, updatedAt: new Date().toISOString() }); setRaceDraft((prev) => ({ ...prev, ...payload, message: raceDraft.status === 'countdown' ? 'Waiting for race start...' : prev.message })); }, [raceDraft.roomId, raceDraft.status, currentUserId, currentUsername, sendRaceBroadcast, updateRaceParticipant, myParticipant]); const handleRaceComplete = useCallback(async (result) => { setLastResult(result); setView('results'); const record = { userId: currentUserId || 'anonymous', username: currentUsername, wpm: Number(result?.wpm || 0), accuracy: Number(result?.accuracy || 0), errors: Number(result?.errors || 0), charactersTyped: Number(result?.charactersTyped || 0), timeSeconds: Number(result?.timeSeconds || 0), rating: result?.rating || '', createdAt: new Date().toISOString(), }; try { await addLeaderboardRecord(record); } catch (error) { console.error('Failed to save leaderboard record:', error); } if (raceDraft.roomId) { sendRaceBroadcast({ type: 'finished', roomId: raceDraft.roomId, userId: currentUserId, username: currentUsername, message: 'Race finished.' }); setRaceDraft((prev) => ({ ...prev, finished: true, endedAt: new Date().toISOString(), message: 'Race finished.' })); } }, [addLeaderboardRecord, currentUserId, currentUsername, raceDraft.roomId, sendRaceBroadcast]); const handleTestComplete = async (result) => { if (raceDraft.roomId) { await handleRaceComplete(result); return; } setLastResult(result); setView('results'); const record = { userId: currentUserId || 'anonymous', username: currentUsername, wpm: Number(result?.wpm || 0), accuracy: Number(result?.accuracy || 0), errors: Number(result?.errors || 0), charactersTyped: Number(result?.charactersTyped || 0), timeSeconds: Number(result?.timeSeconds || 0), rating: result?.rating || '', createdAt: new Date().toISOString(), }; try { await addLeaderboardRecord(record); } catch (error) { console.error('Failed to save leaderboard record:', error); } }; const handleRestart = () => { setLastResult(null); setView('test'); setActiveTab('test'); }; const handleViewRecords = () => { setActiveTab('records'); setView('test'); }; const handleTabChange = (tab) => { setActiveTab(tab); setView('test'); }; const roomPlayers = useMemo(() => participants.filter((p) => p?.roomId === raceDraft.roomId), [participants, raceDraft.roomId]); return (

⌨️ TypePulse

Measure your speed. Track your progress.

{currentUserId ? (

Returning user ID: {currentUserId}

) : identityLoading ? (

Loading your identity...

) : null} {personalBest ? (

Personal best: {personalBest.wpm} WPM at {personalBest.accuracy}% accuracy

) : null}
{view === 'results' && lastResult ? ( <>
) : activeTab === 'test' ? ( <>

Head-to-head Race Room

{raceDraft.roomId ? : null}

Lobby

{lobbyOnlineUsers.length} online
{lobbyOnlineUsers.length ? lobbyOnlineUsers.map((user) => (
{user.username || user.name || 'Anonymous'} {user.roomId && user.roomId !== presenceRoomId ? In room : Available}
)) :

No other users are online yet.

}

Open rooms

{lobbyAvailableRooms.length ? lobbyAvailableRooms.map((room) => (
Room {room.code}
Host: {room.ownerName || 'Anonymous'}
{room.onlinePlayers}/{room.totalPlayers || 1} ready
)) :

No active rooms yet. Create one to start inviting players.

}
{!raceDraft.roomId ? (
setRoomCodeInput(e.target.value)} placeholder="Enter room code" />
) : (

Room: {raceDraft.roomCode || myRoom?.code}

Status: {raceDraft.status}

Opponent: {raceDraft.opponentName || activeOpponent?.username || (raceDraft.opponentConnected ? 'Connected' : 'Waiting...')}

Opponent progress: {Math.round(raceDraft.opponentProgress || 0)}%

{currentRoomOccupancy ?

Room occupancy: {currentRoomOccupancy.online}/{currentRoomOccupancy.total || 1} connected

: null}

{raceDraft.message || (raceDraft.status === 'waiting' ? 'Waiting for your opponent.' : '')}

{myRoom?.ownerId === currentUserId ? : null}
)}
) : ( <>

Share a personal best

{shareFeedback ? {shareFeedback} : null}
{shareableRecords.length ? ( <>
{selectedShareRecord ? (
{buildShareData(selectedShareRecord)?.text}
{buildShareData(selectedShareRecord)?.url}
) : null} ) : (

No personal bests yet. Complete a typing test to unlock sharing.

)}
)}
); } export { App }; ReactDOM.createRoot(document.getElementById("root")).render();