32
app/page.tsx
32
app/page.tsx
@@ -131,7 +131,7 @@ export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-md">
|
||||
<header className="bg-white shadow-md" role="banner">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
@@ -148,12 +148,15 @@ export default function Home() {
|
||||
<button
|
||||
onClick={() => setShowRules(!showRules)}
|
||||
className="text-gray-600 hover:text-gray-900 text-sm font-medium"
|
||||
aria-label="Toggle game rules"
|
||||
aria-expanded={showRules}
|
||||
>
|
||||
Rules
|
||||
</button>
|
||||
<button
|
||||
onClick={leaveRoom}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-red-600 transition-colors"
|
||||
aria-label="Leave current game"
|
||||
>
|
||||
Leave Game
|
||||
</button>
|
||||
@@ -169,6 +172,9 @@ export default function Home() {
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -100, opacity: 0 }}
|
||||
className="fixed top-20 left-1/2 transform -translate-x-1/2 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{error}
|
||||
</motion.div>
|
||||
@@ -184,6 +190,9 @@ export default function Home() {
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
onClick={() => setShowRules(false)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="rules-title"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9 }}
|
||||
@@ -192,7 +201,7 @@ export default function Home() {
|
||||
className="bg-white rounded-lg p-6 max-w-md"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-4">How to Play</h3>
|
||||
<h3 id="rules-title" className="text-xl font-bold mb-4">How to Play</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li>• Click on a tile to select it</li>
|
||||
<li>• Click "Place Left" or "Place Right" to place it</li>
|
||||
@@ -204,6 +213,7 @@ export default function Home() {
|
||||
<button
|
||||
onClick={() => setShowRules(false)}
|
||||
className="mt-4 w-full bg-blue-500 text-white py-2 rounded-lg"
|
||||
aria-label="Close rules dialog"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@@ -213,12 +223,12 @@ export default function Home() {
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Main game area */}
|
||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||
<main className="max-w-7xl mx-auto px-4 py-6" role="main">
|
||||
<div className="grid lg:grid-cols-[1fr_300px] gap-6">
|
||||
{/* Left side - Game board and controls */}
|
||||
<div className="space-y-4">
|
||||
{/* Game info */}
|
||||
<div className="bg-white rounded-lg shadow-md p-4">
|
||||
<div className="bg-white rounded-lg shadow-md p-4" role="status" aria-live="polite" aria-atomic="true">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600">Current Turn</div>
|
||||
@@ -255,11 +265,12 @@ export default function Home() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-3 flex-wrap" role="group" aria-label="Game controls">
|
||||
<button
|
||||
onClick={() => handlePlaceTile('left')}
|
||||
disabled={!selectedTile}
|
||||
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-lg transition-shadow"
|
||||
aria-label={selectedTile ? `Place tile ${selectedTile.left}-${selectedTile.right} on the left side` : 'Place tile on left side (select a tile first)'}
|
||||
>
|
||||
Place Left
|
||||
</button>
|
||||
@@ -267,6 +278,7 @@ export default function Home() {
|
||||
onClick={() => handlePlaceTile('right')}
|
||||
disabled={!selectedTile}
|
||||
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-lg transition-shadow"
|
||||
aria-label={selectedTile ? `Place tile ${selectedTile.left}-${selectedTile.right} on the right side` : 'Place tile on right side (select a tile first)'}
|
||||
>
|
||||
Place Right
|
||||
</button>
|
||||
@@ -274,6 +286,7 @@ export default function Home() {
|
||||
<button
|
||||
onClick={drawTile}
|
||||
className="flex-1 bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-shadow"
|
||||
aria-label={`Draw a tile from the boneyard (${gameState.boneyard.length} tiles remaining)`}
|
||||
>
|
||||
Draw Tile
|
||||
</button>
|
||||
@@ -282,6 +295,7 @@ export default function Home() {
|
||||
<button
|
||||
onClick={handlePass}
|
||||
className="flex-1 bg-gradient-to-r from-yellow-500 to-yellow-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-shadow"
|
||||
aria-label="Pass your turn (no valid moves available)"
|
||||
>
|
||||
Pass Turn
|
||||
</button>
|
||||
@@ -303,7 +317,7 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* Right side - Other players */}
|
||||
<div className="space-y-4">
|
||||
<aside className="space-y-4" role="complementary" aria-label="Other players">
|
||||
<h3 className="text-lg font-semibold text-gray-700">Players</h3>
|
||||
{gameState.players
|
||||
.filter(p => p.id !== currentPlayerId)
|
||||
@@ -338,10 +352,10 @@ export default function Home() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Game over modal */}
|
||||
{gameState.isGameOver && (
|
||||
|
||||
@@ -163,7 +163,7 @@ export function GameBoard({ placedTiles, width = 1200, height = 700, className =
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={className} role="region" aria-label="Game board">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
@@ -173,6 +173,8 @@ export function GameBoard({ placedTiles, width = 1200, height = 700, className =
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
role="img"
|
||||
aria-label={`Domino board with ${placedTiles.length} tiles placed`}
|
||||
/>
|
||||
{placedTiles.length > 0 && (
|
||||
<div className="mt-2 text-center text-sm text-gray-600">
|
||||
|
||||
@@ -17,6 +17,9 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="game-over-title"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, y: 50 }}
|
||||
@@ -32,7 +35,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
>
|
||||
{winner ? '🏆' : '🤝'}
|
||||
</motion.div>
|
||||
<h2 className="text-3xl font-bold text-gray-800 mb-2">
|
||||
<h2 id="game-over-title" className="text-3xl font-bold text-gray-800 mb-2">
|
||||
{winner ? 'Game Over!' : 'Game Blocked!'}
|
||||
</h2>
|
||||
{winner && (
|
||||
@@ -91,6 +94,7 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onPlayAgain}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow"
|
||||
aria-label="Start a new game"
|
||||
>
|
||||
Play Again
|
||||
</motion.button>
|
||||
@@ -100,8 +104,9 @@ export function GameOver({ winner, players, onPlayAgain, onLeave }: GameOverProp
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onLeave}
|
||||
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
|
||||
aria-label="Leave game and return to main menu"
|
||||
>
|
||||
Back to Menu
|
||||
Leave Game
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
|
||||
if (mode === 'menu') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
|
||||
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
@@ -48,23 +48,28 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
<p className="text-center text-gray-600 mb-8">Online Multiplayer Game</p>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<label htmlFor="player-name" className="sr-only">Your name</label>
|
||||
<input
|
||||
id="player-name"
|
||||
type="text"
|
||||
placeholder="Enter your name"
|
||||
value={playerName}
|
||||
onChange={(e) => setPlayerName(e.target.value)}
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none transition-colors"
|
||||
maxLength={20}
|
||||
aria-label="Enter your player name"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3" role="group" aria-label="Game mode selection">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleCreate}
|
||||
disabled={!playerName.trim()}
|
||||
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label="Create a new multiplayer room"
|
||||
>
|
||||
Create Room
|
||||
</motion.button>
|
||||
@@ -75,6 +80,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
onClick={() => setMode('join')}
|
||||
disabled={!playerName.trim()}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label="Join an existing multiplayer room"
|
||||
>
|
||||
Join Room
|
||||
</motion.button>
|
||||
@@ -85,6 +91,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
onClick={handleAI}
|
||||
disabled={!playerName.trim()}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label="Start a game against computer AI"
|
||||
>
|
||||
Play vs AI
|
||||
</motion.button>
|
||||
@@ -94,13 +101,13 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
<p>Built with Next.js, Canvas & Socket.IO</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'join') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
|
||||
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
@@ -109,13 +116,17 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
<h2 className="text-3xl font-bold text-center mb-6 text-gray-800">Join Room</h2>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<label htmlFor="room-id" className="sr-only">Room ID</label>
|
||||
<input
|
||||
id="room-id"
|
||||
type="text"
|
||||
placeholder="Enter Room ID"
|
||||
value={joinRoomId}
|
||||
onChange={(e) => setJoinRoomId(e.target.value.toUpperCase())}
|
||||
className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg focus:border-purple-500 focus:outline-none transition-colors uppercase"
|
||||
maxLength={6}
|
||||
aria-label="Enter the 6-character room ID"
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -126,6 +137,7 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
onClick={handleJoin}
|
||||
disabled={!joinRoomId.trim()}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-purple-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-label="Join the room with entered ID"
|
||||
>
|
||||
Join Game
|
||||
</motion.button>
|
||||
@@ -135,12 +147,13 @@ export function Lobby({ onCreateRoom, onJoinRoom, onStartAI, roomId }: LobbyProp
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setMode('menu')}
|
||||
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
|
||||
aria-label="Go back to main menu"
|
||||
>
|
||||
Back
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function PlayerHand({
|
||||
validTileIds,
|
||||
}: PlayerHandProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-4">
|
||||
<div className="bg-white rounded-lg shadow-lg p-4" role="region" aria-label="Your tiles">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-white ${
|
||||
@@ -63,6 +63,16 @@ export function PlayerHand({
|
||||
className={`relative ${
|
||||
isPlayable && isCurrentPlayer ? 'cursor-pointer' : 'cursor-not-allowed'
|
||||
}`}
|
||||
role="button"
|
||||
tabIndex={isPlayable && isCurrentPlayer ? 0 : -1}
|
||||
aria-label={`Tile ${tile.left}-${tile.right}${isSelected ? ' (selected)' : ''}${!isPlayable ? ' (cannot be played)' : ''}`}
|
||||
aria-pressed={isSelected}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && isPlayable && isCurrentPlayer) {
|
||||
e.preventDefault();
|
||||
onTileClick(tile.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DominoTileSVG
|
||||
tile={tile}
|
||||
@@ -96,6 +106,8 @@ function DominoTileSVG({ tile, isSelected, isPlayable }: DominoTileSVGProps) {
|
||||
className={`transition-all ${
|
||||
isSelected ? 'ring-4 ring-blue-500 rounded' : ''
|
||||
} ${!isPlayable ? 'opacity-50' : ''}`}
|
||||
role="img"
|
||||
aria-label={`Domino tile: ${tile.left} and ${tile.right}`}
|
||||
>
|
||||
{/* Background */}
|
||||
<rect
|
||||
|
||||
@@ -18,7 +18,7 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
|
||||
const canStart = players.length >= 2 && players.every(p => p.isReady);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4">
|
||||
<main className="min-h-screen bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 flex items-center justify-center p-4" role="main">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
@@ -102,11 +102,12 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onReady}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-lg font-semibold shadow-lg hover:shadow-xl transition-shadow"
|
||||
aria-label="Mark yourself as ready to start the game"
|
||||
>
|
||||
Ready to Play
|
||||
</motion.button>
|
||||
) : (
|
||||
<div className="w-full bg-green-100 border-2 border-green-500 text-green-700 py-3 rounded-lg font-semibold text-center">
|
||||
<div className="w-full bg-green-100 border-2 border-green-500 text-green-700 py-3 rounded-lg font-semibold text-center" role="status" aria-live="polite">
|
||||
{canStart ? 'Starting game...' : players.length < 2 ? 'Waiting for at least 1 more player...' : 'Waiting for other players...'}
|
||||
</div>
|
||||
)}
|
||||
@@ -116,12 +117,13 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onLeave}
|
||||
className="w-full bg-gray-300 text-gray-700 py-3 rounded-lg font-semibold hover:bg-gray-400 transition-colors"
|
||||
aria-label="Leave the waiting room and return to menu"
|
||||
>
|
||||
Leave Room
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg" role="region" aria-label="Game rules">
|
||||
<h4 className="font-semibold text-blue-900 mb-2">Game Rules</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• 2-4 players can play (minimum 2 required)</li>
|
||||
@@ -132,6 +134,6 @@ export function WaitingRoom({ roomId, players, currentPlayerId, onReady, onLeave
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Referencia en una nueva incidencia
Block a user