diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 0000000..a37597c --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "exportall.config.relExclusion": ["/src/ui/components/ErrorModal"] +} diff --git a/client/src/game/scripts/scenes/preloadScene.ts b/client/src/game/scripts/scenes/preloadScene.ts index 27a0bbc..c7ca4ba 100644 --- a/client/src/game/scripts/scenes/preloadScene.ts +++ b/client/src/game/scripts/scenes/preloadScene.ts @@ -144,6 +144,13 @@ export default class PreloadScene extends Phaser.Scene { }); this.scene.start("MainScene", { channel }); }); + // channel.onDisconnect(() => { + // this.game.outEmitter.emit("connectionError", { + // message: "Could not connect to the server", + // navigateToMode: "mainMenu", + // }); + // this.game.destroy(true); + // }); }); } else { this.game.outEmitter.emit("loading", { name: "Main Scene", progress: 100 }); diff --git a/client/src/ui/App.tsx b/client/src/ui/App.tsx index ba50b74..1689c97 100644 --- a/client/src/ui/App.tsx +++ b/client/src/ui/App.tsx @@ -1,16 +1,17 @@ -import React, { useEffect } from "react"; +import React from "react"; import { BackgroundImage, Group } from "@mantine/core"; import styled from "@emotion/styled"; -import { syncSettingsToSession, useGame } from "./hooks"; +import { useGame, useMultiplayerDisconnect, useSyncSettingsToSession } from "./hooks"; import { TopLeft } from "./scenes/TopLeft"; import { TopRight } from "./scenes/TopRight"; import { Center } from "./scenes/Center"; import { Right } from "./scenes/Right"; import { BottomLeft } from "./scenes/BottomLeft"; import { BottomRight } from "./scenes/BottomRight"; -import background from "~/assets/ui/background-space.webp"; import { UnderTopRight } from "./scenes/UnderTopRight"; +import background from "~/assets/ui/background-space.webp"; +import { ErrorModal } from "./components"; // @ts-ignore const StyledUI = styled(BackgroundImage)` @@ -92,10 +93,12 @@ const StyledBottomRightGroup = styled(Group)` `; export const App = () => { - useEffect(() => { - const unsub = syncSettingsToSession(); - return unsub; - }, []); + useSyncSettingsToSession(); + + const { connectionError, clearConnectionError } = useMultiplayerDisconnect(); + const clearErrors = () => { + clearConnectionError(); + }; const { computed: { isLoaded }, @@ -110,6 +113,10 @@ export const App = () => { {isLoaded && } + ); }; diff --git a/client/src/ui/Root.tsx b/client/src/ui/Root.tsx index 821dc6f..88efe9c 100644 --- a/client/src/ui/Root.tsx +++ b/client/src/ui/Root.tsx @@ -1,5 +1,3 @@ -//* - import "core-js/actual"; import React from "react"; import { createRoot } from "react-dom/client"; @@ -133,61 +131,3 @@ const events = ["mouseup", "mousedown", "touchstart", "touchmove", "touchend", " events.forEach((event) => { container.addEventListener(event, (e) => e.stopPropagation()); }); - -/*/ - -// import geckosClient from "@geckos.io/client"; -// // @ts-ignore -// const channel = geckosClient({ url: "http://127.0.0.1:3010", port: null }); -// console.log("client channel created", channel); - -// channel.onConnect((error) => { -// console.log("CONNECT"); -// if (error) console.error(error.message); - -// // listens for a disconnection -// channel.onDisconnect(() => { -// console.log("diccsosn"); -// }); - -// // listens for a custom event from the server -// channel.on("chat message", (data) => { -// console.log("is it truly over?", data); -// }); - -// // emits a message to the server -// channel.emit("chat message", "Hi!"); -// }); - -const gg = async () => { - const host = `http://localhost:3010/.wrtc/v2`; - - let headers: any = { "Content-Type": "application/json" }; - let userData = {}; - - try { - console.log("trying"); - const res = await fetch(`${host}/connections`, { - method: "POST", - headers, - }); - console.log("Tried"); - if (res.status >= 300) { - throw { - name: "Error", - message: `Connection failed with status code ${res.status}.`, - status: res.status, - statusText: res.statusText, - }; - } - - const json = await res.json(); - - userData = json.userData; - } catch (error: any) { - console.error("WHAT", error.message); - return { error }; - } -}; -gg(); -//*/ diff --git a/client/src/ui/components/ErrorModal/ErrorModal.tsx b/client/src/ui/components/ErrorModal/ErrorModal.tsx new file mode 100644 index 0000000..eccabc9 --- /dev/null +++ b/client/src/ui/components/ErrorModal/ErrorModal.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from "react"; +import { Modal, Space, Title } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; + +import { Errors } from "./components"; + +export const ErrorModal = ({ errors, clearErrors }) => { + const [opened, { close, open }] = useDisclosure(false); + + useEffect(() => { + if (errors.length) { + open(); + } + }, [errors]); + + const handleClose = () => { + close(); + clearErrors(); + }; + + return ( + + + + + + Error + + + + + + + + + + ); +}; diff --git a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/NonFieldErrors.tsx b/client/src/ui/components/ErrorModal/components/Errors.tsx similarity index 86% rename from client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/NonFieldErrors.tsx rename to client/src/ui/components/ErrorModal/components/Errors.tsx index d1244bf..a395a29 100644 --- a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/NonFieldErrors.tsx +++ b/client/src/ui/components/ErrorModal/components/Errors.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Text } from "@mantine/core"; -export const NonFieldErrors = ({ errors }) => { +export const Errors = ({ errors }) => { return ( <> {errors && diff --git a/client/src/ui/components/ErrorModal/components/index.ts b/client/src/ui/components/ErrorModal/components/index.ts new file mode 100644 index 0000000..b8a5016 --- /dev/null +++ b/client/src/ui/components/ErrorModal/components/index.ts @@ -0,0 +1 @@ +export * from "./Errors"; diff --git a/client/src/ui/components/ErrorModal/index.ts b/client/src/ui/components/ErrorModal/index.ts new file mode 100644 index 0000000..a9c806a --- /dev/null +++ b/client/src/ui/components/ErrorModal/index.ts @@ -0,0 +1,2 @@ +export * from "./components"; +export * from "./ErrorModal"; diff --git a/client/src/ui/components/index.ts b/client/src/ui/components/index.ts index 26d3bd6..3e5eb9f 100644 --- a/client/src/ui/components/index.ts +++ b/client/src/ui/components/index.ts @@ -1 +1,2 @@ export * from "./ToggleButton"; +export * from "./ErrorModal"; diff --git a/client/src/ui/hooks/useGame/index.ts b/client/src/ui/hooks/useGame/index.ts new file mode 100644 index 0000000..af8cabc --- /dev/null +++ b/client/src/ui/hooks/useGame/index.ts @@ -0,0 +1,2 @@ +export * from "./useGame"; +export * from "./useMultiplayerDisconnect"; diff --git a/client/src/ui/hooks/useGame.ts b/client/src/ui/hooks/useGame/useGame.ts similarity index 100% rename from client/src/ui/hooks/useGame.ts rename to client/src/ui/hooks/useGame/useGame.ts diff --git a/client/src/ui/hooks/useGame/useMultiplayerDisconnect/index.ts b/client/src/ui/hooks/useGame/useMultiplayerDisconnect/index.ts new file mode 100644 index 0000000..a423477 --- /dev/null +++ b/client/src/ui/hooks/useGame/useMultiplayerDisconnect/index.ts @@ -0,0 +1 @@ +export * from "./useMultiplayerDisconnect"; diff --git a/client/src/ui/hooks/useGame/useMultiplayerDisconnect/useMultiplayerDisconnect.ts b/client/src/ui/hooks/useGame/useMultiplayerDisconnect/useMultiplayerDisconnect.ts new file mode 100644 index 0000000..95826ef --- /dev/null +++ b/client/src/ui/hooks/useGame/useMultiplayerDisconnect/useMultiplayerDisconnect.ts @@ -0,0 +1,31 @@ +import { useDidUpdate } from "@mantine/hooks"; + +import { useGame } from "../"; +import { useState } from "react"; + +export const useMultiplayerDisconnect = () => { + const [connectionError, setConnectionError] = useState(null); + const { + gameManager, + loadMainMenu, + computed: { isLoaded }, + } = useGame(); + + useDidUpdate(() => { + if (isLoaded) { + console.log("LOADED CHANGED"); + const { channel } = gameManager.game; + channel.onDisconnect(() => { + loadMainMenu(); + console.log("SET ERROR"); + setConnectionError("Connection to server lost"); + }); + } + }, [isLoaded]); + + const clearConnectionError = () => { + setConnectionError(null); + }; + + return { connectionError, clearConnectionError }; +}; diff --git a/client/src/ui/hooks/useSettings/hooks/index.ts b/client/src/ui/hooks/useSettings/hooks/index.ts new file mode 100644 index 0000000..034b974 --- /dev/null +++ b/client/src/ui/hooks/useSettings/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useSyncSettingsToSession"; diff --git a/client/src/ui/hooks/useSettings/hooks/useSyncSettingsToSession.ts b/client/src/ui/hooks/useSettings/hooks/useSyncSettingsToSession.ts new file mode 100644 index 0000000..b5dab07 --- /dev/null +++ b/client/src/ui/hooks/useSettings/hooks/useSyncSettingsToSession.ts @@ -0,0 +1,20 @@ +import { useEffect } from "react"; + +import { useSettings, settingsStorageKey } from "../"; +import { setSessionStorage } from "../services/localStorage"; + +const syncSettingsToSession = () => { + const unsub = useSettings.subscribe( + (state) => state.settings, + (state) => setSessionStorage(settingsStorageKey, state) + ); + + return unsub; +}; + +export const useSyncSettingsToSession = () => { + return useEffect(() => { + const unsub = syncSettingsToSession(); + return unsub; + }, []); +}; diff --git a/client/src/ui/hooks/useSettings/index.ts b/client/src/ui/hooks/useSettings/index.ts new file mode 100644 index 0000000..dad0b0f --- /dev/null +++ b/client/src/ui/hooks/useSettings/index.ts @@ -0,0 +1,3 @@ +export * from "./hooks"; +export * from "./services"; +export * from "./useSettings"; diff --git a/client/src/ui/hooks/useSettings/services/index.ts b/client/src/ui/hooks/useSettings/services/index.ts new file mode 100644 index 0000000..9272ff5 --- /dev/null +++ b/client/src/ui/hooks/useSettings/services/index.ts @@ -0,0 +1 @@ +export * from "./localStorage"; diff --git a/client/src/ui/hooks/useSettings/services/localStorage.ts b/client/src/ui/hooks/useSettings/services/localStorage.ts new file mode 100644 index 0000000..b5b6ed7 --- /dev/null +++ b/client/src/ui/hooks/useSettings/services/localStorage.ts @@ -0,0 +1,23 @@ +export const setSessionStorage = (key, value) => { + sessionStorage.setItem(key, serializeJSON(value)); +}; + +export const getSessionStorage = (key) => { + return deserializeJSON(sessionStorage.getItem(key) ?? "{}"); +}; + +export const serializeJSON = (value: any) => { + try { + return JSON.stringify(value); + } catch (error) { + throw new Error("Failed to serialize the value"); + } +}; + +export const deserializeJSON = (value: string) => { + try { + return JSON.parse(value); + } catch { + return value; + } +}; diff --git a/client/src/ui/hooks/useSettings.ts b/client/src/ui/hooks/useSettings/useSettings.ts similarity index 74% rename from client/src/ui/hooks/useSettings.ts rename to client/src/ui/hooks/useSettings/useSettings.ts index a1dd312..5f0fd63 100644 --- a/client/src/ui/hooks/useSettings.ts +++ b/client/src/ui/hooks/useSettings/useSettings.ts @@ -2,6 +2,8 @@ import { create } from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; import { produce } from "immer"; +import { getSessionStorage } from "./services/localStorage"; + const isTouchDevice = () => "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator["msMaxTouchPoints"] > 0; @@ -16,30 +18,6 @@ interface SettingsStore { toggleTouchControlsSetting: () => void; } -const setSessionStorage = (key, value) => { - sessionStorage.setItem(key, serializeJSON(value)); -}; - -const getSessionStorage = (key) => { - return deserializeJSON(sessionStorage.getItem(key) ?? "{}"); -}; - -const serializeJSON = (value: any) => { - try { - return JSON.stringify(value); - } catch (error) { - throw new Error("Failed to serialize the value"); - } -}; - -const deserializeJSON = (value: string) => { - try { - return JSON.parse(value); - } catch { - return value; - } -}; - const defaultSettings = { musicMute: false, effectsMute: false, @@ -51,11 +29,11 @@ const defaultSettings = { showDeviceInfo: false, }; -const key = "settings"; +export const settingsStorageKey = "settings"; export const useSettings = create()( subscribeWithSelector((set) => ({ - settings: { ...defaultSettings, ...getSessionStorage(key) }, + settings: { ...defaultSettings, ...getSessionStorage(settingsStorageKey) }, toggleEffectsSetting: () => set( produce((state) => { @@ -100,12 +78,3 @@ export const useSettings = create()( ), })) ); - -export const syncSettingsToSession = () => { - const unsub = useSettings.subscribe( - (state) => state.settings, - (state) => setSessionStorage(key, state) - ); - - return unsub; -}; diff --git a/client/src/ui/scenes/BottomRight/BottomRight.tsx b/client/src/ui/scenes/BottomRight/BottomRight.tsx index dbf90c3..fedb849 100644 --- a/client/src/ui/scenes/BottomRight/BottomRight.tsx +++ b/client/src/ui/scenes/BottomRight/BottomRight.tsx @@ -26,15 +26,11 @@ export const BottomRight = ({ GroupComponent }) => { toggleProfileModal(); }; - const handleMainMenu = () => { - loadMainMenu(); - }; - return ( <> {isLoaded && ( - )} diff --git a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/index.ts b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/index.ts deleted file mode 100644 index b1ccc6b..0000000 --- a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./NonFieldErrors"; diff --git a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Login/Login.tsx b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Login/Login.tsx index e2ee7c4..89b70db 100644 --- a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Login/Login.tsx +++ b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Login/Login.tsx @@ -3,8 +3,7 @@ import { useDidUpdate } from "@mantine/hooks"; import { useForm } from "@mantine/form"; import { TextInput, PasswordInput, Space, Stack, Anchor } from "@mantine/core"; -import { Button } from "~/ui/components"; -import { NonFieldErrors } from "../../components"; +import { Button, Errors } from "~/ui/components"; import { useLoginMutation } from "./hooks"; export const Login = ({ setShowLogIn, initEmailPass = {}, setInitEmailPass }) => { @@ -32,7 +31,7 @@ export const Login = ({ setShowLogIn, initEmailPass = {}, setInitEmailPass }) => return (
- + { const save = useSaveMutation(setNonFieldErrors); return ( - + {meStatus === "success" ? ( <> Username: {me.username} diff --git a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Register/Register.tsx b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Register/Register.tsx index f919918..f83d14c 100644 --- a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Register/Register.tsx +++ b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Register/Register.tsx @@ -3,8 +3,7 @@ import { useDidUpdate } from "@mantine/hooks"; import { useForm } from "@mantine/form"; import { TextInput, PasswordInput, Space, Stack, Anchor } from "@mantine/core"; -import { Button } from "~/ui/components"; -import { NonFieldErrors } from "../../components"; +import { Button, Errors } from "~/ui/components"; import { useRegisterMutation } from "./hooks"; export const Register = ({ setShowLogIn, initEmailPass = {}, setInitEmailPass }) => { @@ -33,7 +32,7 @@ export const Register = ({ setShowLogIn, initEmailPass = {}, setInitEmailPass }) return ( - + { ); }; - -// display: flex; -// flex-direction: column; -// & > { -// flex-grow: 1; -// } diff --git a/client/src/ui/scenes/UnderTopRight/scenes/Chat/Chat.tsx b/client/src/ui/scenes/UnderTopRight/scenes/Chat/Chat.tsx index 584395a..62852be 100644 --- a/client/src/ui/scenes/UnderTopRight/scenes/Chat/Chat.tsx +++ b/client/src/ui/scenes/UnderTopRight/scenes/Chat/Chat.tsx @@ -15,12 +15,10 @@ const isoToLocalTime = (isoString) => { return localTime; }; -const getCurrentTime = () => { +const getIsoTime = () => { const now = new Date(); const isoTime = now.toISOString(); - const localTime = isoToLocalTime(isoTime); - - return { isoTime, localTime }; + return isoTime; }; export const Chat = () => { @@ -42,25 +40,33 @@ export const Chat = () => { }; const handleFocus = () => gameManager.lockInput(); - useEffect(() => { - channel.on("message", (data) => { - console.log("Recived message!", data); - }); - }, []); - const [message, setMessage] = useState(""); - const [chatEntires, { append: addChatEntry }] = useListState([]); + const [chatEntires, { append: appendChatEntry }] = useListState([]); const chatViewportRef = useScrollToBottom([chatEntires]); const sendMessage = () => { if (message) { - const { isoTime, localTime } = getCurrentTime(); - channel.emit("message", { nick, message, isoTime }, { reliable: true }); - addChatEntry({ nick, message, localTime }); + const isoTime = getIsoTime(); + const chatEntry = { nick, message, isoTime }; + channel.emit("message", chatEntry, { reliable: true }); + addMessage(chatEntry); + setMessage(""); } }; + const addMessage = (chatEntry) => { + const { nick, message, isoTime } = chatEntry; + const localTime = isoToLocalTime(isoTime); + appendChatEntry({ nick, message, localTime }); + }; + + useEffect(() => { + channel.on("message", (chatEntry) => { + addMessage(chatEntry); + }); + }, []); + return ( Chat diff --git a/server/src/core/app.ts b/server/src/core/app.ts index ab73f62..a8cd25f 100644 --- a/server/src/core/app.ts +++ b/server/src/core/app.ts @@ -6,6 +6,7 @@ import geckos from "@geckos.io/server"; import { createServer } from "http"; import { isAuthenticated, logger, correctContentType } from "~/middleware"; +import { getIsoTime } from "~/utils"; export const app = express(); const server = createServer(app); @@ -15,13 +16,19 @@ const io = geckos({ cors: { allowAuthorization: true, origin: "*" } }); io.addServer(server); io.onConnection((channel) => { - console.log("its happening!"); + console.log("Channel connected"); - channel.emit("chat message", "Hi!"); - - channel.on("chat message", (data) => { - console.log("got?", data); + channel.on("message", (data) => { + console.log("Message:", data); + channel.broadcast.emit("message", data); }); + + channel.emit("ready"); + channel.emit( + "message", + { nick: "Server", message: "Welcome!", isoTime: getIsoTime() }, + { reliable: true } + ); }); const rootRoutes = express.Router(); diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts index 0f2ae23..3cd8e18 100644 --- a/server/src/utils/index.ts +++ b/server/src/utils/index.ts @@ -1,3 +1,2 @@ -import { generateAccessToken, generateRefreshToken, generateTokens, hashToken } from "./jwt"; - -export { generateAccessToken, generateRefreshToken, generateTokens, hashToken }; +export * from "./jwt"; +export * from "./time"; diff --git a/server/src/utils/time.ts b/server/src/utils/time.ts new file mode 100644 index 0000000..6208655 --- /dev/null +++ b/server/src/utils/time.ts @@ -0,0 +1,5 @@ +export const getIsoTime = () => { + const now = new Date(); + const isoTime = now.toISOString(); + return isoTime; +};