58
app/page.tsx
58
app/page.tsx
@@ -132,22 +132,22 @@ export default function Home() {
|
|||||||
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200">
|
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-white shadow-md" role="banner">
|
<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="max-w-7xl mx-auto px-2 sm:px-4 py-3 sm:py-4 flex items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2 sm:gap-4 min-w-0">
|
||||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
<h1 className="text-xl sm:text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
Dominoes
|
Dominoes
|
||||||
</h1>
|
</h1>
|
||||||
{roomId && (
|
{roomId && (
|
||||||
<div className="hidden sm:block bg-gradient-to-r from-blue-500 to-purple-500 text-white px-4 py-1 rounded-full text-sm font-mono">
|
<div className="hidden xs:block bg-gradient-to-r from-blue-500 to-purple-500 text-white px-2 sm:px-4 py-1 rounded-full text-xs sm:text-sm font-mono">
|
||||||
{roomId}
|
{roomId}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRules(!showRules)}
|
onClick={() => setShowRules(!showRules)}
|
||||||
className="text-gray-600 hover:text-gray-900 text-sm font-medium"
|
className="text-gray-600 hover:text-gray-900 text-xs sm:text-sm font-medium px-2 sm:px-3 py-1 sm:py-2"
|
||||||
aria-label="Toggle game rules"
|
aria-label="Toggle game rules"
|
||||||
aria-expanded={showRules}
|
aria-expanded={showRules}
|
||||||
>
|
>
|
||||||
@@ -155,10 +155,10 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={leaveRoom}
|
onClick={leaveRoom}
|
||||||
className="bg-red-500 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-red-600 transition-colors"
|
className="bg-red-500 text-white px-2 sm:px-4 py-1 sm:py-2 rounded-lg text-xs sm:text-sm font-semibold hover:bg-red-600 transition-colors"
|
||||||
aria-label="Leave current game"
|
aria-label="Leave current game"
|
||||||
>
|
>
|
||||||
Leave Game
|
Leave
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,22 +223,22 @@ export default function Home() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Main game area */}
|
{/* Main game area */}
|
||||||
<main className="max-w-7xl mx-auto px-4 py-6" role="main">
|
<main className="max-w-7xl mx-auto px-2 sm:px-4 py-4 sm:py-6" role="main">
|
||||||
<div className="grid lg:grid-cols-[1fr_300px] gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4 lg:gap-6">
|
||||||
{/* Left side - Game board and controls */}
|
{/* Left side - Game board and controls */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Game info */}
|
{/* Game info */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-4" role="status" aria-live="polite" aria-atomic="true">
|
<div className="bg-white rounded-lg shadow-md p-3 sm:p-4" role="status" aria-live="polite" aria-atomic="true">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="text-sm text-gray-600">Current Turn</div>
|
<div className="text-xs sm:text-sm text-gray-600">Current Turn</div>
|
||||||
<div className="text-xl font-bold text-gray-800">
|
<div className="text-lg sm:text-xl font-bold text-gray-800 truncate">
|
||||||
{gameState.players[gameState.currentPlayerIndex]?.name}
|
{gameState.players[gameState.currentPlayerIndex]?.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm text-gray-600">Boneyard</div>
|
<div className="text-xs sm:text-sm text-gray-600">Boneyard</div>
|
||||||
<div className="text-xl font-bold text-gray-800">
|
<div className="text-lg sm:text-xl font-bold text-gray-800">
|
||||||
{gameState.boneyard.length} tiles
|
{gameState.boneyard.length} tiles
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,19 +253,19 @@ export default function Home() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 20, opacity: 0 }}
|
initial={{ y: 20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
className="bg-white rounded-lg shadow-md p-4"
|
className="bg-white rounded-lg shadow-md p-3 sm:p-4"
|
||||||
>
|
>
|
||||||
{selectedTile && (
|
{selectedTile && (
|
||||||
<div className="mb-3 text-center text-sm text-gray-600">
|
<div className="mb-3 text-center text-xs sm:text-sm text-gray-600">
|
||||||
Selected: <span className="font-bold">{selectedTile.left}-{selectedTile.right}</span>
|
Selected: <span className="font-bold">{selectedTile.left}-{selectedTile.right}</span>
|
||||||
{gameState.boardEnds.length > 0 && (
|
{gameState.boardEnds.length > 0 && (
|
||||||
<div className="mt-1">
|
<div className="mt-1 text-xs">
|
||||||
Board ends: <span className="font-bold">{gameState.boardEnds[0]?.value}</span> (left) | <span className="font-bold">{gameState.boardEnds[1]?.value}</span> (right)
|
Board ends: <span className="font-bold">{gameState.boardEnds[0]?.value}</span> (left) | <span className="font-bold">{gameState.boardEnds[1]?.value}</span> (right)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3 flex-wrap" role="group" aria-label="Game controls">
|
<div className="flex items-center gap-2 sm:gap-3 flex-wrap" role="group" aria-label="Game controls">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePlaceTile('left')}
|
onClick={() => handlePlaceTile('left')}
|
||||||
disabled={!selectedTile}
|
disabled={!selectedTile}
|
||||||
@@ -317,27 +317,27 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Other players */}
|
{/* Right side - Other players */}
|
||||||
<aside className="space-y-4" role="complementary" aria-label="Other players">
|
<aside className="space-y-4 order-first lg:order-last" role="complementary" aria-label="Other players">
|
||||||
<h3 className="text-lg font-semibold text-gray-700">Players</h3>
|
<h3 className="text-base sm:text-lg font-semibold text-gray-700">Players</h3>
|
||||||
{gameState.players
|
{gameState.players
|
||||||
.filter(p => p.id !== currentPlayerId)
|
.filter(p => p.id !== currentPlayerId)
|
||||||
.map(player => (
|
.map(player => (
|
||||||
<div key={player.id} className="bg-white rounded-lg shadow-md p-4">
|
<div key={player.id} className="bg-white rounded-lg shadow-md p-3 sm:p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-white ${
|
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center font-bold text-white text-sm sm:text-base ${
|
||||||
player.isAI ? 'bg-purple-500' : 'bg-blue-500'
|
player.isAI ? 'bg-purple-500' : 'bg-blue-500'
|
||||||
}`}>
|
}`}>
|
||||||
{player.name.charAt(0).toUpperCase()}
|
{player.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-semibold text-gray-800">{player.name}</div>
|
<div className="font-semibold text-gray-800 text-sm sm:text-base truncate">{player.name}</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
{player.tiles.length} tiles
|
{player.tiles.length} tiles
|
||||||
{player.isAI && ' (AI)'}
|
{player.isAI && ' (AI)'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{gameState.players[gameState.currentPlayerIndex]?.id === player.id && (
|
{gameState.players[gameState.currentPlayerIndex]?.id === player.id && (
|
||||||
<div className="bg-green-500 text-white px-2 py-1 rounded text-xs font-semibold">
|
<div className="bg-green-500 text-white px-2 py-1 rounded text-xs font-semibold whitespace-nowrap">
|
||||||
Turn
|
Turn
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,9 +12,28 @@ interface GameBoardProps {
|
|||||||
|
|
||||||
export function GameBoard({ placedTiles, width = 1200, height = 700, className = '' }: GameBoardProps) {
|
export function GameBoard({ placedTiles, width = 1200, height = 700, className = '' }: GameBoardProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [offset, setOffset] = useState<Position>({ x: 0, y: 0 });
|
const [offset, setOffset] = useState<Position>({ x: 0, y: 0 });
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dragStart, setDragStart] = useState<Position>({ x: 0, y: 0 });
|
const [dragStart, setDragStart] = useState<Position>({ x: 0, y: 0 });
|
||||||
|
const [canvasSize, setCanvasSize] = useState({ width, height });
|
||||||
|
|
||||||
|
// Handle responsive canvas sizing
|
||||||
|
useEffect(() => {
|
||||||
|
const updateSize = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const containerWidth = containerRef.current.clientWidth;
|
||||||
|
const isMobile = window.innerWidth < 640;
|
||||||
|
const newWidth = isMobile ? containerWidth - 32 : Math.min(width, containerWidth);
|
||||||
|
const newHeight = isMobile ? 400 : height;
|
||||||
|
setCanvasSize({ width: newWidth, height: newHeight });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
}, [width, height]);
|
||||||
|
|
||||||
// Auto-center the board on first tile
|
// Auto-center the board on first tile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -163,21 +182,23 @@ export function GameBoard({ placedTiles, width = 1200, height = 700, className =
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} role="region" aria-label="Game board">
|
<div ref={containerRef} className={className} role="region" aria-label="Game board">
|
||||||
<canvas
|
<div className="w-full overflow-x-auto">
|
||||||
ref={canvasRef}
|
<canvas
|
||||||
width={width}
|
ref={canvasRef}
|
||||||
height={height}
|
width={canvasSize.width}
|
||||||
className={`border border-gray-300 rounded-lg ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
height={canvasSize.height}
|
||||||
|
className={`border border-gray-300 rounded-lg mx-auto ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseUp}
|
onMouseLeave={handleMouseUp}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`Domino board with ${placedTiles.length} tiles placed`}
|
aria-label={`Domino board with ${placedTiles.length} tiles placed`}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{placedTiles.length > 0 && (
|
{placedTiles.length > 0 && (
|
||||||
<div className="mt-2 text-center text-sm text-gray-600">
|
<div className="mt-2 text-center text-xs sm:text-sm text-gray-600">
|
||||||
Drag to pan • {placedTiles.length} tiles placed
|
Drag to pan • {placedTiles.length} tiles placed
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user