📄 +page.svelte
<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import { io, type Socket } from 'socket.io-client';
	import GameScreen from '$lib/components/GameScreen.svelte';
	import ResultsScreen from '$lib/components/ResultsScreen.svelte';
	import type { Player } from '$lib/types';

	let socket: Socket | null = null;
	let roomCodeInput = $state('');

	// State
	let gameState = $state<'join' | 'waiting' | 'playing' | 'results'>('join');
	let currentColor = $state('');
	let currentPlayerId = $state<number | null>(null);
	let scores = $state<Player[]>([]);
	let errorMessage = $state('');

	onMount(() => {
		// Connect to server
		// In production, Socket.IO runs on same server as SvelteKit
		// In development, it runs on separate port (3001)
		const socketUrl = import.meta.env.DEV ? 'http://localhost:3001' : window.location.origin;
		socket = io(socketUrl);

		socket.on('connect', () => {
			console.log('Connected to server');
		});

		socket.on('game-command', (command: any) => {
			handleCommand(command);
		});

		socket.on('room-closed', () => {
			console.log('Room closed by master');
			gameState = 'join';
			currentColor = '';
			releaseWakeLock();
			alert('Room closed by master device');
		});
	});

	onDestroy(() => {
		socket?.disconnect();
		releaseWakeLock();
	});

	function joinRoom() {
		if (!roomCodeInput.trim()) {
			errorMessage = 'Please enter a room code';
			return;
		}

		const code = roomCodeInput.toUpperCase().trim();

		socket?.emit('join-room', code, (response: any) => {
			if (response.success) {
				console.log('Joined room successfully');
				errorMessage = '';
				gameState = 'waiting';
			} else {
				errorMessage = response.error || 'Failed to join room';
			}
		});
	}

	let wakeLock: WakeLockSentinel | null = null;

	async function requestWakeLock() {
		if ('wakeLock' in navigator) {
			try {
				wakeLock = await navigator.wakeLock.request('screen');
				wakeLock.addEventListener('release', () => {
					console.log('Wake lock released');
				});
			} catch (err) {
				console.error('Wake lock error:', err);
			}
		}
	}

	function releaseWakeLock() {
		if (wakeLock) {
			wakeLock.release();
			wakeLock = null;
		}
	}

	async function handleCommand(command: any) {
		console.log('Received command:', command);

		switch (command.type) {
			case 'start-game':
				gameState = 'playing';
				currentColor = '';
				await requestWakeLock();
				break;

			case 'show-color':
				currentColor = command.color;
				currentPlayerId = command.playerId;
				break;

			case 'hide-color':
				currentColor = '';
				currentPlayerId = null;
				break;

			case 'show-results':
				gameState = 'results';
				scores = command.scores;
				releaseWakeLock();
				break;
		}
	}

	function handleTap() {
		if (currentPlayerId !== null && currentColor) {
			// Send tap event to master
			socket?.emit('display-action', {
				type: 'tap',
				playerId: currentPlayerId
			});

			// Hide color immediately
			currentColor = '';
			currentPlayerId = null;
		}
	}
</script>

{#if gameState === 'join'}
	<div class="join-container">
		<h1>Join Game</h1>

		<div class="join-form">
			<div class="form-group">
				<label for="roomCode">Enter Room Code:</label>
				<input
					type="text"
					id="roomCode"
					bind:value={roomCodeInput}
					placeholder="ABC123"
					maxlength="6"
					onkeydown={(e) => e.key === 'Enter' && joinRoom()}
				/>
			</div>

			{#if errorMessage}
				<div class="error">{errorMessage}</div>
			{/if}

			<button class="join-button" onclick={joinRoom}>Join Room</button>
		</div>
	</div>
{:else if gameState === 'waiting'}
	<div class="waiting-container">
		<h1>Waiting for Game to Start</h1>
		<p>Connected to room. Waiting for master to start the game...</p>
		<div class="spinner"></div>
	</div>
{:else if gameState === 'playing'}
	<GameScreen color={currentColor} isMaster={false} onTap={handleTap} />
{:else if gameState === 'results'}
	<ResultsScreen {scores} isMaster={false} />
{/if}

<style>
	:global(body) {
		margin: 0;
		padding: 0;
		font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
	}

	.join-container {
		max-width: 400px;
		margin: 0 auto;
		padding: 2rem;
		text-align: center;
		min-height: 100vh;
		display: flex;
		flex-direction: column;
		justify-content: center;
	}

	h1 {
		color: #333;
		margin-bottom: 2rem;
	}

	.join-form {
		background: #fff;
		border: 2px solid #ddd;
		border-radius: 8px;
		padding: 2rem;
	}

	.form-group {
		margin-bottom: 1.5rem;
		text-align: left;
	}

	label {
		display: block;
		margin-bottom: 0.5rem;
		font-weight: bold;
		color: #555;
	}

	input[type='text'] {
		width: 100%;
		padding: 1rem;
		font-size: 1.5rem;
		text-align: center;
		text-transform: uppercase;
		border: 2px solid #ddd;
		border-radius: 4px;
		box-sizing: border-box;
		letter-spacing: 0.25rem;
	}

	.error {
		color: #dc3545;
		margin-bottom: 1rem;
		padding: 0.75rem;
		background: #f8d7da;
		border: 1px solid #f5c6cb;
		border-radius: 4px;
	}

	.join-button {
		width: 100%;
		padding: 1rem;
		font-size: 1.25rem;
		font-weight: bold;
		background: #007bff;
		color: white;
		border: none;
		border-radius: 8px;
		cursor: pointer;
		transition: background 0.2s;
	}

	.join-button:hover {
		background: #0056b3;
	}

	.join-button:active {
		background: #004085;
	}

	.waiting-container {
		text-align: center;
		padding: 4rem 2rem;
		min-height: 100vh;
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
	}

	.waiting-container h1 {
		margin-bottom: 1rem;
	}

	.waiting-container p {
		color: #666;
		font-size: 1.1rem;
		margin-bottom: 2rem;
	}

	.spinner {
		border: 4px solid #f3f3f3;
		border-top: 4px solid #007bff;
		border-radius: 50%;
		width: 50px;
		height: 50px;
		animation: spin 1s linear infinite;
	}

	@keyframes spin {
		0% { transform: rotate(0deg); }
		100% { transform: rotate(360deg); }
	}
</style>