- Rebuilt Player.tsx with full UI overhaul using Framer Motion animations.

- Added animated transitions between login, lobby, and match states.
- Improved layout, spacing, and visual styling for modern game feel.
- Added smooth auto-connect flow and integrated username lock-in behavior.
- Updated matchmaking and logout buttons with animated interactions.
- Integrated Leaderboard cleanly inside lobby panel.
This commit is contained in:
2025-11-29 02:37:27 +05:30
parent d962d9c5eb
commit a9e2d50b16
3 changed files with 212 additions and 32 deletions

53
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "tictactoe-vite", "name": "tictactoe-vite",
"version": "1.0.0", "version": "v0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tictactoe-vite", "name": "tictactoe-vite",
"version": "1.0.0", "version": "v0.2.0",
"dependencies": { "dependencies": {
"@emotion/react": "latest", "@emotion/react": "latest",
"@emotion/styled": "latest", "@emotion/styled": "latest",
@@ -14,6 +14,7 @@
"@mui/icons-material": "latest", "@mui/icons-material": "latest",
"@mui/material": "latest", "@mui/material": "latest",
"axios": "latest", "axios": "latest",
"framer-motion": "latest",
"markdown-to-jsx": "latest", "markdown-to-jsx": "latest",
"marked": "latest", "marked": "latest",
"react": "latest", "react": "latest",
@@ -2064,6 +2065,33 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3263,6 +3291,21 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3761,6 +3804,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/unified": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",

View File

@@ -19,7 +19,8 @@
"remark-gfm": "latest", "remark-gfm": "latest",
"marked": "latest", "marked": "latest",
"axios": "latest", "axios": "latest",
"@heroiclabs/nakama-js": "^2.8.0" "@heroiclabs/nakama-js": "^2.8.0",
"framer-motion": "latest"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "latest", "@vitejs/plugin-react": "latest",

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Leaderboard from "./Leaderboard"; import Leaderboard from "./Leaderboard";
import { useNakama } from "./providers/NakamaProvider"; import { useNakama } from "./providers/NakamaProvider";
@@ -45,38 +46,167 @@ export default function Player({
return ( return (
<div style={{ marginBottom: "20px" }}> <div style={{ marginBottom: "20px" }}>
{session && !matchId && ( <AnimatePresence mode="wait">
<> {/* ---------------- LOGIN SCREEN ---------------- */}
<h2>Hello, {session.username}</h2> {!session && (
<motion.div
{/* Game mode selection */} key="login"
<label style={{ display: "block", marginTop: "10px" }}> initial={{ opacity: 0, y: 20, scale: 0.97 }}
Select Game Mode: animate={{ opacity: 1, y: 0, scale: 1 }}
</label> exit={{ opacity: 0, y: -20, scale: 0.97 }}
transition={{ duration: 0.4, ease: "easeOut" }}
<select style={{
value={selectedMode} background: "#111",
onChange={(e) => setSelectedMode(e.target.value)} padding: "24px",
style={{ padding: "6px", marginBottom: "10px" }} borderRadius: "16px",
width: "280px",
margin: "0 auto",
color: "white",
boxShadow:
"0 4px 16px rgba(0,0,0,0.4), inset 0 0 20px rgba(255,255,255,0.03)",
textAlign: "center",
}}
> >
<option value="classic">Classic</option> <h2 style={{ marginBottom: "16px" }}>Welcome!</h2>
<option value="blitz">Blitz</option>
</select>
{/* Join matchmaking */} <input
<button onClick={() => startQueue(selectedMode)}>Join Matchmaking</button> placeholder="Enter username"
<button onClick={() => logout()}>Logout</button> value={username}
disabled={username.length > 0}
onChange={(e) => setUsername(e.target.value)}
style={{
padding: "10px",
width: "100%",
borderRadius: "12px",
background: "#222",
color: "white",
border: "1px solid #333",
marginBottom: "12px",
fontSize: "14px",
}}
/>
{/*/!* List open matches *!/*/} <motion.button
{/*<MatchList matches={openMatches} />*/} whileTap={{ scale: 0.95 }}
<Leaderboard/> onClick={handleConnect}
</> disabled={username.length === 0}
)} style={{
{session && matchId && ( width: "100%",
<> padding: "10px",
<h2>Go {session.username}!</h2> borderRadius: "12px",
</> background: username.length ? "#2ecc71" : "#444",
)} border: "none",
cursor: username.length ? "pointer" : "not-allowed",
color: "white",
fontWeight: 600,
fontSize: "14px",
}}
>
Connect
</motion.button>
</motion.div>
)}
{/* ---------------- LOBBY SCREEN ---------------- */}
{session && !matchId && (
<motion.div
key="lobby"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -40 }}
transition={{ duration: 0.45, ease: "easeOut" }}
style={{
padding: "20px",
background: "#0f0f0f",
borderRadius: "20px",
color: "white",
textAlign: "center",
}}
>
<h2 style={{ marginBottom: "10px" }}>
Hello, <span style={{ color: "#2ecc71" }}>{session.username}</span>
</h2>
<label style={{ display: "block", marginTop: "10px", opacity: 0.7 }}>
Select Game Mode
</label>
<select
value={selectedMode}
onChange={(e) => setSelectedMode(e.target.value)}
style={{
padding: "8px",
margin: "10px 0 16px",
width: "60%",
borderRadius: "10px",
background: "#222",
color: "white",
border: "1px solid #333",
}}
>
<option value="classic">Classic</option>
<option value="blitz">Blitz</option>
</select>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={() => startQueue(selectedMode)}
style={{
padding: "10px 20px",
borderRadius: "12px",
background: "#3498db",
color: "white",
border: "none",
marginRight: "10px",
cursor: "pointer",
fontWeight: 600,
}}
>
Join Matchmaking
</motion.button>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={logout}
style={{
padding: "10px 20px",
borderRadius: "12px",
background: "#e74c3c",
color: "white",
border: "none",
cursor: "pointer",
fontWeight: 600,
}}
>
Logout
</motion.button>
<div style={{ marginTop: "24px" }}>
<Leaderboard />
</div>
</motion.div>
)}
{/* ---------------- MATCH SCREEN ---------------- */}
{session && matchId && (
<motion.div
key="match"
initial={{ opacity: 0, scale: 0.9, y: 30 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: -20 }}
transition={{ duration: 0.35, ease: "easeOut" }}
style={{
padding: "20px",
color: "white",
textAlign: "center",
}}
>
<h2 style={{ fontSize: "22px", color: "#f1c40f" }}>
Go {session.username}!
</h2>
</motion.div>
)}
</AnimatePresence>
</div> </div>
); );
} }