implement ready up for players in a game lobby
This commit is contained in:
		@ -1,10 +1,11 @@
 | 
				
			|||||||
import { hasOnlyKeys, hasProperty } from "./validation";
 | 
					import { hasOnlyKeys, hasProperty } from "./validation";
 | 
				
			||||||
import { isId, type Id } from "./Id";
 | 
					import { isId, type Id } from "./Id";
 | 
				
			||||||
import type { State } from "./State";
 | 
					import type { State } from "./State";
 | 
				
			||||||
 | 
					import { isGamePlayer, type GamePlayer } from "./GamePlayer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface GameData {
 | 
					export interface GameData {
 | 
				
			||||||
	isStarted: boolean;
 | 
						isStarted: boolean;
 | 
				
			||||||
	players: { id: Id; username: string }[];
 | 
						players: GamePlayer[];
 | 
				
			||||||
	state: State;
 | 
						state: State;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -17,13 +18,7 @@ export function isGameData(target: unknown): target is GameData {
 | 
				
			|||||||
		const { players } = target as any;
 | 
							const { players } = target as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (const player of players) {
 | 
							for (const player of players) {
 | 
				
			||||||
			if (!isId(player.id)) {
 | 
								if (!isGamePlayer(player)) return false;
 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (!hasProperty(player, "username", "string")) {
 | 
					 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								src/lib/GamePlayer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/lib/GamePlayer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import { isId, type Id } from "./Id";
 | 
				
			||||||
 | 
					import { hasOnlyKeys, hasProperty } from "./validation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface GamePlayer {
 | 
				
			||||||
 | 
						id: Id;
 | 
				
			||||||
 | 
						username: string;
 | 
				
			||||||
 | 
						isReady: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isGamePlayer(target: unknown): target is GamePlayer {
 | 
				
			||||||
 | 
						if (!isId((target as any).id)) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "username", "string")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!hasProperty(target, "isReady", "boolean")) {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return hasOnlyKeys(target, ["username", "id", "isReady"]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,19 +1,29 @@
 | 
				
			|||||||
<script>
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { GamePlayer } from "$lib/GamePlayer";
 | 
				
			||||||
	import { getMeContext } from "$lib/meContext";
 | 
						import { getMeContext } from "$lib/meContext";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { players } = $props();
 | 
						const { players, withReadyStatus = false } = $props();
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const me = getMeContext();
 | 
						const me = getMeContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function getClasses(player: GamePlayer, withReadyStatus: boolean) {
 | 
				
			||||||
 | 
							let classes = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (withReadyStatus) {
 | 
				
			||||||
 | 
								classes += player.isReady ? "ready" : "unready";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (me !== null && player.id === me.id) {
 | 
				
			||||||
 | 
								classes += " you";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return classes;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="list">
 | 
					<div class="list">
 | 
				
			||||||
	<ol>
 | 
						<ol>
 | 
				
			||||||
		{#each players as player}
 | 
							{#each players as player}
 | 
				
			||||||
			{#if me !== null && player.id === me.id}
 | 
								<li class={getClasses(player, withReadyStatus)}>{player.username}</li>
 | 
				
			||||||
				<li class="you">you</li>
 | 
					 | 
				
			||||||
			{:else}
 | 
					 | 
				
			||||||
				<li>{player.username}</li>
 | 
					 | 
				
			||||||
			{/if}
 | 
					 | 
				
			||||||
		{/each}
 | 
							{/each}
 | 
				
			||||||
	</ol>
 | 
						</ol>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
@ -23,6 +33,18 @@
 | 
				
			|||||||
		font-weight: bold;
 | 
							font-weight: bold;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.ready::after {
 | 
				
			||||||
 | 
							content: " [ready]";
 | 
				
			||||||
 | 
							color: green;
 | 
				
			||||||
 | 
							font-weight: bold;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.unready::after {
 | 
				
			||||||
 | 
							content: " [unready]";
 | 
				
			||||||
 | 
							color: red;
 | 
				
			||||||
 | 
							font-weight: bold;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.list {
 | 
						.list {
 | 
				
			||||||
		border: 1pt gray solid;
 | 
							border: 1pt gray solid;
 | 
				
			||||||
		background: white;
 | 
							background: white;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,10 @@
 | 
				
			|||||||
import type { Id } from "../Id";
 | 
					import type { Id } from "../Id";
 | 
				
			||||||
import type { GameData } from "../GameData";
 | 
					import type { GameData } from "../GameData";
 | 
				
			||||||
import type { State } from "../State";
 | 
					import type { State } from "../State";
 | 
				
			||||||
 | 
					import type { GamePlayer } from "$lib/GamePlayer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Game implements GameData {
 | 
					export class Game implements GameData {
 | 
				
			||||||
	players: { id: Id; username: string }[];
 | 
						players: GamePlayer[];
 | 
				
			||||||
	isStarted: boolean;
 | 
						isStarted: boolean;
 | 
				
			||||||
	state: State;
 | 
						state: State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,8 +23,18 @@ export class Game implements GameData {
 | 
				
			|||||||
		return game;
 | 
							return game;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	addPlayer(id: Id, username: string) {
 | 
						addPlayer(player: GamePlayer) {
 | 
				
			||||||
		this.players.push({ id, username });
 | 
							this.players.push(player);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setPlayerReady({ id: playerId, isReady }: GamePlayer) {
 | 
				
			||||||
 | 
							const player = this.players.find(({ id }) => playerId === id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!player) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							player.isReady = isReady;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return player;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	start() {
 | 
						start() {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@ import type { LocalCredentials } from "./auth";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export enum ResourceId {
 | 
					export enum ResourceId {
 | 
				
			||||||
	Game = "gameid",
 | 
						Game = "gameid",
 | 
				
			||||||
 | 
						Turn = "turnid",
 | 
				
			||||||
 | 
						Player = "playerid",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getUser(locals: { user: LocalCredentials }) {
 | 
					export function getUser(locals: { user: LocalCredentials }) {
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,10 @@ export function createdResponse(id: string) {
 | 
				
			|||||||
	return Response.json({ item: id }, { status: 201 });
 | 
						return Response.json({ item: id }, { status: 201 });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function conflictResponse() {
 | 
				
			||||||
 | 
						return Response.json({ error: "Conflict" }, { status: 409 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function tokenResponse(token: string) {
 | 
					export function tokenResponse(token: string) {
 | 
				
			||||||
	return Response.json({ access_token: token });
 | 
						return Response.json({ access_token: token });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -13,9 +13,9 @@ describe("Game", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			equal(game.players.length, 0);
 | 
								equal(game.players.length, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			game.addPlayer(idString, user);
 | 
								game.addPlayer({ id: idString, username: user, isReady: false });
 | 
				
			||||||
			equal(game.players.length, 1);
 | 
								equal(game.players.length, 1);
 | 
				
			||||||
			deepEqual(game.players[0], { id: idString, username: user });
 | 
								deepEqual(game.players[0], { id: idString, username: user, isReady: false });
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -30,12 +30,25 @@ describe("GameData", () => {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		it("rejects an object with a malformed players array", () => {
 | 
							it("rejects an object with a malformed players array", () => {
 | 
				
			||||||
			const data: unknown = {
 | 
								let data: unknown = {
 | 
				
			||||||
				players: [{ id: idString }],
 | 
									players: [{ id: idString, username: "Mr. User" }],
 | 
				
			||||||
				state: {},
 | 
									state: {},
 | 
				
			||||||
				isStarted: false,
 | 
									isStarted: false,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
								equal(isGameData(data), false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								data = {
 | 
				
			||||||
 | 
									players: [{ id: idString, isReady: false }],
 | 
				
			||||||
 | 
									state: {},
 | 
				
			||||||
 | 
									isStarted: false,
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
								equal(isGameData(data), false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								data = {
 | 
				
			||||||
 | 
									players: [{ username: "Mr. User", isReady: false }],
 | 
				
			||||||
 | 
									state: {},
 | 
				
			||||||
 | 
									isStarted: false,
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
			equal(isGameData(data), false);
 | 
								equal(isGameData(data), false);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -50,7 +63,7 @@ describe("GameData", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		it("rejects an object with extra properties", () => {
 | 
							it("rejects an object with extra properties", () => {
 | 
				
			||||||
			const data: unknown = {
 | 
								const data: unknown = {
 | 
				
			||||||
				players: [{ username: "Mr. User", id: idString }],
 | 
									players: [{ username: "Mr. User", id: idString, isReady: false }],
 | 
				
			||||||
				isStarted: false,
 | 
									isStarted: false,
 | 
				
			||||||
				state: {},
 | 
									state: {},
 | 
				
			||||||
				extra: true,
 | 
									extra: true,
 | 
				
			||||||
@ -61,7 +74,7 @@ describe("GameData", () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		it("should accept a proper GameData object", () => {
 | 
							it("should accept a proper GameData object", () => {
 | 
				
			||||||
			const data: unknown = {
 | 
								const data: unknown = {
 | 
				
			||||||
				players: [{ username: "Mr. User", id: idString }],
 | 
									players: [{ username: "Mr. User", id: idString, isReady: false }],
 | 
				
			||||||
				state: {},
 | 
									state: {},
 | 
				
			||||||
				isStarted: false,
 | 
									isStarted: false,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
				
			|||||||
@ -58,11 +58,13 @@ describe("Game Events", () => {
 | 
				
			|||||||
		const playerOne = {
 | 
							const playerOne = {
 | 
				
			||||||
			id: createId(),
 | 
								id: createId(),
 | 
				
			||||||
			username: "Player One",
 | 
								username: "Player One",
 | 
				
			||||||
 | 
								isReady: false,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const playerTwo = {
 | 
							const playerTwo = {
 | 
				
			||||||
			id: createId(),
 | 
								id: createId(),
 | 
				
			||||||
			username: "Player Two",
 | 
								username: "Player Two",
 | 
				
			||||||
 | 
								isReady: false,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		it("should throw if the kind is unkown", () => {
 | 
							it("should throw if the kind is unkown", () => {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										60
									
								
								src/routes/api/games/[gameid]/players/[playerid]/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/routes/api/games/[gameid]/players/[playerid]/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import type { GameData } from "$lib/GameData";
 | 
				
			||||||
 | 
					import { isGamePlayer, type GamePlayer } from "$lib/GamePlayer";
 | 
				
			||||||
 | 
					import { isListing } from "$lib/Listing";
 | 
				
			||||||
 | 
					import { Game } from "$lib/server/Game";
 | 
				
			||||||
 | 
					import { updateListing } from "$lib/server/modifyListing";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						readListingById,
 | 
				
			||||||
 | 
						ServerCollections,
 | 
				
			||||||
 | 
						writeUpdatedListing,
 | 
				
			||||||
 | 
					} from "$lib/server/mongo";
 | 
				
			||||||
 | 
					import { getBody, getParam, ResourceId } from "$lib/server/requestTools";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						badRequestResponse,
 | 
				
			||||||
 | 
						conflictResponse,
 | 
				
			||||||
 | 
						notFoundResponse,
 | 
				
			||||||
 | 
						singleResponse,
 | 
				
			||||||
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
 | 
					import type { RequestHandler } from "@sveltejs/kit";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PUT: RequestHandler = async ({ params, request }): Promise<Response> => {
 | 
				
			||||||
 | 
						const id = getParam(params, ResourceId.Game);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (id === null) {
 | 
				
			||||||
 | 
							return badRequestResponse("missing playerid parameter");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let player: GamePlayer | null;
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							player = await getBody(request, isGamePlayer);
 | 
				
			||||||
 | 
						} catch (err) {
 | 
				
			||||||
 | 
							return badRequestResponse("missing player body");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!player) {
 | 
				
			||||||
 | 
							return badRequestResponse("malformed request");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const listing = await readListingById(ServerCollections.Games, id, isListing<GameData>);
 | 
				
			||||||
 | 
						if (!listing) {
 | 
				
			||||||
 | 
							return notFoundResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (listing.data.isStarted === true) {
 | 
				
			||||||
 | 
							return conflictResponse();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const game = Game.from(listing.data);
 | 
				
			||||||
 | 
						if (game.setPlayerReady(player) === null) return notFoundResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: there's a potential race condition here where some player is unreadying as this
 | 
				
			||||||
 | 
						//       function is running. This should do some kind of check in MongoDB to make sure
 | 
				
			||||||
 | 
						//       all players are ready if it's starting the game. For now: good enough.
 | 
				
			||||||
 | 
						if (game.players.every(({ isReady }) => isReady)) {
 | 
				
			||||||
 | 
							game.start();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await writeUpdatedListing(ServerCollections.Games, updateListing(listing, game));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return singleResponse(game);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -12,7 +12,6 @@ import {
 | 
				
			|||||||
import { getBody, getParam, getUser, ResourceId } from "$lib/server/requestTools";
 | 
					import { getBody, getParam, getUser, ResourceId } from "$lib/server/requestTools";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	badRequestResponse,
 | 
						badRequestResponse,
 | 
				
			||||||
	createdResponse,
 | 
					 | 
				
			||||||
	notFoundResponse,
 | 
						notFoundResponse,
 | 
				
			||||||
	singleResponse,
 | 
						singleResponse,
 | 
				
			||||||
} from "$lib/server/responseBodies";
 | 
					} from "$lib/server/responseBodies";
 | 
				
			||||||
@ -23,9 +23,7 @@
 | 
				
			|||||||
	<div class="game-listing">
 | 
						<div class="game-listing">
 | 
				
			||||||
		<div>{prettyDate(new Date(game.createdAt))}</div>
 | 
							<div>{prettyDate(new Date(game.createdAt))}</div>
 | 
				
			||||||
		<div>
 | 
							<div>
 | 
				
			||||||
			{#each games as game}
 | 
								<PlayerList players={game.data.players} />
 | 
				
			||||||
				<PlayerList players={game.data.players} />
 | 
					 | 
				
			||||||
			{/each}
 | 
					 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<form method="GET" action={`/games/${game.id}`}>
 | 
							<form method="GET" action={`/games/${game.id}`}>
 | 
				
			||||||
			<input type="submit" value="JOIN" />
 | 
								<input type="submit" value="JOIN" />
 | 
				
			||||||
@ -40,20 +38,20 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	.game-listing {
 | 
						.game-listing {
 | 
				
			||||||
		display: flex;
 | 
							display: flex;
 | 
				
			||||||
		gap: 3rem;
 | 
							gap: 1rem;
 | 
				
			||||||
		padding: 0.5rem;
 | 
							padding: 0.5rem;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.game-listing > div {
 | 
						.game-listing > * {
 | 
				
			||||||
		flex: 1 1 auto;
 | 
							flex: auto;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form {
 | 
						.game-listing > div:first-child {
 | 
				
			||||||
		display: flex;
 | 
							max-width: 12rem;
 | 
				
			||||||
		flex: 1 1 auto;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	form input {
 | 
						form input {
 | 
				
			||||||
		width: 100%;
 | 
							width: 100%;
 | 
				
			||||||
 | 
							height: 100%;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,13 +4,14 @@ import { isServerResponse } from "$lib/ServerResponse";
 | 
				
			|||||||
import { error } from "@sveltejs/kit";
 | 
					import { error } from "@sveltejs/kit";
 | 
				
			||||||
import type { PageServerLoad } from "./$types";
 | 
					import type { PageServerLoad } from "./$types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load: PageServerLoad = async ({ fetch, params, cookies }) => {
 | 
					export const load: PageServerLoad = async ({ fetch, params, cookies, depends }) => {
 | 
				
			||||||
	const url = `/api/games/${params.gameid}`;
 | 
						const url = `/api/games/${params.gameid}`;
 | 
				
			||||||
	let res: Response;
 | 
						let res: Response;
 | 
				
			||||||
	let body: unknown;
 | 
						let body: unknown;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const token = cookies.get("access_token");
 | 
						const token = cookies.get("access_token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						depends(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!token) {
 | 
						if (!token) {
 | 
				
			||||||
		error(401);
 | 
							error(401);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -31,7 +32,10 @@ export const load: PageServerLoad = async ({ fetch, params, cookies }) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ("item" in body && isListing<GameData>(body.item, isGameData)) {
 | 
						if ("item" in body && isListing<GameData>(body.item, isGameData)) {
 | 
				
			||||||
		return { game: body.item };
 | 
							return {
 | 
				
			||||||
 | 
								game: body.item,
 | 
				
			||||||
 | 
								token,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		error(res.status, "unable to fetch game data");
 | 
							error(res.status, "unable to fetch game data");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,30 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { invalidate } from "$app/navigation";
 | 
				
			||||||
	import PlayerList from "$lib/components/PlayerList.svelte";
 | 
						import PlayerList from "$lib/components/PlayerList.svelte";
 | 
				
			||||||
 | 
						import type { Me } from "$lib/me";
 | 
				
			||||||
	import type { PageData } from "./$types";
 | 
						import type { PageData } from "./$types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let { data: page }: { data: PageData } = $props();
 | 
						let { data: page }: { data: PageData } = $props();
 | 
				
			||||||
	const handleSubmit = () => {};
 | 
						function getHandler(gameId: string, me: Me) {
 | 
				
			||||||
 | 
							return async () => {
 | 
				
			||||||
 | 
								const playerIndex = page.game.data.players.findIndex(
 | 
				
			||||||
 | 
									({ id }) => id === page.me?.id,
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								const target = page.game.data.players[playerIndex];
 | 
				
			||||||
 | 
								const update = { ...target, isReady: !target.isReady };
 | 
				
			||||||
 | 
								let response = await fetch(`/api/games/${gameId}/players/${me.id}`, {
 | 
				
			||||||
 | 
									method: "PUT",
 | 
				
			||||||
 | 
									headers: [["Authorization", page.token]],
 | 
				
			||||||
 | 
									body: JSON.stringify(update),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (response.status !== 200) {
 | 
				
			||||||
 | 
									throw new Error("unable to set ready");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								invalidate(`/api/games/${gameId}`);
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if page.game.data.isStarted}
 | 
					{#if page.game.data.isStarted}
 | 
				
			||||||
@ -11,12 +33,19 @@
 | 
				
			|||||||
	<h1>This is some lobby</h1>
 | 
						<h1>This is some lobby</h1>
 | 
				
			||||||
	<div id="lobby">
 | 
						<div id="lobby">
 | 
				
			||||||
		<div id="players">
 | 
							<div id="players">
 | 
				
			||||||
			<PlayerList players={page.game.data.players} />
 | 
								<div id="player-list">
 | 
				
			||||||
 | 
									<PlayerList players={page.game.data.players} withReadyStatus />
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<input
 | 
				
			||||||
 | 
									id="ready"
 | 
				
			||||||
 | 
									type="button"
 | 
				
			||||||
 | 
									value="Set Ready"
 | 
				
			||||||
 | 
									onclick={getHandler(page.game.id, page.me!)}
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div id="chat" style="background: palevioletred">
 | 
				
			||||||
 | 
								<span>Chat Feature Under Construction</span>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div id="chat" style="background: pink"></div>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
	<div id="controls">
 | 
					 | 
				
			||||||
		<input type="button" value="Start Game" onsubmit={handleSubmit} />
 | 
					 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,14 +59,30 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	#players {
 | 
						#players {
 | 
				
			||||||
		flex: 1;
 | 
							flex: 1;
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							gap: 1rem;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						#player-list {
 | 
				
			||||||
 | 
							flex: auto;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	#chat {
 | 
						#chat {
 | 
				
			||||||
		flex: 2 2;
 | 
							flex: 2 2;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	#controls {
 | 
						#ready {
 | 
				
			||||||
		margin: 1rem 0;
 | 
							height: 4rem;
 | 
				
			||||||
		text-align: right;
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						#chat span {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							align-items: center;
 | 
				
			||||||
 | 
							justify-content: center;
 | 
				
			||||||
 | 
							height: 100%;
 | 
				
			||||||
 | 
							color: white;
 | 
				
			||||||
 | 
							font-weight: bold;
 | 
				
			||||||
 | 
							text-transform: uppercase;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user