Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

23 players reconnection & auth validation #28

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"filepath": "{client,server}/**/.{js,jsx,ts,tsx,md,json}",
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 80,
"printWidth": 120,
"semi": true,
"singleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"]
Expand Down
4 changes: 2 additions & 2 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<body class="m-0 h-svh w-svw p-0">
<div id="root" class="h-full"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
11 changes: 4 additions & 7 deletions client/src/ConnectionProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,22 @@ function ConnectionProvider(props: { children: React.ReactNode }) {

// Create a socket connection
useEffect(() => {
if (socket.connected) return;
if (socket.connected || code === undefined) return;

socket.io.opts.query = { code };
socket.io.opts.query = { code, name: "Player", id: "0" };

socket.on("connect", () => console.log("Connected to server"));
socket.on("connect_error", (err) => console.error(err));
socket.on("disconnect", () => console.log("Disconnected from server"));
socket.on("info", (data) => console.info(data));
socket.connect();

return () => {
socket.disconnect();
};
}, [code]);

return (
<ConnContext.Provider value={{ socket }}>
{props.children}
</ConnContext.Provider>
);
return <ConnContext.Provider value={{ socket }}>{props.children}</ConnContext.Provider>;
}

export default ConnectionProvider;
18 changes: 18 additions & 0 deletions client/src/hooks/useKeyDown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useEffect } from "react";

export type Keys = "ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown";

export default function useKeyDown(handlers: Partial<Record<Keys, () => void>>): void {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const handler = handlers[e.key as Keys];
if (handler) handler();
};

window.addEventListener("keydown", handleKeyDown);

return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [handlers]);
}
100 changes: 100 additions & 0 deletions client/src/hooks/useSwipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type React from "react";
import { useCallback, useEffect, useRef } from "react";

type Point = {
x: number;
y: number;
};

interface Handlers {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
onSwipeUp?: () => void;
onSwipeDown?: () => void;
}

export default function useSwipe<E extends HTMLElement>(
handlers: Handlers,
sensiivity: number = 50
): React.RefCallback<E> {
const clientStart = useRef<Point>({ x: 0, y: 0 });
const clientEnd = useRef<Point>({ x: 0, y: 0 });

const ref = useRef<E | null>(null);

const handleStart = useCallback((x: number, y: number) => {
clientStart.current = { x, y };
}, []);

const handleMove = useCallback(
(x: number, y: number) => {
clientEnd.current = { x, y };

const diffX = clientEnd.current.x - clientStart.current.x;
const diffY = clientEnd.current.y - clientStart.current.y;

if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > sensiivity && handlers.onSwipeRight)
handlers.onSwipeRight();
else if (diffX < -sensiivity && handlers.onSwipeLeft)
handlers.onSwipeLeft();
} else {
if (diffY > sensiivity && handlers.onSwipeDown) handlers.onSwipeDown();
else if (diffY < -sensiivity && handlers.onSwipeUp)
handlers.onSwipeUp();
}
},
[handlers, sensiivity]
);

const handleMouseDown = useCallback(
(e: MouseEvent) => handleStart(e.clientX, e.clientY),
[handleStart]
);
const handleMouseUp = useCallback(
(e: MouseEvent) => handleMove(e.clientX, e.clientY),
[handleMove]
);

const handleTouchStart = useCallback(
(e: TouchEvent) => {
const touch = e.touches[0];

handleStart(touch.clientX, touch.clientY);
},
[handleStart]
);

const handleTouchEnd = useCallback(
(e: TouchEvent) => {
const touch = e.changedTouches[0];
handleMove(touch.clientX, touch.clientY);
},
[handleMove]
);

useEffect(() => {
if (!ref.current) return;

ref.current.addEventListener("touchstart", handleTouchStart);
ref.current.addEventListener("touchmove", handleTouchEnd);

return () => {
if (!ref.current) return;

ref.current.removeEventListener("touchstart", handleTouchStart);
ref.current.removeEventListener("touchmove", handleTouchEnd);
};
}, [
handlers,
handleTouchStart,
handleTouchEnd,
handleMouseDown,
handleMouseUp,
]);

return (el) => {
if (!el) return;
ref.current = el;
};
}
16 changes: 10 additions & 6 deletions client/src/pages/Game/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useContext, useEffect, useState } from "react";
import { ConnContext } from "@/ConnectionProvider/Context";
import { ConnContext } from "../../ConnectionProvider/Context";
import { Phases } from "@global/Game";
import Lobby from "@/pages/Lobby";

function Game() {
const { socket } = useContext(ConnContext);
Expand All @@ -12,11 +13,14 @@ function Game() {
}, [socket]);

// Render certain components based on the game phase
return (
<div>
<h1>Room is in phase {phase}</h1>
</div>
);

switch (phase) {
case Phases.LOBBY:
return <Lobby />;

default:
return <div>Placeholder for phase {phase}</div>;
}
}

export default Game;
55 changes: 55 additions & 0 deletions client/src/pages/Lobby/Character.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button, Input } from "@/components";

function Character() {
return (
<form className="flex h-full w-full flex-col justify-between p-4" onSubmit={(e) => e.preventDefault()}>
<div className="flex w-full flex-col gap-y-4">
<p className="text-justify">
Create your character and choose your preferred role (your choice will be considered in the drawing)
</p>

{/* Name input */}
<div className="flex flex-col">
<label htmlFor="name-input">Name</label>
<Input
className="rounded-lg border-2 border-neutral-800 p-1 outline-none"
type="text"
id="name-input"
placeholder="np. Jan Kowalski"
/>
</div>

{/* Profession input */}
<div className="flex flex-col">
<label htmlFor="profession-input">Profession</label>
<Input
className="rounded-lg border-2 border-neutral-800 p-1 outline-none"
type="text"
id="profession-input"
placeholder="np. Krawiec"
/>
</div>

{/* Description input */}
<div className="flex flex-col">
<label htmlFor="description-input">Description</label>
<Input type="text" id="description-input" placeholder="np. Zawsze wiedział, że ktoś go obserwuje." />
</div>

{/* Role selection */}
<div className="flex flex-col">
<label htmlFor="role-selection">Which role do you prefer?</label>

<select className="rounded-lg border-2 border-neutral-800 p-1 outline-none" name="" id="role-selection">
<option>Mafiosa</option>
<option>Citizen</option>
</select>
</div>
</div>

<Button size="button-lg">Draw my character</Button>
</form>
);
}

export default Character;
137 changes: 137 additions & 0 deletions client/src/pages/Lobby/Seat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Button } from "@/components";
import useKeyDown from "@/hooks/useKeyDown";
import { mod } from "@/utils/mod";
import { useCallback, useMemo, useState } from "react";

const calcArrowPosition = (p: number, i: number, l: number) => {
// Fun fact: python modulo operator is not the same as JS modulo operator
const f = Math.floor(p / l);
const a = mod(p, l);

if (Math.abs(a - i) > Math.floor(l / 2)) {
return (f + Math.sign(a - i)) * l + i;
} else {
return f * l + i;
}
};

function Seat({
selectSeat,
players,
yourSeat = null,
}: {
selectSeat(i: number): void;
players: string[];
yourSeat: number | null;
}) {
// Circle of seats
const seats = useMemo<({ id: number; name: string } | null)[]>(
() =>
new Array(players.length * 2 - (yourSeat !== null ? 2 : 0)).fill(null).map((_v, i) => {
const isFree = i % 2 === 0;
const playerId = Math.floor((i + 1) / 2);
if (yourSeat === null) return isFree ? null : { id: playerId - 1, name: players[playerId - 1] };

const edge = yourSeat * 2;
if (i == edge + 0) return { id: playerId, name: players[playerId] };
if (i == edge + 1) return { id: playerId, name: players[playerId] };

if (isFree) return null;
else {
const edgedId = playerId - 1 + (i >= edge ? 1 : 0);
return { id: edgedId, name: players[edgedId] };
}
}),
[players, yourSeat]
);

// Arrow position
const [position, setPosition] = useState<number>(0);
const handleArrow = useCallback((i: number) => setPosition((p) => calcArrowPosition(p, i, seats.length)), [seats]);
const arrow = useMemo(() => mod(position, seats.length), [position, seats]);

// paragraph
const playerLeft = seats[mod(arrow + 1, seats.length)];
const playerCent = seats[arrow];
const playerRight = seats[mod(arrow - 1, seats.length)];

// Keyboard navigation
useKeyDown({
ArrowUp: () => handleArrow(arrow - 1),
ArrowDown: () => handleArrow(arrow + 1),
});

return (
<div className="flex h-full flex-col justify-between p-4">
<p className="h-8 text-center">
{(() => {
if (playerCent) return `Here sits ${playerCent.name}`;

if (playerLeft && playerLeft === playerRight) return `Free seat opposite ${playerLeft.name}`;

return `Free seat, on the left sits ${playerLeft?.name} and on the right sits ${playerRight?.name}`;
})()}
</p>

<div className="relative flex h-1/2 w-full items-center justify-center">
<div className="aspect-square w-4/5 rounded-full bg-neutral-300" />

{/* Seats */}
{seats.map((player, i) => {
const slctSeat = i === arrow;
const slctYrSeat = player !== null && yourSeat === player.id;
return player !== null ? (
<div
key={player.name}
onMouseEnter={() => handleArrow(i)}
className="absolute flex aspect-square w-12 items-center justify-center rounded-full border-2 border-neutral-600 bg-neutral-300 p-2 transition-all"
style={{
transform:
`rotate(${i / seats.length}turn) translateX(120px)` +
(slctSeat ? "scale(1.5) translateX(50%) " : "") +
`rotate(${-i / seats.length}turn)`,
zIndex: 1 - (i % 2),
backgroundColor: slctSeat || slctYrSeat ? "white" : "",
}}
>
{player.name[0]}
</div>
) : (
<div
key={i + "seat"}
className="absolute flex aspect-square w-12 items-center justify-center"
style={{
transform: `rotate(${i / seats.length}turn) translateX(120px)`,
zIndex: 1 - (i % 2),
}}
onMouseEnter={() => handleArrow(i)}
onClick={() => selectSeat(seats[arrow + 1]?.id ?? 0)}
>
<div
className="aspect-square h-8 w-8 rounded-full border-2 border-neutral-400 transition-transform"
style={{
transform: slctSeat ? "scale(1.5) translateX(15px)" : "",
}}
/>
</div>
);
})}

{/* Arrow */}
<div
className="absolute z-0 h-[2px] w-[calc(120px)] origin-left bg-neutral-600 transition-all"
style={{
transform: `translateX(50%) rotate(${position / seats.length}turn)`,
}}
/>

{/* Clock face */}
<div className="absolute aspect-square h-2 rounded-full bg-neutral-800" />
</div>

<Button size="button-lg">Reserve your place</Button>
</div>
);
}

export default Seat;
Loading