can play game
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -1,41 +1,26 @@
|
||||
import { useState } from "react";
|
||||
import "./App.css";
|
||||
import { type FC } from "react";
|
||||
import { UserInput } from "./game/UserInput";
|
||||
import { BoardDisplay } from "./game/BoardDisplay";
|
||||
import { ConnectionStatus } from "./game/ConnectionStatus";
|
||||
import { NameInput } from "./game/NameInput";
|
||||
import { SessionOverride } from "./game/SessionOverride";
|
||||
import { useGameChannelContext } from "./contexts/useGameChannelContext";
|
||||
|
||||
const getPlayerNameFromUrl = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("name") || null;
|
||||
};
|
||||
const App: FC<{ playerName: string }> = ({ playerName }) => {
|
||||
const { isOverridden } = useGameChannelContext();
|
||||
|
||||
function App() {
|
||||
const [playerName, setPlayerName] = useState<string | null>(
|
||||
getPlayerNameFromUrl,
|
||||
);
|
||||
|
||||
if (!playerName) {
|
||||
return <NameInput onNameSubmit={setPlayerName} />;
|
||||
if (isOverridden) {
|
||||
return <SessionOverride />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserInput playerName={playerName} />
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: "#1a1a2e",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div className="w-screen h-screen bg-navy-900">
|
||||
<ConnectionStatus />
|
||||
<BoardDisplay playerName={playerName} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -6,6 +6,7 @@ interface GameChannelContextValue {
|
||||
channel: Channel | null;
|
||||
channelStatus: string;
|
||||
isJoined: boolean;
|
||||
isOverridden: boolean;
|
||||
joinGame: (name: string) => void;
|
||||
}
|
||||
|
||||
@@ -13,29 +14,32 @@ const GameChannelContext = createContext<GameChannelContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface GameChannelProviderProps {
|
||||
channelName: string;
|
||||
params?: Record<string, unknown>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function GameChannelProvider({
|
||||
channelName,
|
||||
params = {},
|
||||
userName,
|
||||
children,
|
||||
}: GameChannelProviderProps) {
|
||||
}: {
|
||||
userName: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { socket, isConnected } = useWebSocketContext();
|
||||
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
const [isOverridden, setIsOverridden] = useState(false);
|
||||
const [channel, setChannel] = useState<Channel | null>(null);
|
||||
|
||||
const channelName = "user:" + userName;
|
||||
useEffect(() => {
|
||||
if (!socket || !isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Joining channel: ${channelName}`);
|
||||
const newChannel = socket.channel(channelName, params);
|
||||
const newChannel = socket.channel(channelName, { });
|
||||
|
||||
newChannel.on("new_browser_connection", () => {
|
||||
console.warn("⚠️ Session overridden by new browser connection");
|
||||
setIsOverridden(true);
|
||||
});
|
||||
|
||||
newChannel
|
||||
.join()
|
||||
@@ -62,7 +66,7 @@ export function GameChannelProvider({
|
||||
setIsJoined(false);
|
||||
setChannelStatus("waiting");
|
||||
};
|
||||
}, [socket, isConnected, channelName, params]);
|
||||
}, [socket, isConnected, channelName]);
|
||||
|
||||
const joinGame = (name: string) => {
|
||||
if (!channel) return;
|
||||
@@ -87,6 +91,7 @@ export function GameChannelProvider({
|
||||
channel,
|
||||
channelStatus,
|
||||
isJoined,
|
||||
isOverridden,
|
||||
joinGame,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -6,26 +6,29 @@ interface WebSocketContextValue {
|
||||
connectionStatus: string;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
export function WebSocketProvider({
|
||||
url,
|
||||
userName,
|
||||
children,
|
||||
}: {
|
||||
url: string;
|
||||
userName: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function WebSocketProvider({ url, children }: WebSocketProviderProps) {
|
||||
}) {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<string>("connecting");
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`Connecting to ${url}`);
|
||||
console.log(`Connecting to ${url} as ${userName}`);
|
||||
|
||||
const newSocket = new Socket(url, {
|
||||
params: { user_name: userName },
|
||||
timeout: 100,
|
||||
reconnectAfterMs: (tries) =>
|
||||
[
|
||||
@@ -60,7 +63,7 @@ export function WebSocketProvider({ url, children }: WebSocketProviderProps) {
|
||||
newSocket.disconnect();
|
||||
setSocket(null);
|
||||
};
|
||||
}, [url]);
|
||||
}, [url, userName]);
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useGameChannelContext } from "../contexts/useGameChannelContext";
|
||||
import { z } from "zod";
|
||||
import { BoardPlayer } from "./BoardPlayer";
|
||||
|
||||
interface Player {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
const PlayerSchema = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
});
|
||||
|
||||
interface GameState {
|
||||
[playerName: string]: Player;
|
||||
}
|
||||
const GameStateSchema = z.record(z.string(), PlayerSchema);
|
||||
|
||||
export const BoardDisplay = ({ playerName }: {
|
||||
playerName: string;
|
||||
}) => {
|
||||
type GameState = z.infer<typeof GameStateSchema>;
|
||||
|
||||
export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
||||
const { channel, isJoined, joinGame } = useGameChannelContext();
|
||||
const [players, setPlayers] = useState<GameState>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (channel && !isJoined) {
|
||||
// Send join_game message with player name
|
||||
joinGame(playerName);
|
||||
}
|
||||
}, [channel, isJoined, playerName, joinGame]);
|
||||
@@ -26,62 +25,34 @@ export const BoardDisplay = ({ playerName }: {
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
// Listen for game state updates
|
||||
const ref = channel.on("game_state", (payload: { players: GameState }) => {
|
||||
setPlayers(payload.players);
|
||||
});
|
||||
const ref = channel.on(
|
||||
"game_state",
|
||||
(payload: { game_state: unknown }) => {
|
||||
const result = GameStateSchema.safeParse(payload.game_state);
|
||||
if (result.success) {
|
||||
setPlayers(result.data);
|
||||
} else {
|
||||
console.error("Invalid game state received:", result.error);
|
||||
setPlayers({});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Cleanup listener on unmount
|
||||
return () => {
|
||||
channel.off("game_state", ref);
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "800px",
|
||||
height: "600px",
|
||||
background: "#16213e",
|
||||
margin: "50px auto",
|
||||
border: "2px solid #0f3460",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div className="relative w-200 h-150 bg-navy-800 my-12.5 mx-auto border-2 border-navy-700 overflow-hidden">
|
||||
{Object.entries(players).map(([name, player]) => (
|
||||
<div
|
||||
<BoardPlayer
|
||||
key={name}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: player.x,
|
||||
top: player.y,
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "50%",
|
||||
background: name === playerName ? "#e94560" : "#53a8b6",
|
||||
border:
|
||||
name === playerName ? "3px solid #ff6b6b" : "2px solid #48d6e0",
|
||||
transition: "all 0.1s linear",
|
||||
transform: "translate(-50%, -50%)",
|
||||
boxShadow:
|
||||
name === playerName ? "0 0 10px #e94560" : "0 0 5px #53a8b6",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-25px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
color: "#fff",
|
||||
fontSize: "10px",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
name={name}
|
||||
x={player.x}
|
||||
y={player.y}
|
||||
isCurrentPlayer={name === playerName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
28
client/src/game/BoardPlayer.tsx
Normal file
28
client/src/game/BoardPlayer.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type FC } from "react";
|
||||
|
||||
export const BoardPlayer: FC<{
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
isCurrentPlayer: boolean;
|
||||
}> = ({ name, x, y, isCurrentPlayer }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
left: x,
|
||||
top: y,
|
||||
}}
|
||||
className={`
|
||||
transition-all duration-50
|
||||
absolute w-5 h-5 rounded-full linear -translate-x-1/2 -translate-y-1/2 ${
|
||||
isCurrentPlayer
|
||||
? "bg-accent-600 border-[3px] border-accent-400 shadow-[0_0_10px_var(--color-accent-600)]"
|
||||
: "bg-navy-400 border-2 border-navy-300 shadow-[0_0_5px_var(--color-navy-400)]"
|
||||
}`}
|
||||
>
|
||||
<div className="absolute -top-6.25 left-1/2 -translate-x-1/2 text-navy-50 text-[10px] whitespace-nowrap">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,19 +6,7 @@ export const ConnectionStatus = () => {
|
||||
const { channelStatus } = useGameChannelContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 10,
|
||||
color: "white",
|
||||
fontFamily: "monospace",
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<div className="text-navy-50 font-mono bg-navy-900/80 p-2.5 rounded-[5px] text-xs">
|
||||
<div>WebSocket: {connectionStatus}</div>
|
||||
<div>Channel: {channelStatus}</div>
|
||||
</div>
|
||||
|
||||
@@ -29,35 +29,9 @@ export const NameInput = ({ onNameSubmit }: NameInputProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: "#1a1a2e",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "#16213e",
|
||||
padding: "40px",
|
||||
borderRadius: "10px",
|
||||
border: "2px solid #0f3460",
|
||||
maxWidth: "400px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
color: "#e94560",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "24px",
|
||||
marginBottom: "20px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div className="w-screen h-screen bg-navy-900 flex items-center justify-center">
|
||||
<div className="bg-navy-800 p-10 rounded-[10px] border-2 border-navy-700 max-w-100 w-full">
|
||||
<h1 className="text-accent-500 font-mono text-2xl mb-5 text-center">
|
||||
Enter Your Name
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -67,34 +41,16 @@ export const NameInput = ({ onNameSubmit }: NameInputProps) => {
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your name..."
|
||||
autoFocus
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
fontSize: "16px",
|
||||
background: "#0f3460",
|
||||
border: "2px solid #53a8b6",
|
||||
borderRadius: "5px",
|
||||
color: "white",
|
||||
fontFamily: "monospace",
|
||||
marginBottom: "20px",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
className="w-full p-3 text-base bg-navy-700 border-2 border-navy-500 rounded-[5px] text-navy-50 font-mono mb-5 box-border placeholder:text-navy-400"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim()}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
fontSize: "16px",
|
||||
background: name.trim() ? "#e94560" : "#666",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
color: "white",
|
||||
fontFamily: "monospace",
|
||||
cursor: name.trim() ? "pointer" : "not-allowed",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
className={`w-full p-3 text-base border-none rounded-[5px] text-navy-50 font-mono font-bold ${
|
||||
name.trim()
|
||||
? "bg-accent-600 hover:bg-accent-500 cursor-pointer"
|
||||
: "bg-navy-600 cursor-not-allowed"
|
||||
}`}
|
||||
>
|
||||
Join Game
|
||||
</button>
|
||||
|
||||
24
client/src/game/SessionOverride.tsx
Normal file
24
client/src/game/SessionOverride.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
export const SessionOverride: FC = () => {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-navy-900/95 flex items-center justify-center z-50">
|
||||
<div className="bg-navy-800 border-2 border-accent-600 rounded-lg p-8 max-w-md text-center">
|
||||
<div className="text-6xl mb-4">⚠️</div>
|
||||
<h2 className="text-2xl font-bold text-accent-500 mb-4">
|
||||
Session Overridden
|
||||
</h2>
|
||||
<p className="text-navy-200 mb-6">
|
||||
This session has been replaced by a new browser connection. Your
|
||||
inputs are no longer active.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="bg-accent-600 hover:bg-accent-500 text-navy-900 font-bold py-2 px-6 rounded transition-colors"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,34 +12,37 @@ export const UserInput = ({ playerName }: { playerName: string }) => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (["w", "a", "s", "d"].includes(key)) {
|
||||
e.preventDefault();
|
||||
keysPressed.current.add(key);
|
||||
if (!keysPressed.current.has(key) && channel.state === "joined") {
|
||||
keysPressed.current.add(key);
|
||||
channel.push("key_down", { key, name: playerName });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (["w", "a", "s", "d"].includes(key)) {
|
||||
keysPressed.current.delete(key);
|
||||
if (keysPressed.current.has(key) && channel.state === "joined") {
|
||||
keysPressed.current.delete(key);
|
||||
channel.push("key_up", { key, name: playerName });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
if (channel.state === "joined") {
|
||||
keysPressed.current.forEach((key) => {
|
||||
channel.push("key_up", { key, name: playerName });
|
||||
});
|
||||
}
|
||||
keysPressed.current.clear();
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (keysPressed.current.size > 0 && channel.state === "joined") {
|
||||
const directions = Array.from(keysPressed.current);
|
||||
channel.push("move", { directions, name: playerName });
|
||||
}
|
||||
}, 100);
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
window.addEventListener("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
|
||||
@@ -1,66 +1,92 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
@import "tailwindcss";
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
/* :root {
|
||||
--color-navy-50: hsl(222 30% 94%);
|
||||
--color-navy-100: hsl(222 32% 88%);
|
||||
--color-navy-200: hsl(222 35% 77%);
|
||||
--color-navy-300: hsl(222 38% 66%);
|
||||
--color-navy-400: hsl(222 40% 55%);
|
||||
--color-navy-500: hsl(222 45% 43%);
|
||||
--color-navy-600: hsl(222 50% 34%);
|
||||
--color-navy-700: hsl(222 55% 26%);
|
||||
--color-navy-800: hsl(222 58% 17%);
|
||||
--color-navy-900: hsl(222 60% 9%);
|
||||
--color-navy-950: hsl(222 55% 5%);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
--color-accent-50: hsl(13 35% 94%);
|
||||
--color-accent-100: hsl(13 38% 88%);
|
||||
--color-accent-200: hsl(13 40% 77%);
|
||||
--color-accent-300: hsl(13 42% 66%);
|
||||
--color-accent-400: hsl(13 45% 55%);
|
||||
--color-accent-500: hsl(13 48% 50%);
|
||||
--color-accent-600: hsl(13 50% 42%);
|
||||
--color-accent-700: hsl(13 52% 34%);
|
||||
--color-accent-800: hsl(13 55% 23%);
|
||||
--color-accent-900: hsl(13 58% 12%);
|
||||
--color-accent-950: hsl(13, 60%, 6%);
|
||||
} */
|
||||
@theme {
|
||||
--color-navy-50: hsl(222 30% 94%);
|
||||
--color-navy-100: hsl(222 32% 88%);
|
||||
--color-navy-200: hsl(222 35% 77%);
|
||||
--color-navy-300: hsl(222 38% 66%);
|
||||
--color-navy-400: hsl(222 40% 55%);
|
||||
--color-navy-500: hsl(222 45% 43%);
|
||||
--color-navy-600: hsl(222 50% 34%);
|
||||
--color-navy-700: hsl(222 55% 26%);
|
||||
--color-navy-800: hsl(222 58% 17%);
|
||||
--color-navy-900: hsl(222 60% 9%);
|
||||
--color-navy-950: hsl(222 55% 5%);
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
--color-accent-50: hsl(7 35% 94%);
|
||||
--color-accent-100: hsl(7 38% 88%);
|
||||
--color-accent-200: hsl(7 40% 77%);
|
||||
--color-accent-300: hsl(7 42% 66%);
|
||||
--color-accent-400: hsl(7 45% 55%);
|
||||
--color-accent-500: hsl(7 48% 50%);
|
||||
--color-accent-600: hsl(7 50% 42%);
|
||||
--color-accent-700: hsl(7 52% 34%);
|
||||
--color-accent-800: hsl(7 55% 23%);
|
||||
--color-accent-900: hsl(7 58% 12%);
|
||||
--color-accent-950: hsl(7 60% 6%);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
@apply m-0 min-w-[320px] min-h-screen font-sans antialiased;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
@apply text-5xl font-bold leading-tight;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-4xl font-bold leading-tight;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-3xl font-semibold leading-snug;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl font-semibold leading-snug;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-xl font-semibold leading-normal;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-lg font-semibold leading-normal;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply font-medium no-underline transition-colors;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
@apply rounded-lg border border-transparent px-5 py-2.5 text-base font-medium cursor-pointer transition-colors;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
button:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
import { StrictMode } from "react";
|
||||
import { StrictMode, useState, type FC } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { GameChannelProvider } from "./contexts/GameChannelContext";
|
||||
import { WebSocketProvider } from "./contexts/WebSocketContext.tsx";
|
||||
import { GameChannelProvider } from "./contexts/GameChannelContext.tsx";
|
||||
import { NameInput } from "./game/NameInput.tsx";
|
||||
|
||||
const WS_SERVER = "ws://localhost:4000/socket";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<WebSocketProvider url={WS_SERVER}>
|
||||
<GameChannelProvider channelName="game:lobby">
|
||||
<App />
|
||||
const getPlayerNameFromUrl = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("name") || null;
|
||||
};
|
||||
|
||||
export const ProvidersWithName: FC = () => {
|
||||
const [playerName, setPlayerName] = useState<string | null>(
|
||||
getPlayerNameFromUrl,
|
||||
);
|
||||
|
||||
if (!playerName) {
|
||||
return <NameInput onNameSubmit={setPlayerName} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WebSocketProvider url={WS_SERVER} userName={playerName}>
|
||||
<GameChannelProvider userName={playerName}>
|
||||
<App playerName={playerName} />
|
||||
</GameChannelProvider>
|
||||
</WebSocketProvider>
|
||||
);
|
||||
};
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ProvidersWithName />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user