Files
tic-tac-toe-ui/src/tictactoe/Player.tsx

283 lines
8.6 KiB
TypeScript

import React, { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Leaderboard from "./Leaderboard";
import { useNakama } from "./providers/NakamaProvider";
export default function Player({
onMatchDataCallback,
}: {
onMatchDataCallback: (msg: any) => void;
}) {
const {
session,
matchId,
loginOrRegister,
logout,
onMatchData,
joinMatchmaker,
} = useNakama();
const [username, setUsername] = useState(
localStorage.getItem("username") ?? ""
);
const [selectedMode, setSelectedMode] = useState("classic");
const [isQueueing, setIsQueueing] = useState(false);
const isRegistered = localStorage.getItem("registered") === "yes";
// ------------------------------------------
// CONNECT
// ------------------------------------------
async function handleConnect() {
await loginOrRegister(username);
// Match data listener
onMatchData(onMatchDataCallback);
}
// ------------------------------------------
// MATCHMAKING
// ------------------------------------------
async function startQueue(selectedMode: string) {
setIsQueueing(true);
try {
const ticket = await joinMatchmaker(selectedMode);
console.log("Queued:", ticket);
} catch (err) {
console.error("Matchmaking failed:", err);
setIsQueueing(false);
}
}
function cancelQueue() {
setIsQueueing(false);
// Nakama matchmaker tickets auto-expire by default in your setup.
// If you later add manual ticket cancel RPC, call it here.
}
useEffect(() => {
handleConnect();
}, []);
return (
<div style={{ marginBottom: "20px" }}>
<AnimatePresence mode="wait">
{/* ---------------- LOGIN SCREEN ---------------- */}
{!session && (
<motion.div
key="login"
initial={{ opacity: 0, y: 20, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.97 }}
transition={{ duration: 0.4, ease: "easeOut" }}
style={{
background: "#111",
padding: "24px",
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",
}}
>
<h2 style={{ marginBottom: "16px" }}>Welcome!</h2>
<input
placeholder="Enter username"
value={username}
disabled={isRegistered}
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",
}}
/>
<motion.button
whileTap={{ scale: 0.95 }}
onClick={handleConnect}
disabled={username.length === 0}
style={{
width: "100%",
padding: "10px",
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}
disabled={isQueueing}
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>
{!isQueueing && (
<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>
)}
{/* Queueing animation */}
{isQueueing && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
style={{
marginTop: "10px",
marginBottom: "10px",
padding: "12px 16px",
borderRadius: "12px",
background: "#222",
color: "white",
display: "inline-block",
fontSize: "14px",
border: "1px solid #333",
}}
>
<div style={{ marginBottom: "6px", fontWeight: 600 }}>
Finding an opponent
</div>
{/* Animated pulsing dots */}
<motion.div
animate={{ opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.2, repeat: Infinity }}
style={{ letterSpacing: "2px", fontSize: "18px" }}
>
</motion.div>
{/* Cancel button */}
<button
onClick={cancelQueue}
style={{
marginTop: "10px",
padding: "6px 12px",
borderRadius: "8px",
background: "#e74c3c",
color: "white",
border: "none",
cursor: "pointer",
fontSize: "12px",
fontWeight: 600,
}}
>
Cancel
</button>
</motion.div>
)}
<div style={{ marginTop: "24px" }}>
<Leaderboard />
</div>
<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>
</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={{ marginBottom: "10px" }}>
Go, <span style={{ color: "#2ecc71" }}>{session.username}</span>
</h2>
</motion.div>
)}
</AnimatePresence>
</div>
);
}