Tantawi65 commited on
Commit ·
c0f8abf
1
Parent(s): e07d173
Mobile fullscreen support and responsive layout improvements
Browse files- client/index.html +6 -1
- client/public/manifest.json +22 -0
- client/src/components/screens/GameBoard.tsx +68 -32
- client/src/components/screens/Lobby.tsx +12 -12
- client/src/components/screens/MainMenu.tsx +17 -17
- client/src/components/screens/Room.tsx +7 -7
- client/src/index.css +47 -13
client/index.html
CHANGED
|
@@ -3,13 +3,18 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/png" href="/Assests/ui_logo.png" />
|
|
|
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
|
|
| 7 |
<meta name="apple-mobile-web-app-capable" content="yes" />
|
| 8 |
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
| 9 |
<meta name="mobile-web-app-capable" content="yes" />
|
| 10 |
<meta name="screen-orientation" content="landscape" />
|
| 11 |
<meta name="theme-color" content="#1a1a2e" />
|
| 12 |
-
<
|
|
|
|
|
|
|
|
|
|
| 13 |
<style>
|
| 14 |
/* Prevent flash of unstyled content */
|
| 15 |
body {
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/png" href="/Assests/ui_logo.png" />
|
| 6 |
+
<link rel="manifest" href="/manifest.json" />
|
| 7 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
| 8 |
+
<!-- PWA / Fullscreen support -->
|
| 9 |
<meta name="apple-mobile-web-app-capable" content="yes" />
|
| 10 |
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
| 11 |
<meta name="mobile-web-app-capable" content="yes" />
|
| 12 |
<meta name="screen-orientation" content="landscape" />
|
| 13 |
<meta name="theme-color" content="#1a1a2e" />
|
| 14 |
+
<meta name="msapplication-navbutton-color" content="#1a1a2e" />
|
| 15 |
+
<meta name="apple-mobile-web-app-title" content="Khofo" />
|
| 16 |
+
<link rel="apple-touch-icon" href="/Assests/ui_logo.png" />
|
| 17 |
+
<title>Khofo - هتتحنط هنا</title>
|
| 18 |
<style>
|
| 19 |
/* Prevent flash of unstyled content */
|
| 20 |
body {
|
client/public/manifest.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Khofo Card Game",
|
| 3 |
+
"short_name": "Khofo",
|
| 4 |
+
"description": "Egyptian Mummy Card Game",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "fullscreen",
|
| 7 |
+
"orientation": "landscape",
|
| 8 |
+
"background_color": "#1a1a2e",
|
| 9 |
+
"theme_color": "#1a1a2e",
|
| 10 |
+
"icons": [
|
| 11 |
+
{
|
| 12 |
+
"src": "/Assests/ui_logo.png",
|
| 13 |
+
"sizes": "192x192",
|
| 14 |
+
"type": "image/png"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"src": "/Assests/ui_logo.png",
|
| 18 |
+
"sizes": "512x512",
|
| 19 |
+
"type": "image/png"
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
}
|
client/src/components/screens/GameBoard.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { useState, useEffect } from 'react';
|
| 2 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import { useGameStore } from '../../store/gameStore';
|
| 4 |
import { Card, CardBack } from '../game/Card';
|
|
@@ -6,6 +6,18 @@ import { emitDrawCard, emitPlayCard } from '../../socket/socket';
|
|
| 6 |
import type { CardInstance, Player } from '@shared/types';
|
| 7 |
import { CARD_DATABASE } from '@shared/types';
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
export function GameBoard() {
|
| 10 |
const gameState = useGameStore((state) => state.gameState);
|
| 11 |
const myHand = useGameStore((state) => state.myHand);
|
|
@@ -19,6 +31,20 @@ export function GameBoard() {
|
|
| 19 |
const reactionWindowActive = useGameStore((state) => state.reactionWindowActive);
|
| 20 |
|
| 21 |
const [pendingCard, setPendingCard] = useState<CardInstance | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
// Clear pendingCard when relevant modals close (not target-select or card-type-select)
|
| 24 |
useEffect(() => {
|
|
@@ -119,15 +145,25 @@ export function GameBoard() {
|
|
| 119 |
|
| 120 |
return (
|
| 121 |
<div
|
| 122 |
-
className="game-board flex-1 flex flex-col"
|
| 123 |
style={{
|
| 124 |
-
backgroundImage: 'url(/menu_background.png)',
|
| 125 |
backgroundSize: 'cover',
|
| 126 |
backgroundPosition: 'center',
|
| 127 |
}}
|
| 128 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
{/* Top area - Other players */}
|
| 130 |
-
<div className="flex justify-center gap-4 p-2">
|
| 131 |
{otherPlayers.map((player) => (
|
| 132 |
<PlayerDisplay
|
| 133 |
key={player.id}
|
|
@@ -138,16 +174,16 @@ export function GameBoard() {
|
|
| 138 |
</div>
|
| 139 |
|
| 140 |
{/* Middle area - Deck and discard */}
|
| 141 |
-
<div className="flex-1 flex items-center justify-center gap-8">
|
| 142 |
{/* Deck */}
|
| 143 |
<motion.div
|
| 144 |
-
className="deck-pile"
|
| 145 |
data-count={gameState.deckCount}
|
| 146 |
whileHover={isMyTurn ? { scale: 1.05 } : {}}
|
| 147 |
onClick={handleDrawCard}
|
| 148 |
>
|
| 149 |
<CardBack size="large" />
|
| 150 |
-
<p className="text-center text-papyrus mt-
|
| 151 |
</motion.div>
|
| 152 |
|
| 153 |
{/* Turn info */}
|
|
@@ -156,13 +192,13 @@ export function GameBoard() {
|
|
| 156 |
key={currentPlayer?.id}
|
| 157 |
initial={{ opacity: 0, scale: 0.8 }}
|
| 158 |
animate={{ opacity: 1, scale: 1 }}
|
| 159 |
-
className="bg-black/60 rounded-lg px-6 py-3"
|
| 160 |
>
|
| 161 |
-
<p className="text-papyrus mb-1">
|
| 162 |
{currentPlayer?.id === playerId ? "Your Turn!" : `${currentPlayer?.name}'s Turn`}
|
| 163 |
</p>
|
| 164 |
{turnsRemaining > 1 && (
|
| 165 |
-
<p className="text-egyptian-gold text-
|
| 166 |
{turnsRemaining} turns remaining
|
| 167 |
</p>
|
| 168 |
)}
|
|
@@ -170,7 +206,7 @@ export function GameBoard() {
|
|
| 170 |
</div>
|
| 171 |
|
| 172 |
{/* Discard pile */}
|
| 173 |
-
<div className="relative">
|
| 174 |
{gameState.discardPile.length > 0 ? (
|
| 175 |
<Card
|
| 176 |
cardId={gameState.discardPile[gameState.discardPile.length - 1]}
|
|
@@ -178,25 +214,25 @@ export function GameBoard() {
|
|
| 178 |
disabled
|
| 179 |
/>
|
| 180 |
) : (
|
| 181 |
-
<div className="w-32 h-48 border-2 border-dashed border-papyrus/30 rounded-lg flex items-center justify-center">
|
| 182 |
-
<p className="text-papyrus/40 text-center text-
|
| 183 |
</div>
|
| 184 |
)}
|
| 185 |
-
<p className="text-center text-papyrus mt-
|
| 186 |
</div>
|
| 187 |
</div>
|
| 188 |
|
| 189 |
{/* Bottom area - My hand */}
|
| 190 |
-
<div className="bg-black/50 backdrop-blur-sm p-4">
|
| 191 |
{/* My player info */}
|
| 192 |
-
<div className="flex items-center justify-between mb-2">
|
| 193 |
-
<div className="flex items-center gap-2">
|
| 194 |
-
<div className={`player-avatar ${isMyTurn ? 'current-turn' : ''}`}>
|
| 195 |
{myPlayer?.name.charAt(0).toUpperCase()}
|
| 196 |
</div>
|
| 197 |
-
<span className="text-papyrus">{myPlayer?.name}
|
| 198 |
</div>
|
| 199 |
-
<span className="text-egyptian-gold">{myHand.length} cards</span>
|
| 200 |
</div>
|
| 201 |
|
| 202 |
{/* Hand */}
|
|
@@ -229,11 +265,11 @@ export function GameBoard() {
|
|
| 229 |
{/* Draw button for mobile */}
|
| 230 |
{isMyTurn && (
|
| 231 |
<motion.button
|
| 232 |
-
className="btn btn-primary w-full mt-2"
|
| 233 |
onClick={handleDrawCard}
|
| 234 |
whileTap={{ scale: 0.95 }}
|
| 235 |
>
|
| 236 |
-
Draw Card
|
| 237 |
</motion.button>
|
| 238 |
)}
|
| 239 |
</div>
|
|
@@ -249,32 +285,32 @@ interface PlayerDisplayProps {
|
|
| 249 |
function PlayerDisplay({ player, isCurrentTurn }: PlayerDisplayProps) {
|
| 250 |
return (
|
| 251 |
<motion.div
|
| 252 |
-
className={`bg-black/50 rounded-lg p-2 ${isCurrentTurn ? 'ring-2 ring-green-500' : ''}`}
|
| 253 |
animate={isCurrentTurn ? { scale: [1, 1.02, 1] } : {}}
|
| 254 |
transition={{ repeat: isCurrentTurn ? Infinity : 0, duration: 1.5 }}
|
| 255 |
>
|
| 256 |
-
<div className="flex items-center gap-2 mb-
|
| 257 |
-
<div className={`player-avatar w-8 h-8 text-sm ${!player.isAlive ? 'eliminated' : ''} ${isCurrentTurn ? 'current-turn' : ''}`}>
|
| 258 |
{player.name.charAt(0).toUpperCase()}
|
| 259 |
</div>
|
| 260 |
<div>
|
| 261 |
-
<p className={`text-sm ${player.isAlive ? 'text-papyrus' : 'text-papyrus/50 line-through'}`}>
|
| 262 |
{player.name}
|
| 263 |
</p>
|
| 264 |
-
{!player.isAlive && <p className="text-mummy-red text-xs">☠️
|
| 265 |
</div>
|
| 266 |
</div>
|
| 267 |
|
| 268 |
{player.isAlive && (
|
| 269 |
-
<div className="flex gap-
|
| 270 |
-
{Array.from({ length: Math.min(player.cardCount,
|
| 271 |
<div
|
| 272 |
key={i}
|
| 273 |
-
className="w-4 h-6 bg-gradient-to-br from-egyptian-gold to-sand rounded shadow-sm"
|
| 274 |
/>
|
| 275 |
))}
|
| 276 |
-
{player.cardCount >
|
| 277 |
-
<span className="text-egyptian-gold text-xs">+{player.cardCount -
|
| 278 |
)}
|
| 279 |
</div>
|
| 280 |
)}
|
|
|
|
| 1 |
+
import { useState, useEffect, useCallback } from 'react';
|
| 2 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import { useGameStore } from '../../store/gameStore';
|
| 4 |
import { Card, CardBack } from '../game/Card';
|
|
|
|
| 6 |
import type { CardInstance, Player } from '@shared/types';
|
| 7 |
import { CARD_DATABASE } from '@shared/types';
|
| 8 |
|
| 9 |
+
// Fullscreen helper
|
| 10 |
+
const requestFullscreen = () => {
|
| 11 |
+
const elem = document.documentElement;
|
| 12 |
+
if (elem.requestFullscreen) {
|
| 13 |
+
elem.requestFullscreen();
|
| 14 |
+
} else if ((elem as any).webkitRequestFullscreen) {
|
| 15 |
+
(elem as any).webkitRequestFullscreen();
|
| 16 |
+
} else if ((elem as any).msRequestFullscreen) {
|
| 17 |
+
(elem as any).msRequestFullscreen();
|
| 18 |
+
}
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
export function GameBoard() {
|
| 22 |
const gameState = useGameStore((state) => state.gameState);
|
| 23 |
const myHand = useGameStore((state) => state.myHand);
|
|
|
|
| 31 |
const reactionWindowActive = useGameStore((state) => state.reactionWindowActive);
|
| 32 |
|
| 33 |
const [pendingCard, setPendingCard] = useState<CardInstance | null>(null);
|
| 34 |
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
| 35 |
+
|
| 36 |
+
// Track fullscreen state
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
const handleFullscreenChange = () => {
|
| 39 |
+
setIsFullscreen(!!document.fullscreenElement);
|
| 40 |
+
};
|
| 41 |
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
| 42 |
+
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
| 43 |
+
return () => {
|
| 44 |
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
| 45 |
+
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
| 46 |
+
};
|
| 47 |
+
}, []);
|
| 48 |
|
| 49 |
// Clear pendingCard when relevant modals close (not target-select or card-type-select)
|
| 50 |
useEffect(() => {
|
|
|
|
| 145 |
|
| 146 |
return (
|
| 147 |
<div
|
| 148 |
+
className="game-board flex-1 flex flex-col h-full"
|
| 149 |
style={{
|
| 150 |
+
backgroundImage: 'url(/Assests/menu_background.png)',
|
| 151 |
backgroundSize: 'cover',
|
| 152 |
backgroundPosition: 'center',
|
| 153 |
}}
|
| 154 |
>
|
| 155 |
+
{/* Fullscreen button - mobile only */}
|
| 156 |
+
{!isFullscreen && (
|
| 157 |
+
<button
|
| 158 |
+
onClick={requestFullscreen}
|
| 159 |
+
className="fixed top-2 right-2 z-50 bg-black/60 text-papyrus px-2 py-1 rounded text-xs md:hidden"
|
| 160 |
+
>
|
| 161 |
+
⛶ Fullscreen
|
| 162 |
+
</button>
|
| 163 |
+
)}
|
| 164 |
+
|
| 165 |
{/* Top area - Other players */}
|
| 166 |
+
<div className="flex justify-center gap-2 md:gap-4 p-1 md:p-2 flex-shrink-0">
|
| 167 |
{otherPlayers.map((player) => (
|
| 168 |
<PlayerDisplay
|
| 169 |
key={player.id}
|
|
|
|
| 174 |
</div>
|
| 175 |
|
| 176 |
{/* Middle area - Deck and discard */}
|
| 177 |
+
<div className="flex-1 flex items-center justify-center gap-4 md:gap-8 min-h-0">
|
| 178 |
{/* Deck */}
|
| 179 |
<motion.div
|
| 180 |
+
className="deck-pile scale-75 md:scale-100"
|
| 181 |
data-count={gameState.deckCount}
|
| 182 |
whileHover={isMyTurn ? { scale: 1.05 } : {}}
|
| 183 |
onClick={handleDrawCard}
|
| 184 |
>
|
| 185 |
<CardBack size="large" />
|
| 186 |
+
<p className="text-center text-papyrus text-xs md:text-base mt-1">Deck</p>
|
| 187 |
</motion.div>
|
| 188 |
|
| 189 |
{/* Turn info */}
|
|
|
|
| 192 |
key={currentPlayer?.id}
|
| 193 |
initial={{ opacity: 0, scale: 0.8 }}
|
| 194 |
animate={{ opacity: 1, scale: 1 }}
|
| 195 |
+
className="bg-black/60 rounded-lg px-3 py-2 md:px-6 md:py-3"
|
| 196 |
>
|
| 197 |
+
<p className="text-papyrus text-xs md:text-base mb-1">
|
| 198 |
{currentPlayer?.id === playerId ? "Your Turn!" : `${currentPlayer?.name}'s Turn`}
|
| 199 |
</p>
|
| 200 |
{turnsRemaining > 1 && (
|
| 201 |
+
<p className="text-egyptian-gold text-xs">
|
| 202 |
{turnsRemaining} turns remaining
|
| 203 |
</p>
|
| 204 |
)}
|
|
|
|
| 206 |
</div>
|
| 207 |
|
| 208 |
{/* Discard pile */}
|
| 209 |
+
<div className="relative scale-75 md:scale-100">
|
| 210 |
{gameState.discardPile.length > 0 ? (
|
| 211 |
<Card
|
| 212 |
cardId={gameState.discardPile[gameState.discardPile.length - 1]}
|
|
|
|
| 214 |
disabled
|
| 215 |
/>
|
| 216 |
) : (
|
| 217 |
+
<div className="w-24 h-36 md:w-32 md:h-48 border-2 border-dashed border-papyrus/30 rounded-lg flex items-center justify-center">
|
| 218 |
+
<p className="text-papyrus/40 text-center text-xs">Discard</p>
|
| 219 |
</div>
|
| 220 |
)}
|
| 221 |
+
<p className="text-center text-papyrus text-xs md:text-base mt-1">Discard ({gameState.discardPile.length})</p>
|
| 222 |
</div>
|
| 223 |
</div>
|
| 224 |
|
| 225 |
{/* Bottom area - My hand */}
|
| 226 |
+
<div className="bg-black/50 backdrop-blur-sm p-2 md:p-4 flex-shrink-0">
|
| 227 |
{/* My player info */}
|
| 228 |
+
<div className="flex items-center justify-between mb-1 md:mb-2">
|
| 229 |
+
<div className="flex items-center gap-1 md:gap-2">
|
| 230 |
+
<div className={`player-avatar w-6 h-6 md:w-12 md:h-12 text-xs md:text-lg ${isMyTurn ? 'current-turn' : ''}`}>
|
| 231 |
{myPlayer?.name.charAt(0).toUpperCase()}
|
| 232 |
</div>
|
| 233 |
+
<span className="text-papyrus text-xs md:text-base">{myPlayer?.name}</span>
|
| 234 |
</div>
|
| 235 |
+
<span className="text-egyptian-gold text-xs md:text-base">{myHand.length} cards</span>
|
| 236 |
</div>
|
| 237 |
|
| 238 |
{/* Hand */}
|
|
|
|
| 265 |
{/* Draw button for mobile */}
|
| 266 |
{isMyTurn && (
|
| 267 |
<motion.button
|
| 268 |
+
className="btn btn-primary w-full mt-1 md:mt-2 py-2 text-sm md:text-base"
|
| 269 |
onClick={handleDrawCard}
|
| 270 |
whileTap={{ scale: 0.95 }}
|
| 271 |
>
|
| 272 |
+
Draw Card
|
| 273 |
</motion.button>
|
| 274 |
)}
|
| 275 |
</div>
|
|
|
|
| 285 |
function PlayerDisplay({ player, isCurrentTurn }: PlayerDisplayProps) {
|
| 286 |
return (
|
| 287 |
<motion.div
|
| 288 |
+
className={`bg-black/50 rounded-lg p-1 md:p-2 ${isCurrentTurn ? 'ring-2 ring-green-500' : ''}`}
|
| 289 |
animate={isCurrentTurn ? { scale: [1, 1.02, 1] } : {}}
|
| 290 |
transition={{ repeat: isCurrentTurn ? Infinity : 0, duration: 1.5 }}
|
| 291 |
>
|
| 292 |
+
<div className="flex items-center gap-1 md:gap-2 mb-1">
|
| 293 |
+
<div className={`player-avatar w-5 h-5 md:w-8 md:h-8 text-xs md:text-sm ${!player.isAlive ? 'eliminated' : ''} ${isCurrentTurn ? 'current-turn' : ''}`}>
|
| 294 |
{player.name.charAt(0).toUpperCase()}
|
| 295 |
</div>
|
| 296 |
<div>
|
| 297 |
+
<p className={`text-xs md:text-sm ${player.isAlive ? 'text-papyrus' : 'text-papyrus/50 line-through'}`}>
|
| 298 |
{player.name}
|
| 299 |
</p>
|
| 300 |
+
{!player.isAlive && <p className="text-mummy-red text-xs">☠️</p>}
|
| 301 |
</div>
|
| 302 |
</div>
|
| 303 |
|
| 304 |
{player.isAlive && (
|
| 305 |
+
<div className="flex gap-0.5 justify-center">
|
| 306 |
+
{Array.from({ length: Math.min(player.cardCount, 6) }).map((_, i) => (
|
| 307 |
<div
|
| 308 |
key={i}
|
| 309 |
+
className="w-2 h-3 md:w-4 md:h-6 bg-gradient-to-br from-egyptian-gold to-sand rounded shadow-sm"
|
| 310 |
/>
|
| 311 |
))}
|
| 312 |
+
{player.cardCount > 6 && (
|
| 313 |
+
<span className="text-egyptian-gold text-xs">+{player.cardCount - 6}</span>
|
| 314 |
)}
|
| 315 |
</div>
|
| 316 |
)}
|
client/src/components/screens/Lobby.tsx
CHANGED
|
@@ -46,9 +46,9 @@ export function Lobby() {
|
|
| 46 |
|
| 47 |
return (
|
| 48 |
<div
|
| 49 |
-
className="flex-1 flex flex-col items-center justify-center p-4"
|
| 50 |
style={{
|
| 51 |
-
backgroundImage: 'url(/menu_background.png)',
|
| 52 |
backgroundSize: 'cover',
|
| 53 |
backgroundPosition: 'center',
|
| 54 |
}}
|
|
@@ -56,24 +56,24 @@ export function Lobby() {
|
|
| 56 |
<motion.div
|
| 57 |
initial={{ opacity: 0, scale: 0.9 }}
|
| 58 |
animate={{ opacity: 1, scale: 1 }}
|
| 59 |
-
className="bg-black/70 backdrop-blur-sm rounded-2xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
| 60 |
>
|
| 61 |
{/* Header */}
|
| 62 |
-
<div className="flex items-center justify-between mb-6">
|
| 63 |
-
<button onClick={handleBack} className="text-egyptian-gold hover:text-yellow-400">
|
| 64 |
← Back
|
| 65 |
</button>
|
| 66 |
-
<h1 className="text-2xl font-bold text-egyptian-gold">Lobby</h1>
|
| 67 |
-
<span className="text-papyrus">{playerName}</span>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
{/* Tabs */}
|
| 71 |
-
<div className="flex border-b border-egyptian-gold/30 mb-6">
|
| 72 |
{(['create', 'join', 'browse'] as const).map((tab) => (
|
| 73 |
<button
|
| 74 |
key={tab}
|
| 75 |
onClick={() => setActiveTab(tab)}
|
| 76 |
-
className={`flex-1 py-2 text-center capitalize transition-colors ${
|
| 77 |
activeTab === tab
|
| 78 |
? 'text-egyptian-gold border-b-2 border-egyptian-gold'
|
| 79 |
: 'text-papyrus/60 hover:text-papyrus'
|
|
@@ -90,10 +90,10 @@ export function Lobby() {
|
|
| 90 |
initial={{ opacity: 0, x: -20 }}
|
| 91 |
animate={{ opacity: 1, x: 0 }}
|
| 92 |
>
|
| 93 |
-
<p className="text-papyrus mb-4">Create a new room for your friends to join.</p>
|
| 94 |
|
| 95 |
-
<div className="mb-4">
|
| 96 |
-
<label className="block text-papyrus mb-2">Room Name</label>
|
| 97 |
<input
|
| 98 |
type="text"
|
| 99 |
value={roomName}
|
|
|
|
| 46 |
|
| 47 |
return (
|
| 48 |
<div
|
| 49 |
+
className="flex-1 flex flex-col items-center justify-center p-2 md:p-4 overflow-hidden"
|
| 50 |
style={{
|
| 51 |
+
backgroundImage: 'url(/Assests/menu_background.png)',
|
| 52 |
backgroundSize: 'cover',
|
| 53 |
backgroundPosition: 'center',
|
| 54 |
}}
|
|
|
|
| 56 |
<motion.div
|
| 57 |
initial={{ opacity: 0, scale: 0.9 }}
|
| 58 |
animate={{ opacity: 1, scale: 1 }}
|
| 59 |
+
className="bg-black/70 backdrop-blur-sm rounded-2xl p-3 md:p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
| 60 |
>
|
| 61 |
{/* Header */}
|
| 62 |
+
<div className="flex items-center justify-between mb-3 md:mb-6">
|
| 63 |
+
<button onClick={handleBack} className="text-egyptian-gold hover:text-yellow-400 text-sm md:text-base">
|
| 64 |
← Back
|
| 65 |
</button>
|
| 66 |
+
<h1 className="text-lg md:text-2xl font-bold text-egyptian-gold">Lobby</h1>
|
| 67 |
+
<span className="text-papyrus text-xs md:text-base">{playerName}</span>
|
| 68 |
</div>
|
| 69 |
|
| 70 |
{/* Tabs */}
|
| 71 |
+
<div className="flex border-b border-egyptian-gold/30 mb-3 md:mb-6">
|
| 72 |
{(['create', 'join', 'browse'] as const).map((tab) => (
|
| 73 |
<button
|
| 74 |
key={tab}
|
| 75 |
onClick={() => setActiveTab(tab)}
|
| 76 |
+
className={`flex-1 py-1 md:py-2 text-center capitalize transition-colors text-sm md:text-base ${
|
| 77 |
activeTab === tab
|
| 78 |
? 'text-egyptian-gold border-b-2 border-egyptian-gold'
|
| 79 |
: 'text-papyrus/60 hover:text-papyrus'
|
|
|
|
| 90 |
initial={{ opacity: 0, x: -20 }}
|
| 91 |
animate={{ opacity: 1, x: 0 }}
|
| 92 |
>
|
| 93 |
+
<p className="text-papyrus text-sm md:text-base mb-2 md:mb-4">Create a new room for your friends to join.</p>
|
| 94 |
|
| 95 |
+
<div className="mb-3 md:mb-4">
|
| 96 |
+
<label className="block text-papyrus text-sm mb-1 md:mb-2">Room Name</label>
|
| 97 |
<input
|
| 98 |
type="text"
|
| 99 |
value={roomName}
|
client/src/components/screens/MainMenu.tsx
CHANGED
|
@@ -19,9 +19,9 @@ export function MainMenu() {
|
|
| 19 |
|
| 20 |
return (
|
| 21 |
<div
|
| 22 |
-
className="flex-1 flex flex-col items-center justify-center p-4"
|
| 23 |
style={{
|
| 24 |
-
backgroundImage: 'url(/menu_background.png)',
|
| 25 |
backgroundSize: 'cover',
|
| 26 |
backgroundPosition: 'center',
|
| 27 |
}}
|
|
@@ -30,13 +30,13 @@ export function MainMenu() {
|
|
| 30 |
initial={{ opacity: 0, y: -50 }}
|
| 31 |
animate={{ opacity: 1, y: 0 }}
|
| 32 |
transition={{ duration: 0.5 }}
|
| 33 |
-
className="bg-black/60 backdrop-blur-sm rounded-2xl p-8 max-w-md w-full"
|
| 34 |
>
|
| 35 |
{/* Logo */}
|
| 36 |
<motion.img
|
| 37 |
-
src="/ui_logo.png"
|
| 38 |
-
alt="
|
| 39 |
-
className="w-48 h-48 mx-auto mb-6 object-contain"
|
| 40 |
initial={{ scale: 0 }}
|
| 41 |
animate={{ scale: 1, rotate: [0, -5, 5, 0] }}
|
| 42 |
transition={{ duration: 0.5, delay: 0.2 }}
|
|
@@ -45,30 +45,30 @@ export function MainMenu() {
|
|
| 45 |
}}
|
| 46 |
/>
|
| 47 |
|
| 48 |
-
<h1 className="text-3xl font-bold text-center text-egyptian-gold mb-2">
|
| 49 |
-
|
| 50 |
</h1>
|
| 51 |
-
<h2 className="text-xl text-center text-papyrus mb-8" dir="rtl">
|
| 52 |
هتتحنط هنا
|
| 53 |
</h2>
|
| 54 |
|
| 55 |
{/* Connection status */}
|
| 56 |
-
<div className="flex items-center justify-center gap-2 mb-6">
|
| 57 |
-
<span className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
| 58 |
-
<span className="text-sm text-papyrus">
|
| 59 |
-
{isConnected ? 'Connected
|
| 60 |
</span>
|
| 61 |
</div>
|
| 62 |
|
| 63 |
{/* Name input */}
|
| 64 |
-
<div className="mb-6">
|
| 65 |
-
<label className="block text-papyrus mb-2">Your Name</label>
|
| 66 |
<input
|
| 67 |
type="text"
|
| 68 |
value={playerName}
|
| 69 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 70 |
placeholder="Enter your name..."
|
| 71 |
-
className="w-full"
|
| 72 |
maxLength={20}
|
| 73 |
onKeyDown={(e) => e.key === 'Enter' && handlePlay()}
|
| 74 |
/>
|
|
@@ -76,7 +76,7 @@ export function MainMenu() {
|
|
| 76 |
|
| 77 |
{/* Play button */}
|
| 78 |
<motion.button
|
| 79 |
-
className="btn btn-primary w-full text-xl"
|
| 80 |
onClick={handlePlay}
|
| 81 |
disabled={!isConnected}
|
| 82 |
whileHover={{ scale: 1.02 }}
|
|
|
|
| 19 |
|
| 20 |
return (
|
| 21 |
<div
|
| 22 |
+
className="flex-1 flex flex-col items-center justify-center p-2 md:p-4 overflow-hidden"
|
| 23 |
style={{
|
| 24 |
+
backgroundImage: 'url(/Assests/menu_background.png)',
|
| 25 |
backgroundSize: 'cover',
|
| 26 |
backgroundPosition: 'center',
|
| 27 |
}}
|
|
|
|
| 30 |
initial={{ opacity: 0, y: -50 }}
|
| 31 |
animate={{ opacity: 1, y: 0 }}
|
| 32 |
transition={{ duration: 0.5 }}
|
| 33 |
+
className="bg-black/60 backdrop-blur-sm rounded-2xl p-4 md:p-8 max-w-md w-full"
|
| 34 |
>
|
| 35 |
{/* Logo */}
|
| 36 |
<motion.img
|
| 37 |
+
src="/Assests/ui_logo.png"
|
| 38 |
+
alt="Khofo Card Game"
|
| 39 |
+
className="w-24 h-24 md:w-48 md:h-48 mx-auto mb-3 md:mb-6 object-contain"
|
| 40 |
initial={{ scale: 0 }}
|
| 41 |
animate={{ scale: 1, rotate: [0, -5, 5, 0] }}
|
| 42 |
transition={{ duration: 0.5, delay: 0.2 }}
|
|
|
|
| 45 |
}}
|
| 46 |
/>
|
| 47 |
|
| 48 |
+
<h1 className="text-xl md:text-3xl font-bold text-center text-egyptian-gold mb-1 md:mb-2">
|
| 49 |
+
Khofo Card Game
|
| 50 |
</h1>
|
| 51 |
+
<h2 className="text-base md:text-xl text-center text-papyrus mb-4 md:mb-8" dir="rtl">
|
| 52 |
هتتحنط هنا
|
| 53 |
</h2>
|
| 54 |
|
| 55 |
{/* Connection status */}
|
| 56 |
+
<div className="flex items-center justify-center gap-2 mb-3 md:mb-6">
|
| 57 |
+
<span className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
| 58 |
+
<span className="text-xs md:text-sm text-papyrus">
|
| 59 |
+
{isConnected ? 'Connected' : 'Connecting...'}
|
| 60 |
</span>
|
| 61 |
</div>
|
| 62 |
|
| 63 |
{/* Name input */}
|
| 64 |
+
<div className="mb-4 md:mb-6">
|
| 65 |
+
<label className="block text-papyrus text-sm md:text-base mb-1 md:mb-2">Your Name</label>
|
| 66 |
<input
|
| 67 |
type="text"
|
| 68 |
value={playerName}
|
| 69 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 70 |
placeholder="Enter your name..."
|
| 71 |
+
className="w-full text-sm md:text-base"
|
| 72 |
maxLength={20}
|
| 73 |
onKeyDown={(e) => e.key === 'Enter' && handlePlay()}
|
| 74 |
/>
|
|
|
|
| 76 |
|
| 77 |
{/* Play button */}
|
| 78 |
<motion.button
|
| 79 |
+
className="btn btn-primary w-full text-base md:text-xl py-2 md:py-3"
|
| 80 |
onClick={handlePlay}
|
| 81 |
disabled={!isConnected}
|
| 82 |
whileHover={{ scale: 1.02 }}
|
client/src/components/screens/Room.tsx
CHANGED
|
@@ -36,9 +36,9 @@ export function Room() {
|
|
| 36 |
|
| 37 |
return (
|
| 38 |
<div
|
| 39 |
-
className="flex-1 flex flex-col items-center justify-center p-4"
|
| 40 |
style={{
|
| 41 |
-
backgroundImage: 'url(/menu_background.png)',
|
| 42 |
backgroundSize: 'cover',
|
| 43 |
backgroundPosition: 'center',
|
| 44 |
}}
|
|
@@ -46,15 +46,15 @@ export function Room() {
|
|
| 46 |
<motion.div
|
| 47 |
initial={{ opacity: 0, scale: 0.9 }}
|
| 48 |
animate={{ opacity: 1, scale: 1 }}
|
| 49 |
-
className="bg-black/70 backdrop-blur-sm rounded-2xl p-6 max-w-lg w-full"
|
| 50 |
>
|
| 51 |
{/* Header */}
|
| 52 |
-
<div className="flex items-center justify-between mb-4">
|
| 53 |
-
<button onClick={handleLeave} className="text-egyptian-gold hover:text-yellow-400">
|
| 54 |
← Leave
|
| 55 |
</button>
|
| 56 |
-
<h1 className="text-xl font-bold text-egyptian-gold">{currentRoom.name}</h1>
|
| 57 |
-
<span className="text-papyrus/60">
|
| 58 |
{currentRoom.players.length}/{currentRoom.maxPlayers}
|
| 59 |
</span>
|
| 60 |
</div>
|
|
|
|
| 36 |
|
| 37 |
return (
|
| 38 |
<div
|
| 39 |
+
className="flex-1 flex flex-col items-center justify-center p-2 md:p-4 overflow-hidden"
|
| 40 |
style={{
|
| 41 |
+
backgroundImage: 'url(/Assests/menu_background.png)',
|
| 42 |
backgroundSize: 'cover',
|
| 43 |
backgroundPosition: 'center',
|
| 44 |
}}
|
|
|
|
| 46 |
<motion.div
|
| 47 |
initial={{ opacity: 0, scale: 0.9 }}
|
| 48 |
animate={{ opacity: 1, scale: 1 }}
|
| 49 |
+
className="bg-black/70 backdrop-blur-sm rounded-2xl p-3 md:p-6 max-w-lg w-full"
|
| 50 |
>
|
| 51 |
{/* Header */}
|
| 52 |
+
<div className="flex items-center justify-between mb-2 md:mb-4">
|
| 53 |
+
<button onClick={handleLeave} className="text-egyptian-gold hover:text-yellow-400 text-sm md:text-base">
|
| 54 |
← Leave
|
| 55 |
</button>
|
| 56 |
+
<h1 className="text-base md:text-xl font-bold text-egyptian-gold truncate max-w-[150px] md:max-w-none">{currentRoom.name}</h1>
|
| 57 |
+
<span className="text-papyrus/60 text-sm md:text-base">
|
| 58 |
{currentRoom.players.length}/{currentRoom.maxPlayers}
|
| 59 |
</span>
|
| 60 |
</div>
|
client/src/index.css
CHANGED
|
@@ -17,6 +17,8 @@ html, body {
|
|
| 17 |
touch-action: none;
|
| 18 |
user-select: none;
|
| 19 |
-webkit-user-select: none;
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
body {
|
|
@@ -25,13 +27,23 @@ body {
|
|
| 25 |
color: #f5f5dc;
|
| 26 |
min-height: 100vh;
|
| 27 |
min-height: 100dvh;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
#root {
|
| 31 |
min-height: 100vh;
|
| 32 |
min-height: 100dvh;
|
|
|
|
| 33 |
display: flex;
|
| 34 |
flex-direction: column;
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
/* Landscape lock overlay */
|
|
@@ -144,16 +156,17 @@ body {
|
|
| 144 |
.modal-overlay {
|
| 145 |
@apply fixed inset-0 bg-black/70 flex items-center justify-center z-50;
|
| 146 |
@apply backdrop-blur-sm;
|
|
|
|
| 147 |
}
|
| 148 |
|
| 149 |
.modal-content {
|
| 150 |
-
@apply bg-gradient-to-br from-nile-blue to-gray-900 rounded-xl p-6;
|
| 151 |
@apply border-2 border-egyptian-gold shadow-2xl;
|
| 152 |
-
@apply max-w-lg w-full mx-4 max-h-[
|
| 153 |
}
|
| 154 |
|
| 155 |
.modal-title {
|
| 156 |
-
@apply text-xl font-bold text-egyptian-gold mb-4 text-center;
|
| 157 |
}
|
| 158 |
|
| 159 |
/* Toast/Notification styles */
|
|
@@ -181,12 +194,12 @@ body {
|
|
| 181 |
|
| 182 |
/* Player avatar */
|
| 183 |
.player-avatar {
|
| 184 |
-
@apply
|
| 185 |
-
@apply text-nile-blue font-bold
|
| 186 |
}
|
| 187 |
|
| 188 |
.player-avatar.current-turn {
|
| 189 |
-
@apply ring-4 ring-green-500 animate-pulse;
|
| 190 |
}
|
| 191 |
|
| 192 |
.player-avatar.eliminated {
|
|
@@ -206,35 +219,56 @@ body {
|
|
| 206 |
|
| 207 |
/* Game board */
|
| 208 |
.game-board {
|
| 209 |
-
@apply flex-1 flex flex-col justify-between
|
| 210 |
background-size: cover;
|
| 211 |
background-position: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}
|
| 213 |
|
| 214 |
/* Hand area */
|
| 215 |
.hand-area {
|
| 216 |
-
@apply flex justify-center items-end gap-
|
| 217 |
max-width: 100%;
|
| 218 |
-
flex-wrap:
|
| 219 |
-
overflow:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
|
| 222 |
.hand-area .card {
|
| 223 |
@apply flex-shrink-0;
|
| 224 |
-
width:
|
| 225 |
-
max-width:
|
|
|
|
| 226 |
}
|
| 227 |
|
| 228 |
@media (min-width: 768px) {
|
| 229 |
.hand-area {
|
| 230 |
@apply gap-2 p-4;
|
| 231 |
-
flex-wrap: nowrap;
|
| 232 |
overflow-x: auto;
|
| 233 |
}
|
| 234 |
|
| 235 |
.hand-area .card {
|
| 236 |
width: 100px;
|
| 237 |
max-width: 120px;
|
|
|
|
| 238 |
}
|
| 239 |
}
|
| 240 |
|
|
|
|
| 17 |
touch-action: none;
|
| 18 |
user-select: none;
|
| 19 |
-webkit-user-select: none;
|
| 20 |
+
/* Support safe areas for notch/home indicator */
|
| 21 |
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
| 22 |
}
|
| 23 |
|
| 24 |
body {
|
|
|
|
| 27 |
color: #f5f5dc;
|
| 28 |
min-height: 100vh;
|
| 29 |
min-height: 100dvh;
|
| 30 |
+
/* Fullscreen mobile */
|
| 31 |
+
position: fixed;
|
| 32 |
+
top: 0;
|
| 33 |
+
left: 0;
|
| 34 |
+
right: 0;
|
| 35 |
+
bottom: 0;
|
| 36 |
+
width: 100%;
|
| 37 |
+
height: 100%;
|
| 38 |
}
|
| 39 |
|
| 40 |
#root {
|
| 41 |
min-height: 100vh;
|
| 42 |
min-height: 100dvh;
|
| 43 |
+
height: 100%;
|
| 44 |
display: flex;
|
| 45 |
flex-direction: column;
|
| 46 |
+
overflow: hidden;
|
| 47 |
}
|
| 48 |
|
| 49 |
/* Landscape lock overlay */
|
|
|
|
| 156 |
.modal-overlay {
|
| 157 |
@apply fixed inset-0 bg-black/70 flex items-center justify-center z-50;
|
| 158 |
@apply backdrop-blur-sm;
|
| 159 |
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
| 160 |
}
|
| 161 |
|
| 162 |
.modal-content {
|
| 163 |
+
@apply bg-gradient-to-br from-nile-blue to-gray-900 rounded-xl p-3 md:p-6;
|
| 164 |
@apply border-2 border-egyptian-gold shadow-2xl;
|
| 165 |
+
@apply max-w-lg w-full mx-2 md:mx-4 max-h-[85vh] overflow-y-auto;
|
| 166 |
}
|
| 167 |
|
| 168 |
.modal-title {
|
| 169 |
+
@apply text-base md:text-xl font-bold text-egyptian-gold mb-2 md:mb-4 text-center;
|
| 170 |
}
|
| 171 |
|
| 172 |
/* Toast/Notification styles */
|
|
|
|
| 194 |
|
| 195 |
/* Player avatar */
|
| 196 |
.player-avatar {
|
| 197 |
+
@apply rounded-full bg-egyptian-gold flex items-center justify-center flex-shrink-0;
|
| 198 |
+
@apply text-nile-blue font-bold;
|
| 199 |
}
|
| 200 |
|
| 201 |
.player-avatar.current-turn {
|
| 202 |
+
@apply ring-2 md:ring-4 ring-green-500 animate-pulse;
|
| 203 |
}
|
| 204 |
|
| 205 |
.player-avatar.eliminated {
|
|
|
|
| 219 |
|
| 220 |
/* Game board */
|
| 221 |
.game-board {
|
| 222 |
+
@apply flex-1 flex flex-col justify-between;
|
| 223 |
background-size: cover;
|
| 224 |
background-position: center;
|
| 225 |
+
height: 100%;
|
| 226 |
+
overflow: hidden;
|
| 227 |
+
padding: 4px;
|
| 228 |
+
padding-top: max(4px, env(safe-area-inset-top));
|
| 229 |
+
padding-bottom: max(4px, env(safe-area-inset-bottom));
|
| 230 |
+
padding-left: max(4px, env(safe-area-inset-left));
|
| 231 |
+
padding-right: max(4px, env(safe-area-inset-right));
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
@media (min-width: 768px) {
|
| 235 |
+
.game-board {
|
| 236 |
+
padding: 16px;
|
| 237 |
+
}
|
| 238 |
}
|
| 239 |
|
| 240 |
/* Hand area */
|
| 241 |
.hand-area {
|
| 242 |
+
@apply flex justify-center items-end gap-0 p-1;
|
| 243 |
max-width: 100%;
|
| 244 |
+
flex-wrap: nowrap;
|
| 245 |
+
overflow-x: auto;
|
| 246 |
+
overflow-y: hidden;
|
| 247 |
+
-webkit-overflow-scrolling: touch;
|
| 248 |
+
scrollbar-width: none;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.hand-area::-webkit-scrollbar {
|
| 252 |
+
display: none;
|
| 253 |
}
|
| 254 |
|
| 255 |
.hand-area .card {
|
| 256 |
@apply flex-shrink-0;
|
| 257 |
+
width: 50px;
|
| 258 |
+
max-width: 60px;
|
| 259 |
+
margin: 0 -4px;
|
| 260 |
}
|
| 261 |
|
| 262 |
@media (min-width: 768px) {
|
| 263 |
.hand-area {
|
| 264 |
@apply gap-2 p-4;
|
|
|
|
| 265 |
overflow-x: auto;
|
| 266 |
}
|
| 267 |
|
| 268 |
.hand-area .card {
|
| 269 |
width: 100px;
|
| 270 |
max-width: 120px;
|
| 271 |
+
margin: 0;
|
| 272 |
}
|
| 273 |
}
|
| 274 |
|