refactored frontent to separate files
This commit is contained in:
@@ -1,132 +1,12 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { Socket, Channel } from "phoenix";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
import { UserInput } from "./game/UserInput";
|
||||||
interface Player {
|
import { BoardDisplay } from "./game/BoardDisplay";
|
||||||
x: number;
|
import { ConnectionStatus } from "./game/ConnectionStatus";
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameState {
|
|
||||||
[playerId: string]: Player;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to nginx load balancer
|
|
||||||
const WS_SERVER = "ws://localhost:4000/socket";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [players, setPlayers] = useState<GameState>({});
|
|
||||||
const [myPlayerId, setMyPlayerId] = useState<string | null>(null);
|
|
||||||
const [connectionStatus, setConnectionStatus] =
|
|
||||||
useState<string>("connecting");
|
|
||||||
const socketRef = useRef<Socket | null>(null);
|
|
||||||
const channelRef = useRef<Channel | null>(null);
|
|
||||||
const keysPressed = useRef<Set<string>>(new Set());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Connect to nginx load balancer
|
|
||||||
console.log(`Connecting to ${WS_SERVER}`);
|
|
||||||
|
|
||||||
const socket = new Socket(WS_SERVER, {
|
|
||||||
timeout: 3000,
|
|
||||||
reconnectAfterMs: (tries) =>
|
|
||||||
[1000, 2000, 5000, 10000][tries - 1] || 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.onOpen(() => {
|
|
||||||
console.log(`✓ Connected to load balancer`);
|
|
||||||
setConnectionStatus("Connected");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.onError((error) => {
|
|
||||||
console.error(`✗ Connection error:`, error);
|
|
||||||
setConnectionStatus("Connection error");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.onClose(() => {
|
|
||||||
console.log(`✗ Disconnected from load balancer`);
|
|
||||||
setConnectionStatus("Disconnected - reconnecting...");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.connect();
|
|
||||||
socketRef.current = socket;
|
|
||||||
|
|
||||||
// Join game channel
|
|
||||||
const channel = socket.channel("game:lobby", {});
|
|
||||||
|
|
||||||
channel
|
|
||||||
.join()
|
|
||||||
.receive("ok", () => {
|
|
||||||
console.log(`✓ Joined game channel`);
|
|
||||||
setConnectionStatus("Connected & playing");
|
|
||||||
})
|
|
||||||
.receive("error", (resp) => {
|
|
||||||
console.log(`✗ Failed to join:`, resp);
|
|
||||||
setConnectionStatus("Failed to join game");
|
|
||||||
})
|
|
||||||
.receive("timeout", () => {
|
|
||||||
console.log(`✗ Timeout joining`);
|
|
||||||
setConnectionStatus("Connection timeout");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for game state updates
|
|
||||||
channel.on("game_state", (payload: { players: GameState }) => {
|
|
||||||
setPlayers(payload.players);
|
|
||||||
|
|
||||||
if (!myPlayerId && Object.keys(payload.players).length > 0) {
|
|
||||||
const playerIds = Object.keys(payload.players);
|
|
||||||
if (playerIds.length > 0) {
|
|
||||||
setMyPlayerId(playerIds[playerIds.length - 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
channelRef.current = channel;
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
return () => {
|
|
||||||
channel.leave();
|
|
||||||
socket.disconnect();
|
|
||||||
};
|
|
||||||
}, [myPlayerId]);
|
|
||||||
|
|
||||||
// Handle keyboard input - send to active channel
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
if (["w", "a", "s", "d"].includes(key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Only send if not already pressed (prevent repeat)
|
|
||||||
if (!keysPressed.current.has(key)) {
|
|
||||||
keysPressed.current.add(key);
|
|
||||||
|
|
||||||
// Send to active channel
|
|
||||||
const activeChannel = channelRef.current;
|
|
||||||
if (activeChannel && activeChannel.state === "joined") {
|
|
||||||
activeChannel.push("move", { direction: key });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
if (["w", "a", "s", "d"].includes(key)) {
|
|
||||||
keysPressed.current.delete(key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
window.addEventListener("keyup", handleKeyUp);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
window.removeEventListener("keyup", handleKeyUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<UserInput />
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100vw",
|
width: "100vw",
|
||||||
@@ -136,74 +16,10 @@ function App() {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Connection status */}
|
<ConnectionStatus />
|
||||||
<div
|
<BoardDisplay />
|
||||||
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>Status: {connectionStatus}</div>
|
|
||||||
<div>Players: {Object.keys(players).length}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Game canvas */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width: "800px",
|
|
||||||
height: "600px",
|
|
||||||
background: "#16213e",
|
|
||||||
margin: "50px auto",
|
|
||||||
border: "2px solid #0f3460",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Object.entries(players).map(([id, player]) => (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: player.x,
|
|
||||||
top: player.y,
|
|
||||||
width: "20px",
|
|
||||||
height: "20px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
background: id === myPlayerId ? "#e94560" : "#53a8b6",
|
|
||||||
border:
|
|
||||||
id === myPlayerId ? "3px solid #ff6b6b" : "2px solid #48d6e0",
|
|
||||||
transition: "all 0.1s linear",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
boxShadow:
|
|
||||||
id === myPlayerId ? "0 0 10px #e94560" : "0 0 5px #53a8b6",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{id === myPlayerId && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "-25px",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: "10px",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
You
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
client/src/contexts/GameChannelContext.tsx
Normal file
81
client/src/contexts/GameChannelContext.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { createContext, useEffect, useState, type ReactNode } from "react";
|
||||||
|
import { Channel } from "phoenix";
|
||||||
|
import { useWebSocketContext } from "./useWebSocketContext";
|
||||||
|
|
||||||
|
interface GameChannelContextValue {
|
||||||
|
channel: Channel | null;
|
||||||
|
channelStatus: string;
|
||||||
|
isJoined: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameChannelContext = createContext<GameChannelContextValue | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface GameChannelProviderProps {
|
||||||
|
channelName: string;
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameChannelProvider({
|
||||||
|
channelName,
|
||||||
|
params = {},
|
||||||
|
children,
|
||||||
|
}: GameChannelProviderProps) {
|
||||||
|
const { socket, isConnected } = useWebSocketContext();
|
||||||
|
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
||||||
|
const [isJoined, setIsJoined] = useState(false);
|
||||||
|
const [channel, setChannel] = useState<Channel | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket || !isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Joining channel: ${channelName}`);
|
||||||
|
const newChannel = socket.channel(channelName, params);
|
||||||
|
|
||||||
|
newChannel
|
||||||
|
.join()
|
||||||
|
.receive("ok", () => {
|
||||||
|
console.log(`✓ Joined channel: ${channelName}`);
|
||||||
|
setChannelStatus("joined");
|
||||||
|
setIsJoined(true);
|
||||||
|
})
|
||||||
|
.receive("error", (resp: unknown) => {
|
||||||
|
console.log(`✗ Failed to join ${channelName}:`, resp);
|
||||||
|
setChannelStatus("join failed");
|
||||||
|
setIsJoined(false);
|
||||||
|
})
|
||||||
|
.receive("timeout", () => {
|
||||||
|
console.log(`✗ Timeout joining ${channelName}`);
|
||||||
|
setChannelStatus("timeout");
|
||||||
|
setIsJoined(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set channel asynchronously to avoid cascading renders
|
||||||
|
setTimeout(() => setChannel(newChannel), 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log(`Leaving channel: ${channelName}`);
|
||||||
|
newChannel.leave();
|
||||||
|
setChannel(null);
|
||||||
|
setIsJoined(false);
|
||||||
|
};
|
||||||
|
}, [socket, isConnected, channelName, params]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameChannelContext.Provider
|
||||||
|
value={{
|
||||||
|
channel,
|
||||||
|
channelStatus,
|
||||||
|
isJoined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GameChannelContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GameChannelContext };
|
||||||
78
client/src/contexts/WebSocketContext.tsx
Normal file
78
client/src/contexts/WebSocketContext.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { createContext, useEffect, useState, type ReactNode } from "react";
|
||||||
|
import { Socket } from "phoenix";
|
||||||
|
|
||||||
|
interface WebSocketContextValue {
|
||||||
|
socket: Socket | null;
|
||||||
|
connectionStatus: string;
|
||||||
|
isConnected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebSocketContext = createContext<WebSocketContextValue | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface WebSocketProviderProps {
|
||||||
|
url: 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}`);
|
||||||
|
|
||||||
|
const newSocket = new Socket(url, {
|
||||||
|
timeout: 100,
|
||||||
|
reconnectAfterMs: (tries) =>
|
||||||
|
[
|
||||||
|
300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300,
|
||||||
|
300, 5000, 10000,
|
||||||
|
][tries - 1] || 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.onOpen(() => {
|
||||||
|
console.log(`✓ Connected to server`);
|
||||||
|
setConnectionStatus("Connected");
|
||||||
|
setIsConnected(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.onError((error) => {
|
||||||
|
console.error(`✗ Connection error:`, error);
|
||||||
|
setConnectionStatus("Connection error");
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.onClose(() => {
|
||||||
|
console.log(`✗ Disconnected from server`);
|
||||||
|
setConnectionStatus("Disconnected - reconnecting...");
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
newSocket.connect();
|
||||||
|
// Set socket state asynchronously to avoid cascading renders
|
||||||
|
setTimeout(() => setSocket(newSocket), 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newSocket.disconnect();
|
||||||
|
setSocket(null);
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebSocketContext.Provider
|
||||||
|
value={{
|
||||||
|
socket,
|
||||||
|
connectionStatus,
|
||||||
|
isConnected,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</WebSocketContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WebSocketContext };
|
||||||
10
client/src/contexts/useGameChannelContext.ts
Normal file
10
client/src/contexts/useGameChannelContext.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { GameChannelContext } from "./GameChannelContext";
|
||||||
|
|
||||||
|
export function useGameChannelContext() {
|
||||||
|
const context = useContext(GameChannelContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useGameChannel must be used within a GameChannelProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
10
client/src/contexts/useWebSocketContext.ts
Normal file
10
client/src/contexts/useWebSocketContext.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { WebSocketContext } from "./WebSocketContext";
|
||||||
|
|
||||||
|
export function useWebSocketContext() {
|
||||||
|
const context = useContext(WebSocketContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useWebSocket must be used within a WebSocketProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
89
client/src/game/BoardDisplay.tsx
Normal file
89
client/src/game/BoardDisplay.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useGameChannelContext } from "../contexts/useGameChannelContext";
|
||||||
|
|
||||||
|
interface Player {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameState {
|
||||||
|
[playerId: string]: Player;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BoardDisplay = () => {
|
||||||
|
const { channel, isJoined } = useGameChannelContext();
|
||||||
|
const [players, setPlayers] = useState<GameState>({});
|
||||||
|
const [myPlayerId, setMyPlayerId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!channel || !isJoined) return;
|
||||||
|
|
||||||
|
// Listen for game state updates
|
||||||
|
const ref = channel.on("game_state", (payload: { players: GameState }) => {
|
||||||
|
setPlayers(payload.players);
|
||||||
|
|
||||||
|
if (!myPlayerId && Object.keys(payload.players).length > 0) {
|
||||||
|
const playerIds = Object.keys(payload.players);
|
||||||
|
if (playerIds.length > 0) {
|
||||||
|
setMyPlayerId(playerIds[playerIds.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup listener on unmount
|
||||||
|
return () => {
|
||||||
|
channel.off("game_state", ref);
|
||||||
|
};
|
||||||
|
}, [channel, isJoined, myPlayerId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "800px",
|
||||||
|
height: "600px",
|
||||||
|
background: "#16213e",
|
||||||
|
margin: "50px auto",
|
||||||
|
border: "2px solid #0f3460",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(players).map(([id, player]) => (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: player.x,
|
||||||
|
top: player.y,
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: id === myPlayerId ? "#e94560" : "#53a8b6",
|
||||||
|
border:
|
||||||
|
id === myPlayerId ? "3px solid #ff6b6b" : "2px solid #48d6e0",
|
||||||
|
transition: "all 0.1s linear",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
boxShadow:
|
||||||
|
id === myPlayerId ? "0 0 10px #e94560" : "0 0 5px #53a8b6",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{id === myPlayerId && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-25px",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "10px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
client/src/game/ConnectionStatus.tsx
Normal file
26
client/src/game/ConnectionStatus.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useWebSocketContext } from "../contexts/useWebSocketContext";
|
||||||
|
import { useGameChannelContext } from "../contexts/useGameChannelContext";
|
||||||
|
|
||||||
|
export const ConnectionStatus = () => {
|
||||||
|
const { connectionStatus } = useWebSocketContext();
|
||||||
|
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>WebSocket: {connectionStatus}</div>
|
||||||
|
<div>Channel: {channelStatus}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
client/src/game/UserInput.tsx
Normal file
45
client/src/game/UserInput.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useGameChannelContext } from "../contexts/useGameChannelContext";
|
||||||
|
|
||||||
|
export const UserInput = () => {
|
||||||
|
const { channel } = useGameChannelContext();
|
||||||
|
const keysPressed = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (["w", "a", "s", "d"].includes(key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Only send if not already pressed (prevent repeat)
|
||||||
|
if (!keysPressed.current.has(key)) {
|
||||||
|
keysPressed.current.add(key);
|
||||||
|
|
||||||
|
// Send to active channel
|
||||||
|
if (channel.state === "joined") {
|
||||||
|
channel.push("move", { direction: key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
if (["w", "a", "s", "d"].includes(key)) {
|
||||||
|
keysPressed.current.delete(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [channel]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import App from './App.tsx'
|
import App from "./App.tsx";
|
||||||
|
import { GameChannelProvider } from "./contexts/GameChannelContext";
|
||||||
|
import { WebSocketProvider } from "./contexts/WebSocketContext.tsx";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
const WS_SERVER = "ws://localhost:4000/socket";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<WebSocketProvider url={WS_SERVER}>
|
||||||
|
<GameChannelProvider channelName="game:lobby">
|
||||||
<App />
|
<App />
|
||||||
|
</GameChannelProvider>
|
||||||
|
</WebSocketProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
);
|
||||||
|
|||||||
61
flake.lock
generated
61
flake.lock
generated
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1771369470,
|
|
||||||
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
39
flake.nix
39
flake.nix
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Development environment with pnpm and Elixir";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
# Elixir and Erlang
|
|
||||||
elixir
|
|
||||||
elixir-ls
|
|
||||||
|
|
||||||
# Node.js and pnpm
|
|
||||||
nodejs_22
|
|
||||||
pnpm
|
|
||||||
|
|
||||||
# Additional tools
|
|
||||||
git
|
|
||||||
inotify-tools
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
echo "🚀 Development environment loaded"
|
|
||||||
echo "Elixir version: $(elixir --version | head -n 1)"
|
|
||||||
echo "Node.js version: $(node --version)"
|
|
||||||
echo "pnpm version: $(pnpm --version)"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,9 @@ upstream phoenix_backend {
|
|||||||
# Note: Each new connection gets a new key, so reconnections may route differently
|
# Note: Each new connection gets a new key, so reconnections may route differently
|
||||||
hash $http_sec_websocket_key consistent;
|
hash $http_sec_websocket_key consistent;
|
||||||
|
|
||||||
# Failover configuration: mark server as down after 3 failed attempts within 30s
|
server phoenix1:4000 max_fails=1 fail_timeout=10s;
|
||||||
# Server will be retried after 30s
|
server phoenix2:4000 max_fails=1 fail_timeout=10s;
|
||||||
server phoenix1:4000 max_fails=1 fail_timeout=30s;
|
server phoenix3:4000 max_fails=1 fail_timeout=10s;
|
||||||
server phoenix2:4000 max_fails=1 fail_timeout=30s;
|
|
||||||
server phoenix3:4000 max_fails=1 fail_timeout=30s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|||||||
Reference in New Issue
Block a user