diff --git a/v2/fbg-web/infra/hooks/useCredential.ts b/v2/fbg-web/infra/hooks/useCredential.ts new file mode 100644 index 000000000..5ffd86f41 --- /dev/null +++ b/v2/fbg-web/infra/hooks/useCredential.ts @@ -0,0 +1,66 @@ +import { useState, useEffect } from "react"; + +export interface Credential { + playerID: string; + playerCredentials: string; +} + +export interface FbgCredentialResult { + loaded: boolean; + credential?: Credential; + error?: string; +} + +export interface FbgCredentialInput { + hostname: string; + serverId: number; + gameId: string; + roomId: string; + nickname: string; +} + +export function useCredential(input: FbgCredentialInput): FbgCredentialResult { + const initialState: FbgCredentialResult = { loaded: false }; + const [credential, setCredential] = useState(initialState); + useEffect(() => { + const savedCredential = localStorage.getItem(getKey(input)); + if (savedCredential) { + const credential: Credential = JSON.parse(savedCredential); + setCredential({ loaded: true, credential }); + return; + } + join(input).then( + (credential) => { + setCredential({ loaded: true, credential }); + }, + (error: any) => { + setCredential({ loaded: true, error: `${error}` }); + } + ); + }); + return credential; +} + +function getKey(input: FbgCredentialInput) { + return `credential-${input.serverId}-${input.roomId}`; +} + +async function join(input: FbgCredentialInput): Promise { + const protocol = location.protocol || "http:"; + const url = `${protocol}//${input.hostname}/games/${input.gameId}/${input.roomId}/join`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + playerID: "1", // TODO new to detect next playerID + playerName: input.nickname, + }), + }); + if (!response.ok) { + const text = await response.text(); + throw new Error(`Join failed: ${response.status}, ${text}`); + } + return (await response.json()) as Credential; +} diff --git a/v2/fbg-web/infra/hooks/useLogin.ts b/v2/fbg-web/infra/hooks/useLogin.ts index bc5250aa8..9400c7956 100644 --- a/v2/fbg-web/infra/hooks/useLogin.ts +++ b/v2/fbg-web/infra/hooks/useLogin.ts @@ -1,27 +1,35 @@ import { useState, useEffect } from "react"; -const LOCALSTORAGE_KEY = "fbgNickname2"; - export interface FbgLogin { - loaded: boolean; - loggedIn: boolean; + resolved: boolean; nickname?: string; } +const LOCALSTORAGE_KEY = "fbgNickname2"; + +function getNickname(): string | null { + const savedNickname = localStorage.getItem(LOCALSTORAGE_KEY); + return savedNickname; +} + +function setNickname(nickname: string) { + localStorage.setItem(LOCALSTORAGE_KEY, nickname); +} export function useLogin(): [FbgLogin, (nickname: string) => void] { - const initialState: FbgLogin = { loaded: false, loggedIn: false }; + const initialState: FbgLogin = { resolved: false }; const [login, setLogin] = useState(initialState); useEffect(() => { - const savedNickname = localStorage.getItem(LOCALSTORAGE_KEY); + const savedNickname = getNickname(); if (savedNickname) { - setLogin({ loaded: true, loggedIn: true, nickname: savedNickname }); + setLogin({ resolved: true, nickname: savedNickname }); } else { - setLogin({ loaded: true, loggedIn: false }); + setLogin({ resolved: true }); } + return () => {}; }, []); const setAndSaveLogin = (nickname: string) => { - localStorage.setItem(LOCALSTORAGE_KEY, nickname); - setLogin({ loaded: true, loggedIn: true, nickname }); + setNickname(nickname); + setLogin({ resolved: true, nickname }); }; return [login, setAndSaveLogin]; } diff --git a/v2/fbg-web/infra/hooks/useNewRoom.ts b/v2/fbg-web/infra/hooks/useNewRoom.ts index 45ec93fe9..b2c74c91a 100644 --- a/v2/fbg-web/infra/hooks/useNewRoom.ts +++ b/v2/fbg-web/infra/hooks/useNewRoom.ts @@ -1,47 +1,44 @@ +import { FbgNewRoomResult, getNewRoom } from "infra/promises/getNewRoom"; import { useState, useEffect } from "react"; -export interface FbgNewRoomResult { - loaded: boolean; - success?: boolean; - roomId?: string; +export interface NewRoomState { + resolved: boolean; + result?: FbgNewRoomResult; + error?: string; } -export interface FbgNewRoomInput { - hostname: string; +export function useNewRoom({ + gameId, + numPlayers, + nickname, +}: { gameId: string; - nickname: string; numPlayers: number; -} - -export function useNewRoom(): [ - FbgNewRoomResult, - (input: FbgNewRoomInput) => void -] { - const initialState: FbgNewRoomResult = { loaded: false }; + nickname?: string; +}): NewRoomState { + const initialState: NewRoomState = { resolved: false }; const [newRoom, setNewRoom] = useState(initialState); - const createNewRoom = (input: FbgNewRoomInput) => { - const data = { numPlayers: input.numPlayers }; - fetch( - `${location.protocol}//${input.hostname}/games/${input.gameId}/create`, - { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, + useEffect(() => { + if (!nickname) { + return; + } + const abortController = new AbortController(); + getNewRoom({ + signal: abortController.signal, + gameId, + numPlayers, + nickname, + }).then( + (result) => { + setNewRoom({ resolved: true, result }); + }, + (error) => { + setNewRoom({ resolved: true, error: error.toString() }); } - ) - .then((response) => response.json()) - .then( - (responseJson) => { - const roomId = responseJson["matchID"]; - setNewRoom({ loaded: true, success: true, roomId }); - }, - (error) => { - console.error(error); - setNewRoom({ loaded: true, success: false }); - } - ); - }; - return [newRoom, createNewRoom]; + ); + return () => { + abortController.abort(); + }; + }, [nickname]); + return newRoom; } diff --git a/v2/fbg-web/infra/hooks/useRoom.ts b/v2/fbg-web/infra/hooks/useRoom.ts new file mode 100644 index 000000000..d5abc6f6a --- /dev/null +++ b/v2/fbg-web/infra/hooks/useRoom.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; +import { useCredential } from "./useCredential"; + +export interface FbgRoomResult { + loaded: boolean; + credential: any; + matchStarted?: boolean; +} + +export interface FbgRoomInput { + hostname: string; + nickname: string; + serverId: number; + gameId: string; + roomId: string; +} + +export function useRoom(input: FbgRoomInput): FbgRoomResult { + const credential = useCredential(input); + return { loaded: true, matchStarted: false, credential }; +} diff --git a/v2/fbg-web/infra/hooks/useServer.ts b/v2/fbg-web/infra/hooks/useServer.ts deleted file mode 100644 index d3c9919bf..000000000 --- a/v2/fbg-web/infra/hooks/useServer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { useState, useEffect } from "react"; - -const FBG_SERVERS = [null, "charizard", "pikachu"]; - -const LOCAL_SERVERS = wrapWithIndex([null, "localhost:8001"]); - -export interface FbgServer { - resolved: boolean; - serversDown?: string[]; - hostname?: string; - index?: number; -} - -export function useServer(i?: number): FbgServer { - const initialState: FbgServer = { resolved: false }; - const [server, setServer] = useState(initialState); - - useEffect(() => { - (async () => { - let serverList = getServerList(); - if (i) { - setServer(getFbgServer(serverList[i])); - return; - } - serverList.sort(() => Math.random() - 0.5); - for (const server of serverList) { - if (await isServerUp(server.hostname)) { - setServer(getFbgServer(server)); - return; - } - } - serverList.sort(); - const serversDown = serverList - .filter((x) => x.hostname !== null) - .map((x) => x.hostname!); - setServer({ resolved: true, serversDown }); - })(); - return () => {}; - }, []); - - return server; -} - -function getFbgServer(server: { - hostname: string | null; - index: number; -}): FbgServer { - const hostname = server.hostname!; - const index = server.index; - return { resolved: true, hostname, index }; -} - -function getServerList() { - if (!location) { - return LOCAL_SERVERS; - } - if (!location.hostname) { - return LOCAL_SERVERS; - } - if (location.hostname.toLowerCase() != "www.freeboardgames.org") { - return LOCAL_SERVERS; - } - return wrapWithIndex( - FBG_SERVERS.map((x) => (x ? `${x}.freeboardgames.org:8001` : null)) - ); -} - -function wrapWithIndex( - serverList: (string | null)[] -): { hostname: string | null; index: number }[] { - return serverList.map((hostname, index) => ({ hostname, index })); -} - -async function isServerUp(hostname: string | null): Promise { - if (!hostname) { - return false; - } - const protocol = location.protocol || "http:"; - let response; - try { - response = await fetch(`${protocol}//${hostname}/open`); - } catch (e) { - return false; - } - if (!response.ok) { - return false; - } - const text = await response.text(); - return text === "open"; -} diff --git a/v2/fbg-web/infra/promises/getNewRoom.ts b/v2/fbg-web/infra/promises/getNewRoom.ts new file mode 100644 index 000000000..844460032 --- /dev/null +++ b/v2/fbg-web/infra/promises/getNewRoom.ts @@ -0,0 +1,37 @@ +import { getServer } from "./getServer"; + +export interface FbgNewRoomResult { + roomID: string; + serverIndex: number; +} + +export interface FbgNewRoomInput { + signal: AbortSignal; + gameId: string; + nickname: string; + numPlayers: number; +} + +export async function getNewRoom( + input: FbgNewRoomInput +): Promise { + const server = await getServer(input.signal); + const data = { numPlayers: input.numPlayers, unlisted: true }; + const response = await fetch( + `${location.protocol}//${server.hostname}/games/${input.gameId}/create`, + { + signal: input.signal, + method: "POST", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + } + ); + if (!response.ok) { + throw new Error(`New Room failed: ${response.status}, ${response.body}`); + } + const json = await response.json(); + const roomID = json["matchID"] as string; + return { roomID, serverIndex: server.index }; +} diff --git a/v2/fbg-web/infra/promises/getServer.ts b/v2/fbg-web/infra/promises/getServer.ts new file mode 100644 index 000000000..00cdb0803 --- /dev/null +++ b/v2/fbg-web/infra/promises/getServer.ts @@ -0,0 +1,76 @@ +const FBG_SERVERS = [null, "charizard", "pikachu"]; + +const LOCAL_SERVERS = wrapWithIndex([null, "localhost:8001"]); + +export interface FbgServer { + hostname: string; + index: number; +} + +export function getServerByIndex(i: number) { + let serverList = getServerList(); + return convertServer(serverList[i]); +} + +export async function getServer(signal: AbortSignal): Promise { + let serverList = getServerList(); + serverList.sort(() => Math.random() - 0.5); + for (const server of serverList) { + if (await isServerUp(signal, server.hostname)) { + return convertServer(server); + } + } + serverList.sort(); + throw new Error("All servers are offline."); +} + +function convertServer(server: { + hostname: string | null; + index: number; +}): FbgServer { + const hostname = server.hostname!; + const index = server.index; + return { hostname, index }; +} + +function getServerList() { + if (!location) { + return LOCAL_SERVERS; + } + if (!location.hostname) { + return LOCAL_SERVERS; + } + if (location.hostname.toLowerCase() != "www.freeboardgames.org") { + return LOCAL_SERVERS; + } + return wrapWithIndex( + FBG_SERVERS.map((x) => (x ? `${x}.freeboardgames.org:8001` : null)) + ); +} + +function wrapWithIndex( + serverList: (string | null)[] +): { hostname: string | null; index: number }[] { + return serverList.map((hostname, index) => ({ hostname, index })); +} + +async function isServerUp( + signal: AbortSignal, + hostname: string | null +): Promise { + if (!hostname) { + return false; + } + const protocol = location.protocol || "http:"; + let response; + try { + response = await fetch(`${protocol}//${hostname}/open`, { signal }); + } catch (e) { + return false; + } + if (!response.ok) { + return false; + } + const text = await response.text(); + return text === "open"; +} diff --git a/v2/fbg-web/infra/settings/CustomizationBar.tsx b/v2/fbg-web/infra/settings/CustomizationBar.tsx index dc6d8a277..92b0d361e 100644 --- a/v2/fbg-web/infra/settings/CustomizationBar.tsx +++ b/v2/fbg-web/infra/settings/CustomizationBar.tsx @@ -28,6 +28,7 @@ const CustomizationBar = function (props: CustomizationBarProps) { EMPTY_FULL_GAME_CUSTOMIZATION_STATE ); const [showDialog, setShowDialog] = useState(false); + // TODO: refactor this into useCustomization hook useEffect(() => { setCustomizationState(getGameCustomization(props.gameId)); }, []); diff --git a/v2/fbg-web/pages/[lang]/[playVerb]/[gameCode]/online/index.tsx b/v2/fbg-web/pages/[lang]/[playVerb]/[gameCode]/online/index.tsx index af868426f..c9a2a84fc 100644 --- a/v2/fbg-web/pages/[lang]/[playVerb]/[gameCode]/online/index.tsx +++ b/v2/fbg-web/pages/[lang]/[playVerb]/[gameCode]/online/index.tsx @@ -6,7 +6,6 @@ import { } from "infra/misc/gameStaticPaths"; import { getGameIdFromCode } from "infra/i18n/I18nGetGameId"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { useServer } from "infra/hooks/useServer"; import { useLogin } from "infra/hooks/useLogin"; import { NicknamePrompt } from "infra/widgets/NicknamePrompt"; import { FreeBoardGamesBar } from "fbg-games/gamesShared/components/fbg/FreeBoardGamesBar"; @@ -23,44 +22,37 @@ interface NewRoomProps { } const NewRoom: NextPage = function (props: NewRoomProps) { - const server = useServer(); const [login, setLogin] = useLogin(); - const [newRoom, createNewRoom] = useNewRoom(); - if (!server.resolved) { + const newRoom = useNewRoom({ + nickname: login.nickname, + numPlayers: 2, + gameId: props.gameId, + }); + if (!login.resolved) { return ; } - if (server.serversDown) { - return ( - - ); - } - if (!login.loaded) { - return ; - } - if (!login.loggedIn) { + if (!login.nickname) { return ( ); } - const nickname = login.nickname!; - if (!newRoom.loaded) { - const hostname = server.hostname!; - const gameId = props.gameId; - const numPlayers = 2; // TODO: FIX THIS - createNewRoom({ nickname, gameId, hostname, numPlayers }); + if (!newRoom.resolved) { return ; } - if (!newRoom.success || !newRoom.roomId) { - return ; + if (newRoom.error || !newRoom.result) { + return ( + + ); } - Router.replace( - `/${props.params.lang}/room?s=${server.index}&i=${newRoom.roomId}` - ); + Router.replace({ + pathname: `/${props.params.lang}/room`, + query: { s: newRoom.result.serverIndex, i: newRoom.result.roomID }, + }); return ; }; diff --git a/v2/fbg-web/pages/[lang]/match/index.tsx b/v2/fbg-web/pages/[lang]/match/index.tsx new file mode 100644 index 000000000..52d071f1e --- /dev/null +++ b/v2/fbg-web/pages/[lang]/match/index.tsx @@ -0,0 +1,36 @@ +import languages from "../../../public/locales/languages.json"; +import type { NextPage } from "next"; +import { useRouter } from "next/router"; + +interface MatchProps { + lang: string; +} + +const Match: NextPage = function (props: MatchProps) { + const router = useRouter(); + const s = router.query.s ? parseInt(router.query.s as string) : undefined; + const i = router.query.i; + // Do not require login to watch a match. + return ( +

+ <> + MATCH lang: {props.lang} i: {i} nickname:{" "} + +

+ ); +}; + +export async function getStaticProps({ + params, +}: { + params: { lang: string }; +}): Promise<{ props: { lang: string } }> { + return { props: { lang: params.lang } }; +} + +export async function getStaticPaths() { + const paths = languages.map((lang: string) => ({ params: { lang } })); + return { paths, fallback: false }; +} + +export default Match; diff --git a/v2/fbg-web/pages/[lang]/room/index.tsx b/v2/fbg-web/pages/[lang]/room/index.tsx new file mode 100644 index 000000000..8ff13c1a8 --- /dev/null +++ b/v2/fbg-web/pages/[lang]/room/index.tsx @@ -0,0 +1,58 @@ +import languages from "../../../public/locales/languages.json"; +import type { NextPage } from "next"; + +interface RoomProps { + lang: string; +} + +const Room: NextPage = function (props: RoomProps) { + /*const router = useRouter(); + const s = router.query.s ? parseInt(router.query.s as string) : undefined; + const i = router.query.i; + const server = useServer(s); + const [login, setLogin] = useLogin(); + const room = useRoom({ + gameId: 'tictactoe', // TODO: use await getGameIdFromCode(lang, gameCode); + roomId: router.query.i as string, + nickname: login.nickname!, + serverId: server.index!, + hostname: server.hostname! + }); + if (!server.resolved || !login.loaded) { + return ; + } + if (!login.loggedIn) { + return ( + + + + ); + } + if (room.matchStarted) { + Router.replace({ + pathname: `/${props.lang}/match`, + query: { s: router.query.s, i: router.query.i }, + }); + return ; + }*/ + return ( +

+
ROOM lang: {props.lang}
+

+ ); +}; + +export async function getStaticProps({ + params, +}: { + params: { lang: string }; +}): Promise<{ props: { lang: string } }> { + return { props: { lang: params.lang } }; +} + +export async function getStaticPaths() { + const paths = languages.map((lang: string) => ({ params: { lang } })); + return { paths, fallback: false }; +} + +export default Room;