From b4af6e08ca295f2ba3a9a4526fe6300e6f21eb91 Mon Sep 17 00:00:00 2001 From: NigeParis Date: Sat, 10 Jan 2026 18:09:20 +0100 Subject: [PATCH] Chat moved up to top level now in all services frontend --- frontend/index.html | 5 +- frontend/src/{pages => }/chat/chat.css | 16 + frontend/src/chat/chat.html | 53 +++ frontend/src/chat/chat.ts | 420 ++++++++++++++++++ .../actionBtnPopUpBlock.ts | 0 .../actionBtnPopUpInvite.ts | 0 .../chat/chatHelperFunctions/addMessage.ts | 0 .../chat/chatHelperFunctions/blockUser.ts | 0 .../chat/chatHelperFunctions/broadcastMsg.ts | 0 .../chatHelperFunctions/clearChatWindow.ts | 0 .../chat/chatHelperFunctions/cmdList.ts | 0 .../chat/chatHelperFunctions/connected.ts | 0 .../chat/chatHelperFunctions/getProfil.ts | 0 .../chatHelperFunctions/incrementCounter.ts | 0 .../chatHelperFunctions/inviteToPlayPong.ts | 0 .../chat/chatHelperFunctions/isLoggedIn.ts | 0 .../chat/chatHelperFunctions/listBuddies.ts | 0 .../chat/chatHelperFunctions/logout.ts | 5 +- .../chatHelperFunctions/openMessagePopup.ts | 0 .../chatHelperFunctions/openProfilePopup.ts | 0 .../chat/chatHelperFunctions/parseCmdMsg.ts | 0 .../chat/chatHelperFunctions/quitChat.ts | 0 .../chat/chatHelperFunctions/setGuestInfo.ts | 0 .../waitSocketConnected.ts | 0 .../chatHelperFunctions/windowStateHidden.ts | 3 +- .../chatHelperFunctions/windowStateVisable.ts | 3 +- frontend/src/{pages => }/chat/types_front.ts | 0 frontend/src/pages/index.ts | 2 +- frontend/src/pages/pong/pong.css | 206 +++++++-- frontend/src/pages/pong/pong.ts | 15 +- frontend/src/pages/root/pong_box_image.png | Bin 25973 -> 0 bytes frontend/src/pages/root/root.html | 1 + 32 files changed, 687 insertions(+), 42 deletions(-) rename frontend/src/{pages => }/chat/chat.css (94%) create mode 100644 frontend/src/chat/chat.html create mode 100644 frontend/src/chat/chat.ts rename frontend/src/{pages => }/chat/chatHelperFunctions/actionBtnPopUpBlock.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/actionBtnPopUpInvite.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/addMessage.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/blockUser.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/broadcastMsg.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/clearChatWindow.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/cmdList.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/connected.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/getProfil.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/incrementCounter.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/inviteToPlayPong.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/isLoggedIn.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/listBuddies.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/logout.ts (71%) rename frontend/src/{pages => }/chat/chatHelperFunctions/openMessagePopup.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/openProfilePopup.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/parseCmdMsg.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/quitChat.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/setGuestInfo.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/waitSocketConnected.ts (100%) rename frontend/src/{pages => }/chat/chatHelperFunctions/windowStateHidden.ts (85%) rename frontend/src/{pages => }/chat/chatHelperFunctions/windowStateVisable.ts (89%) rename frontend/src/{pages => }/chat/types_front.ts (100%) delete mode 100644 frontend/src/pages/root/pong_box_image.png diff --git a/frontend/index.html b/frontend/index.html index 1257854..f5da4a5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -34,13 +34,14 @@ 🏠 Home 👤 Login 👤 Signin - 👤 Chat ⭕ Tic-Tac-Toe ▮•▮ Ping Pong ⚙️ Settings 🚪 Logout +
+ @@ -51,12 +52,14 @@
+ + diff --git a/frontend/src/pages/chat/chat.css b/frontend/src/chat/chat.css similarity index 94% rename from frontend/src/pages/chat/chat.css rename to frontend/src/chat/chat.css index 9e27632..9b4b0f1 100644 --- a/frontend/src/pages/chat/chat.css +++ b/frontend/src/chat/chat.css @@ -282,6 +282,22 @@ div-private { items-center; } +.chat-button { + @apply + z-100 + text-3xl + fixed bottom-6 + right-6 + w-14 + h-14 + rounded-full + bg-gray-600 + text-white + shadow-lg + flex items-center + justify-center + hover:bg-red-700 +} diff --git a/frontend/src/chat/chat.html b/frontend/src/chat/chat.html new file mode 100644 index 0000000..e4601d3 --- /dev/null +++ b/frontend/src/chat/chat.html @@ -0,0 +1,53 @@ +
+
+

+ Chatter Box +


+ + + +
System: connecting ...
+
+ + +
+ +
+
+
+ +
🔕
+
💔
+ +
+
+ +
+

Ping Buddies

+
+
+
+
+ + +
+
+
+
+
+ + + + diff --git a/frontend/src/chat/chat.ts b/frontend/src/chat/chat.ts new file mode 100644 index 0000000..d6ab56d --- /dev/null +++ b/frontend/src/chat/chat.ts @@ -0,0 +1,420 @@ +import "./chat.css"; +import io, { Socket } from "socket.io-client"; +import type { blockedUnBlocked } from "./types_front"; +import type { + ClientMessage, + ClientProfil, + ClientProfilPartial, +} from "./types_front"; +import type { User } from "@app/auth"; +import { + addRoute, + setTitle, + type RouteHandlerParams, + type RouteHandlerReturn, +} from "@app/routing"; +import authHtml from "./chat.html?raw"; +import { getUser } from "@app/auth"; +import { listBuddies } from "./chatHelperFunctions/listBuddies"; +import { getProfil } from "./chatHelperFunctions/getProfil"; +import { addMessage } from "./chatHelperFunctions/addMessage"; +import { broadcastMsg } from "./chatHelperFunctions/broadcastMsg"; +import { openProfilePopup } from "./chatHelperFunctions/openProfilePopup"; +import { actionBtnPopUpBlock } from "./chatHelperFunctions/actionBtnPopUpBlock"; +import { windowStateHidden } from "./chatHelperFunctions/windowStateHidden"; +import { blockUser } from "./chatHelperFunctions/blockUser"; +import { parseCmdMsg } from "./chatHelperFunctions/parseCmdMsg"; +import { actionBtnPopUpInvite } from "./chatHelperFunctions/actionBtnPopUpInvite"; +import { waitSocketConnected } from "./chatHelperFunctions/waitSocketConnected"; +import { connected } from "./chatHelperFunctions/connected"; +import { quitChat } from "./chatHelperFunctions/quitChat"; +import { openMessagePopup } from "./chatHelperFunctions/openMessagePopup"; +import { windowStateVisable } from "./chatHelperFunctions/windowStateVisable"; +import { cmdList } from "./chatHelperFunctions/cmdList"; +import { showInfo } from '../toast'; + +const MAX_SYSTEM_MESSAGES = 10; +let inviteMsgFlag: boolean = false; +export let noGuestFlag: boolean = true; + +declare module "ft_state" { + interface State { + chatSock?: Socket; + } +} + +export function getSocket(): Socket { + if (window.__state.chatSock === undefined) + window.__state.chatSock = io(window.location.host, { + path: "/api/chat/socket.io/", + }) as any as Socket; + return window.__state.chatSock; +} + +const chatBox = document.getElementById("chatBox")!; +chatBox.classList.add('hidden'); +chatBox.innerHTML = authHtml; + +let socket = getSocket(); +let blockMessage: boolean; +// Listen for the 'connect' event +socket.on("connect", async () => { + const systemWindow = document.getElementById("system-box") as HTMLDivElement; + const sendtextbox = document.getElementById( + "t-chat-window" + ) as HTMLButtonElement; + const noGuest = document.getElementById("noGuest") ?? null; + + await waitSocketConnected(socket); + const user = getUser()?.name; + const userID = getUser()?.id; + // Ensure we have a user AND socket is connected + if (!user || !socket.connected || !noGuest) return; + const message = { + command: "", + destination: "system-info", + type: "chat", + user, + token: document.cookie ?? "", + text: " has Just ARRIVED in the chat", + timestamp: Date.now(), + SenderWindowID: socket.id, + SenderID: userID, + }; + socket.emit("message", JSON.stringify(message)); + const guest = getUser()?.guest; + if (guest) { + noGuest.innerText = ""; + } else { + noGuest.innerText = "❤️"; + } + + const userProfile: ClientProfil = { + command: "@noguest", + destination: "", + type: "chat", + timestamp: Date.now(), + guestmsg: true, + }; + socket.emit("guestmsg", JSON.stringify(userProfile)); + const messageElement = document.createElement("div"); + messageElement.textContent = `${user}: is connected au server`; + systemWindow.appendChild(messageElement); + systemWindow.scrollTop = systemWindow.scrollHeight; +}); + +// Listen for messages from the server "MsgObjectServer" +socket.on("MsgObjectServer", (data: { message: ClientMessage }) => { + const systemWindow = document.getElementById("system-box") as HTMLDivElement; + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + + if (socket) { + connected(socket); + } + + if (chatWindow && data.message.destination === "") { + const messageElement = document.createElement("div"); + messageElement.textContent = `${data.message.user}: ${data.message.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } + + if (chatWindow && data.message.destination === "privateMsg") { + const messageElement = document.createElement("div-private"); + messageElement.textContent = `🔒${data.message.user}: ${data.message.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } + + if (chatWindow && data.message.destination === "inviteMsg") { + const messageElement = document.createElement("div-private"); + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + messageElement.innerHTML = `🏓${data.message.SenderUserName}: ${data.message.innerHtml}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } + + if (systemWindow && data.message.destination === "system-info") { + const messageElement = document.createElement("div"); + messageElement.textContent = `${data.message.user}: ${data.message.text}`; + systemWindow.appendChild(messageElement); + + // keep only last 10 + while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { + systemWindow.removeChild(systemWindow.firstChild!); + } + systemWindow.scrollTop = systemWindow.scrollHeight; + } +}); + +socket.on("profilMessage", (profil: ClientProfil) => { + profil.SenderID = getUser()?.id ?? ""; + profil.SenderName = getUser()?.name ?? ""; + openProfilePopup(profil); + socket.emit("isBlockdBtn", profil); + socket.emit("check_Block_button", profil); + actionBtnPopUpInvite(profil, socket); + actionBtnPopUpBlock(profil, socket); +}); + +socket.on("blockUser", (blocked: ClientProfil) => { + let icon = "⛔"; + if (inviteMsgFlag) { + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + const messageElement = document.createElement("div"); + if (`${blocked.text}` === "I have un-blocked you") { + icon = "💚"; + } + messageElement.innerText = `${icon}${blocked.SenderName}: ${blocked.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } +}); + +socket.on("blockBtn", (data: blockedUnBlocked) => { + const blockUserBtn = document.querySelector("#popup-b-block"); + if (blockUserBtn) { + let message = ""; + if (data.userState === "block") { + (message = "un-block"), (blockMessage = true); + } else { + (message = "block"), (blockMessage = false); + } + blockUserBtn.textContent = message; + } +}); + +socket.on("logout", () => { + quitChat(socket); +}); + +socket.on("privMessageCopy", (message: string) => { + addMessage(message); +}); + +//receives broadcast of the next GAME +socket.on("nextGame", (message: string) => { + openMessagePopup(message); +}); + +let toggle = false; +window.addEventListener("focus", async () => { + setTimeout(() => { + connected(socket); + }, 16); + if (window.location.pathname === "/app/chat") { + if (socket.id) { + await windowStateVisable(); + } + toggle = true; + } +}); + +window.addEventListener("blur", () => { + if (socket.id) windowStateHidden(); + toggle = false; +}); + +socket.on("listBud", async (myBuddies: string[]) => { + const buddies = document.getElementById("div-buddies") as HTMLDivElement; + listBuddies(socket, buddies, myBuddies); +}); + +socket.once("welcome", (data) => { + const buddies = document.getElementById("div-buddies") as HTMLDivElement; + buddies.textContent = ""; + buddies.innerHTML = ""; + connected(socket); + addMessage(`${data.msg} ` + getUser()?.name); +}); +setTitle("Chat Page"); +const sendButton = document.getElementById("b-send") as HTMLButtonElement; +const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; +const sendtextbox = document.getElementById( + "t-chat-window" +) as HTMLButtonElement; +const clearText = document.getElementById("b-clear") as HTMLButtonElement; +const buddies = document.getElementById("div-buddies") as HTMLDivElement; +const bquit = document.getElementById("b-quit") as HTMLDivElement; + +buddies.textContent = ""; +buddies.innerHTML = ""; +const buttonPro = document.getElementById("close-modal") ?? null; + +if (buttonPro) + buttonPro.addEventListener("click", () => { + const profilList = document.getElementById("profile-modal") ?? null; + if (profilList) profilList.classList.add("hidden"); + }); + +const buttonMessage = document.getElementById("close-modal-message") ?? null; + +if (buttonMessage) + buttonMessage.addEventListener("click", () => { + const gameMessage = document.getElementById("game-modal") ?? null; + if (gameMessage) gameMessage.classList.add("hidden"); + const modalmessage = document.getElementById("modal-message") ?? null; + if (modalmessage) { + modalmessage.innerHTML = ""; + } + }); + +// Send button +sendButton?.addEventListener("click", () => { + const notify = document.getElementById("notify") ?? null; + const noGuest = document.getElementById("noGuest") ?? null; + const userId = getUser()?.id; + const userAskingToBlock = getUser()?.name; + if (sendtextbox && sendtextbox.value.trim()) { + let msgText: string = sendtextbox.value.trim(); + const msgCommand = parseCmdMsg(msgText) ?? ""; + connected(socket); + if (msgCommand !== "") { + switch (msgCommand[0]) { + case "@msg": + broadcastMsg(socket, msgCommand); + break; + + case "@block": + if (msgCommand[1] === "") { + break; + } + if (!userAskingToBlock) return; + if (!userId) return; + const userToBlock: ClientProfil = { + command: msgCommand[0], + destination: "", + type: "chat", + user: msgCommand[1], + userID: userId, + timestamp: Date.now(), + SenderWindowID: socket.id, + SenderName: userAskingToBlock, + }; + blockUser(userToBlock, socket); + break; + + case "@notify": + if (notify === null) { + break; + } + if (inviteMsgFlag === false) { + notify.innerText = "🔔"; + inviteMsgFlag = true; + } else { + notify.innerText = "🔕"; + inviteMsgFlag = false; + } + break; + + case "@guest": + if (!userId) { + return; + } + if (!userAskingToBlock) { + return; + } + if (noGuest === null) { + break; + } + const guest = getUser()?.guest; + if (noGuestFlag === false && noGuest.innerText === "💔") { + noGuest.innerText = "❤️​"; + noGuestFlag = true; + } else { + noGuest.innerText = "💔"; + noGuestFlag = false; + } + if (guest) { + noGuestFlag = true; + noGuest.innerText = ""; + sendtextbox.value = ""; + } + const userProfile: ClientProfilPartial = { + command: "@noguest", + destination: "", + type: "chat", + user: userAskingToBlock, + userID: userId, + timestamp: Date.now(), + SenderWindowID: "", + guestmsg: noGuestFlag, + }; + socket.emit("guestmsg", JSON.stringify(userProfile)); + break; + + case "@profile": + if (msgCommand[1] === "") { + break; + } + getProfil(socket, msgCommand[1]); + break; + case "@cls": + chatWindow.innerHTML = ""; + break; + case "@help": + cmdList(); + break; + + case "@quit": + quitChat(socket); + break; + + default: + const user: User | null = getUser(); + if (!user) return; + if (!user || !socket.connected) return; + const message: ClientProfilPartial = { + command: msgCommand[0], + destination: "", + type: "chat", + user: user.name, + userID: user.id, + token: document.cookie ?? "", + text: msgCommand[1], + timestamp: Date.now(), + SenderWindowID: socket.id ?? "", + SenderID: user.id, + }; + socket.emit("privMessage", JSON.stringify(message)); + break; + } + // Clear the input in all cases + sendtextbox.value = ""; + } + } +}); + +// Clear Text button +clearText?.addEventListener("click", () => { + if (chatWindow) { + chatWindow.innerHTML = ""; + } +}); + +bquit?.addEventListener("click", () => { + showInfo('Nigel close the chat overlay please') +}); + +// Enter key to send message +sendtextbox.addEventListener("keydown", (event) => { + if (!sendtextbox) return; + if (event.key === "Enter") { + event.preventDefault(); + sendButton?.click(); + } +}); + +const chatButton = document.querySelector('#chatButton'); +chatButton!.addEventListener("click", () => { + + const overlay = document.querySelector('#overlay')!; + if (chatBox.classList.contains('hidden')) { + chatBox.classList.toggle('hidden'); + overlay.classList.add('opacity-60'); + } else { + chatBox.classList.toggle('hidden'); + overlay.classList.remove('opacity-60'); + } +}); + diff --git a/frontend/src/pages/chat/chatHelperFunctions/actionBtnPopUpBlock.ts b/frontend/src/chat/chatHelperFunctions/actionBtnPopUpBlock.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/actionBtnPopUpBlock.ts rename to frontend/src/chat/chatHelperFunctions/actionBtnPopUpBlock.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/actionBtnPopUpInvite.ts b/frontend/src/chat/chatHelperFunctions/actionBtnPopUpInvite.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/actionBtnPopUpInvite.ts rename to frontend/src/chat/chatHelperFunctions/actionBtnPopUpInvite.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/addMessage.ts b/frontend/src/chat/chatHelperFunctions/addMessage.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/addMessage.ts rename to frontend/src/chat/chatHelperFunctions/addMessage.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/blockUser.ts b/frontend/src/chat/chatHelperFunctions/blockUser.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/blockUser.ts rename to frontend/src/chat/chatHelperFunctions/blockUser.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/broadcastMsg.ts b/frontend/src/chat/chatHelperFunctions/broadcastMsg.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/broadcastMsg.ts rename to frontend/src/chat/chatHelperFunctions/broadcastMsg.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/clearChatWindow.ts b/frontend/src/chat/chatHelperFunctions/clearChatWindow.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/clearChatWindow.ts rename to frontend/src/chat/chatHelperFunctions/clearChatWindow.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/cmdList.ts b/frontend/src/chat/chatHelperFunctions/cmdList.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/cmdList.ts rename to frontend/src/chat/chatHelperFunctions/cmdList.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/connected.ts b/frontend/src/chat/chatHelperFunctions/connected.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/connected.ts rename to frontend/src/chat/chatHelperFunctions/connected.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/getProfil.ts b/frontend/src/chat/chatHelperFunctions/getProfil.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/getProfil.ts rename to frontend/src/chat/chatHelperFunctions/getProfil.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/incrementCounter.ts b/frontend/src/chat/chatHelperFunctions/incrementCounter.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/incrementCounter.ts rename to frontend/src/chat/chatHelperFunctions/incrementCounter.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/inviteToPlayPong.ts b/frontend/src/chat/chatHelperFunctions/inviteToPlayPong.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/inviteToPlayPong.ts rename to frontend/src/chat/chatHelperFunctions/inviteToPlayPong.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/isLoggedIn.ts b/frontend/src/chat/chatHelperFunctions/isLoggedIn.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/isLoggedIn.ts rename to frontend/src/chat/chatHelperFunctions/isLoggedIn.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/listBuddies.ts b/frontend/src/chat/chatHelperFunctions/listBuddies.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/listBuddies.ts rename to frontend/src/chat/chatHelperFunctions/listBuddies.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/logout.ts b/frontend/src/chat/chatHelperFunctions/logout.ts similarity index 71% rename from frontend/src/pages/chat/chatHelperFunctions/logout.ts rename to frontend/src/chat/chatHelperFunctions/logout.ts index 9e0d9bc..1aeadfc 100644 --- a/frontend/src/pages/chat/chatHelperFunctions/logout.ts +++ b/frontend/src/chat/chatHelperFunctions/logout.ts @@ -1,11 +1,10 @@ import { Socket } from "socket.io-client"; -import { __socket } from "../chat"; export function logout(socket: Socket) { socket.emit("logout"); // notify server socket.disconnect(); // actually close the socket localStorage.clear(); - if (__socket !== undefined) - __socket.close(); + if (window.__state.chatSock !== undefined) + window.__state.chatSock.close(); }; diff --git a/frontend/src/pages/chat/chatHelperFunctions/openMessagePopup.ts b/frontend/src/chat/chatHelperFunctions/openMessagePopup.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/openMessagePopup.ts rename to frontend/src/chat/chatHelperFunctions/openMessagePopup.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/openProfilePopup.ts b/frontend/src/chat/chatHelperFunctions/openProfilePopup.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/openProfilePopup.ts rename to frontend/src/chat/chatHelperFunctions/openProfilePopup.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/parseCmdMsg.ts b/frontend/src/chat/chatHelperFunctions/parseCmdMsg.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/parseCmdMsg.ts rename to frontend/src/chat/chatHelperFunctions/parseCmdMsg.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/quitChat.ts b/frontend/src/chat/chatHelperFunctions/quitChat.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/quitChat.ts rename to frontend/src/chat/chatHelperFunctions/quitChat.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/setGuestInfo.ts b/frontend/src/chat/chatHelperFunctions/setGuestInfo.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/setGuestInfo.ts rename to frontend/src/chat/chatHelperFunctions/setGuestInfo.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/waitSocketConnected.ts b/frontend/src/chat/chatHelperFunctions/waitSocketConnected.ts similarity index 100% rename from frontend/src/pages/chat/chatHelperFunctions/waitSocketConnected.ts rename to frontend/src/chat/chatHelperFunctions/waitSocketConnected.ts diff --git a/frontend/src/pages/chat/chatHelperFunctions/windowStateHidden.ts b/frontend/src/chat/chatHelperFunctions/windowStateHidden.ts similarity index 85% rename from frontend/src/pages/chat/chatHelperFunctions/windowStateHidden.ts rename to frontend/src/chat/chatHelperFunctions/windowStateHidden.ts index a38ae54..27d45f9 100644 --- a/frontend/src/pages/chat/chatHelperFunctions/windowStateHidden.ts +++ b/frontend/src/chat/chatHelperFunctions/windowStateHidden.ts @@ -1,8 +1,7 @@ -import { __socket } from '../chat'; import { updateUser } from "@app/auth"; export async function windowStateHidden() { - const socketId = __socket || undefined; + const socketId = window.__state.chatSock || undefined; // let oldName = localStorage.getItem("oldName") ?? undefined; let oldName: string; if (socketId === undefined) return; diff --git a/frontend/src/pages/chat/chatHelperFunctions/windowStateVisable.ts b/frontend/src/chat/chatHelperFunctions/windowStateVisable.ts similarity index 89% rename from frontend/src/pages/chat/chatHelperFunctions/windowStateVisable.ts rename to frontend/src/chat/chatHelperFunctions/windowStateVisable.ts index ed5108f..6ff315b 100644 --- a/frontend/src/pages/chat/chatHelperFunctions/windowStateVisable.ts +++ b/frontend/src/chat/chatHelperFunctions/windowStateVisable.ts @@ -1,4 +1,3 @@ -import { __socket } from "../chat"; import { setTitle } from "@app/routing"; import { updateUser } from "@app/auth"; @@ -11,7 +10,7 @@ import { updateUser } from "@app/auth"; export async function windowStateVisable() { const buddies = document.getElementById('div-buddies') as HTMLDivElement; - const socketId = __socket || undefined; + const socketId = window.__state.chatSock || undefined; let oldName = localStorage.getItem("oldName") || undefined; if (socketId === undefined || oldName === undefined) {return;}; diff --git a/frontend/src/pages/chat/types_front.ts b/frontend/src/chat/types_front.ts similarity index 100% rename from frontend/src/pages/chat/types_front.ts rename to frontend/src/chat/types_front.ts diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index ec82683..4a5e931 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,6 +1,6 @@ import { setTitle, handleRoute } from '@app/routing'; import './root/root.ts' -import './chat/chat.ts' +import '../chat/chat.ts' import './pong/pong.ts' import './login/login.ts' import './signin/signin.ts' diff --git a/frontend/src/pages/pong/pong.css b/frontend/src/pages/pong/pong.css index adfb9c6..0be18a5 100644 --- a/frontend/src/pages/pong/pong.css +++ b/frontend/src/pages/pong/pong.css @@ -6,38 +6,147 @@ @tailwind utilities; -@layer utilities { - .gray-color { - @apply border-gray-500 bg-gray-500 - } - .white-color { - @apply border-white bg-white - } - .fit-all { - @apply - w-fit h-fit - } - .blue-hover { - @apply - hover:bg-blue-200 - hover:border-blue-200 - } - .rounded-elem { - @apply - border-6 rounded-3xl - } - .circle-8 { - @apply w-8 h-8 rounded-full - } - .base-box { - @apply - flex items-center justify-center - } - .focus-elem { - @apply - z-50 - shadow-2xl - text-center +.btn-style { + @apply + w-25 + h-8 + border + border-gray-500 + rounded-3xl + bg-gray-500 + text-white + cursor-pointer + shadow-[0_2px_0_0_black] + transition-all + hover:bg-blue-200 + active:bg-gray-400 + active:translate-y-px + active:shadow-[0_2px_0_0_black]; +} + +.chatbox-style { + @apply + w-162.5 + h-75 /* increase height if needed */ + p-2 + border + border-black + shadow-2xl + text-left + text-gray-700 + bg-white + rounded-3xl + overflow-y-auto + whitespace-pre-line + flex + flex-col + mx-auto; +} + +.system-info { + @apply + h-10 + bg-gray-200 + text-gray-700 + p-3 + rounded-3xl + mb-2 border + border-gray-200 + text-center + shadow + overflow-y-auto + justify-end /* 👈 forces text to bottom */ + relative; /* needed for overlay */ +} + +.displaybox { + @apply + fixed + inset-0 + flex + items-center + justify-center + bg-[#43536b]; + +} + +.mainboxDisplay { + @apply + fixed + top-1/2 + left-1/2 + -translate-x-1/2 + -translate-y-1/2 + bg-gray-200 w-212.5 + p-6 rounded-xl + shadow-2xl + text-center + z-50; +} + +.mainboxDisplay button { + @apply + cursor-pointer +} + +.pongbox-style { + @apply + h-112.5 + w-200 + bg-gray-400 + text-6xl + flex + items-center + justify-center; +} + +.text-style { + @apply + text-black + items-center + justify-center + min-w-[2rem] h-8 px-2 + rounded-md border border-gray-300 + bg-gray-100 text-gray-800 + font-mono text-sm font-medium + shadow-sm + select-none +} + + +.pong-field { + @apply relative w-200 h-112.5 bg-black; +} + +.pong-bat { + @apply absolute w-3 h-20 bg-white; +} + +.pong-batleft { + @apply absolute left-4 w-3 h-20 top-0; +} + +.pong-batright { + @apply absolute right-4 w-3 h-20 top-0; +} + +.pong-center-line { + @apply + absolute + left-1/2 + top-0 + h-full + w-1 + -translate-x-1/2 + bg-[linear-gradient(to_bottom,white_50%,transparent_50%)] + bg-size-[4px_20px]; +} + +.pong-end-screen { + @apply + rounded-2xl + absolute + justify-center text-black absolute text-xl @@ -115,3 +224,36 @@ absolute right-4 top-0; } } + +.pong-protips-key { + @apply + inline-flex + items-center + justify-center + min-w-[2rem] h-8 px-2 + rounded-md border border-gray-300 + bg-gray-100 text-gray-800 + font-mono text-sm font-medium + shadow-sm + select-none +} + +.pong-how-to-play { + @apply + inline-flex items-center justify-center + rounded-full w-8 h-8 bg-blue-500 + border-10 border-blue-500 +} + +.chatPopUp { + @apply + fixed + inset-0 + flex + justify-center + items-center; +} + +.hidden{ + display: none; +} diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts index 7bf44fb..5f0e07d 100644 --- a/frontend/src/pages/pong/pong.ts +++ b/frontend/src/pages/pong/pong.ts @@ -125,6 +125,7 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn navigateTo("/app"); return; } + if ( !batLeft || !batRight || @@ -142,7 +143,8 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn !tour_infos ) // sanity check - return showError("fatal error"); + return showError("fatal error"); 👤 Chat + if (!how_to_play_btn || !protips) showError("missing protips"); // not a fatal error tournamentBtn.addEventListener("click", () => { @@ -179,6 +181,16 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn how_to_play_btn.innerText = how_to_play_btn.innerText === "?" ? "x" : "?"; }); + } + + document.addEventListener("keydown", (e) => {keys[e.key.toLowerCase()] = true;}); + document.addEventListener("keyup", (e) => {keys[e.key.toLowerCase()] = false;}); + + setInterval(() => { // key sender + if (keys['escape'] === true && protips && how_to_play_btn) { + protips.classList.add("hidden"); + how_to_play_btn.innerText = '?'; + } document.addEventListener("keydown", (e) => { keys[e.key.toLowerCase()] = true; @@ -324,6 +336,7 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn queueBtn.innerText = QueueState.InQueu; socket.emit("enqueue"); }); + LocalGameBtn.addEventListener("click", () => { if ( queueBtn.innerText !== QueueState.Iddle || diff --git a/frontend/src/pages/root/pong_box_image.png b/frontend/src/pages/root/pong_box_image.png deleted file mode 100644 index b9a870ea7987c8c53b0c207e245053b9b23d4e7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25973 zcmeAS@N?(olHy`uVBq!ia0y~yVB%z8VD#W%V_;yo)t9@5fk9+bRY*ihP-3}4K~a8M zW=^U?No7H*LTW{38UsVct*f&l%br+Nwcg*zrO)_zgG|r!3ih~t`?=jTmaaMR?p5CY zh*d!bnv)p*J>*+(=HLAPcF*?z@X~Y@4qmzQY0l3sU9Mh$G)z3O5NW@->?7t`1wzT?SsqtB{#$lt&I?9IF9zW=Ik{r`WV zZ1It+f0lawm>D*Ezn%TO@|S-XeAhp8{l~xGCrax?-}HZew}<V6+u{{8Zg0{^f{SN`y5wXMi6ui?~P6R>hhWwr*}e^c@D_T2IvFIUAL(>uQO z-bVw`>-+!a{(IN@e8u!R!8*T%O`kozI`_;Wt4r(o^}0_T(O&VA?Z@B0zo&naI+3s| zah=YF+llksd!E#ZhVSU)i<$AGX40EeRkjTRh9yZxdJ`CD$9m`)FMd>8*yj)?bY#hD z{)_xZ4}v~*d;XGeQnM^!PcHT^{2ny_Pq9=8@5KviW-eQsvSRWY3E{87NlV+RxHOxN zTTTrL_O@+bbMExcl!L|k3y)0l(pwr59QD%lN<{G4tyiL=-=5vhP_x`^{opu9_|Qf=mxIZvk;8cA&b{BpSGGnp=@s+>F7+_%isyL*-|u6wOK$D~h{d-YqfLs6MMkqakm5z`1g zc{1WlaP5R9J<08bzs&5m2UzdqvfORV-1NNXfCv}Mp2!8(8Ov<{2C$sIlzZ>pLW_5f z_nEgO)EXL`TEV2}{wrty()9m7)6b;;S>aRcy}9rG)}=Xj=j=VTddHTxHp_4BZCCbb z$qHJj{59&$;)T37*4td`CTz*&da-3A z$-T^Wy5!QrNZW;a9P0}WqZa;j-8q?6Lfp~&wXTXoM45}1>Y>X^g<5;0Ix5e4F7Uaj zA+Y}x4|^)Z!t>X(FQ!c}ip-gDBU+BR@sn}1Q%FYem1A1UyVtB)kk@o_os$2uPcDC+ zvv@hbs7~D?l)+aMF1G&Hq4@iJ4hDap3OBhdD+raSNNP(xwMFNbqO$0pds`lO?wh(- zNY(PX#LPeqwo^OTUlCbe5*_6mt>P|z)yIKf;ZWulE?zI0gHKD+823aPpY3cY@ZgnD zS-3j$quZ+_y^B5-S?>*&D`?*|D_dJxc`E4GWR@O3vk8qSzPzfH=3I9Bh^0c&i`rNF z-R3Xb*q||GW8Y(QTV~c1a@V(MKPf&w>!sGu7b-`C1S2zMG)VBU^jc1;{*o~_C0FpP zU=!E&ucqGmmPdlEg;oEQDf||go&MyG+exz)!%8Domvg6>);k=mo0a%*>zg^(Gi;uA zT)$Mn8aY#ZZc4WD>1b>v~9KSgTga=5njW!GMToem*iAF${KpMJBpaGU4Uz|S2_XKD@! zsR*1YxcVX0>tn_K>#h>>RO`>AK2+=z&*i9=d$gd0F zd{Ftq|IKgpDOOEpNu?b@N@uwf*`9?kiv&Eo+is+>!BwOq{d{Np*4j;>#s1!!Yh0iG z7RYJ5{Kav}AGtRr4@;P1ADF+8=$6r+qo~EPh|*Ej)@bPMSN$nt#B2YJxRBB!p_`dLAM>h@tv4tz}we#?=#DlgANyc?yqQ`sM@x% zs&7%-F4ak6WsI+-=HZQ@Bvnv-D_V5T=?iD7r%-c&Gv#TdwG+xSmi4^5*iCiM1#!Ei9JVrUoh!4;G)&wmEhDdm>m;9u zPDSd$nU{2DIDSoD%)FQ7NBR??w{2&S-%@ULiEdzcmbu&R;T;RJl!;S?6vZ9fy%Njr zi0RdM$WKpPVasssr2pYh*3;ikII*Vu(V^!(qOzZo_RIfu`o!&%iSat*70t$r7G z%WrPuHt8uRd3ej7h-@uu{k=tNO84w@OZu}WZfMBfZI||-i9;)uGjei|)v5^zMlw@a z4ly4|bN+p+EkP|Py8iXD_0>$G9IsW+X1uMMDbp%cV0h)l5ys`cbJ=fb%&WGZ!1w*x zI;E^$FB#KY(g!vQlxpTSKVX~~us~5g=7rBa@e7Z7!lWaZa@Q&DoATwoChx<>vV=5_ zN&PW9JOWQ0>uuzGHL>milgouw3d_v;q&55%Ck0)6_EE@yAyt%d?k@SOoSQF}R;oJR z`MqX};8Y(eFPifwQTJ{I9mE)f>Nh zWbFF((%{RN67^Rf{_wbViCv#p`%Z*6XdmCfYm9vdMa((3X(!D#a;<)0%)(l(I3@61 zhQyYD@&=BicO3nRfl)p}u4})=f5&4p)8qpive|a> zRwy^ga$ne|u-8VX(?VpG^N|Vz`=obEd%qo0{i@@AJSa0qXMfhspNTq`njXiju$jkl z^O4xanIS#54sm|WWyo6jM?=WLYT^7wOY6N3o4sdUaa!;ADbgsmsqvibj<3v~s=r>8 z&AZh+)v3RM|7yRD$JV&af~U_G^PNerEcm%SVc+8eem>K>Q>57cPSO4po+a47{qmWm zv2Qkhkg_vdyioR>%ozuv2kqe}H+3(RTcwi8F4EUI`(FMEEv*Od9xvv-uxu?$%38&& zjt5--ekO8yOjeuBsO>mAz-McBp6!?ZP1#48UY|&rqU^xEDQD)6nR6CuoJ=U;k}(` zA8@fVkwa#kc6CqG<0oG)-COH0|N6CQahrKI^WRkpZQZpkJZ90743mWlS1+{OzmmLg zhHV$uYMEy%F7=W?io{Mw<>jTBG~!`s2m`X`6z*U5z*9 zn5gD-mPP;6*sFGOQTkg?=DNWCgY5_2n{*4GN|RDO)Z8MzbIYWX!o?5e64$?-c`U1K z5696>4Tp9o`uA* zA@RIiqO4JsDDM;wv+Lj2)rv4p)1G>0QBx^9ca)Ww<0m&?Zl5EQwlG_MzoOmkJI&?! zg(nBM@N=(S#h)pCsM|s_rqM}pZIzYY$|jrJK0IQf?OS*~=6W`sU-{if@Xo1Tj>MUJ z;uIN9xWr5{YYvW^t9|}vqVfTg8U0^+c$nvuJv-MV`t}E#myqXVDgTa+gQ-U^zq|BB z_2um?&xO7JH|Dv%qIjn_#*`seprP zna2`-hfsE2M#qB*+CO#|dx;+qv#{upTHI*eQ}y_i+_aXO z|Dvj5*Fndfrrb_S+c=jx-&)8ycgF0m28&s?eObWkE8(rTF!=yO)0CC-7qu>Yr{A4$ zH{d|Ef6I|u85Jp9b#od_H%Q%d-2b5Fh|+>gXHRA~4(UrWTSLWeg`7RKtm&q$Trp4B zdAHal7dK9ojBr2OVDNLsvi*w`gt909+IhwP7SDWE-Frq{@_#PHMXmaEn!ks2`XaB? zI?l_wK^!;PnbYR3td+>(WL$NY@zpf9ZH*V6&ReeKT{Qnfb(-}jn-%#NjyC1=cr3qg zgYB-|@d=81^zE!#-D4vDXC07ymCPx2*-_5rI=hU_7bBZe#fyVdS2sijnKfn+6f^qsv6FRSFm?5>@PEym6TrEeE9g`MCVT4 z1xyFi97G-zXRL8h$vND@q`IK+e!{kIvEJ;3HHM!gdtcqN`*pld$*gma*^MpS4XUej zxL?<>-i$eLZi3UnC@ZHUI^0g{q_p_%sLL#VyC(PawnfjYyoBb=jr4X7OS-l|RYFIh zu=e^g1Gy8s19ZESD;xIj(N=9pE55~ikS}xE72)pb0t`#I?ECs}39}@v*z;0>L4tFq z!_<4*BSq~0*o3UV@NjeH|2<*9+VdxVV-sKI-|>E;%Oth~i(ght`K$;!uG4&KrsC~A z6Pp{lXQlsMx0iKy>Kw13eR`~n%#QCdx;yb)T zSoX9wWiS*tsJ~K;xpZGFVh4NcitRtPS6&l{J)_BUUpA7s`%y84P~x@0Z&=ZJifm0Vg24iKB>o|r#;;C z^Ww3Sujgi5-zHj@?vXR)&S{rL*{hkJZq#>ee0cSb)v~L{+Hc+|pZ6l5zd7f(O=al$ zOYIYacjo=cX_mOBQ6PK&@Rs>)KVSPC%3r)f#>DSH?arKkJ6`Ng+pAu?*2>2E^n25K zdH3HjZq8fV5@mz<_Xj=Mm!ADMvGV?+?7Ft^$uj>FCGK;Z>~Bk&(!K9_u+ID+3mdlR zO9t4lsNrr6oV+#Z`sAIvZ-0NU_|NLAS@3)~=zV(fu|Lax zuzljld3S_cns1)*%144}x0FJeUu_NCmv7r1_jA3}``cHu{(XGCZvX$xf0x<+*jEaO zR(}bXc~;M8y>?23g853&P-aV}vvYu_vomb;l7XROPVGcnkHZcUZTFXXxoUBhO?Wh6 zp;e@Y)}s}n8cwYN+FM>F6up!;F;vmi>ubtBcyRvFRn6U-*YT}uQux9ANY6rZmq+J) z)mAaXdxdunSKt3#^j%iLWR=h6T|y1ZZl|eD6)FBHYWStgfGtB!VCu}&GhRVb`yRiR zW88QDXyvr}yYmqhW!bR020W03o(^wCP++Yk2m%Os{os@kc`8NX~g(jOCUQ};Ng zDDuHfLF3somHPy`m-2;K&6w)jTQSLES(epPZp;4AbJ26QX7z4;cQRf=@5O2FnB}HD zB4QgjIF&@M1UQPCJ=*emNyLc@m6wjom;M#nDN+=A;Dlj6Z^ndJ`ycXu-mMLtBk_IG zALt6 z!i$p=d~Z%|+R~CFSYf|7{}glcc8B+?u5Z6)=_lWy^Hr;sSw@CwvFfR~&z0r=>G%Jw z&HVbFeeQvx9X@g&rZO<_N@a#bltlRYSS9D@>LsS+C#C9DBQBlHCStb$zJphgs>q}eKEl#~=$>Fbx5m+O@q>*W`v>l<2HTIw4Z=^Gj87Nw-=7FXt# zBv$C=6)S^`fSBQuTAW;zSx}OhpQivaGchT@w8U0PiAzC20cvLcqYE^#d@!LOq@q_QAYKPa_0zqBYh)wL`&uS6Nyh?Hcw{({n? z9I$s%lJ!$_Qgc)DN{aOj^$bz0bocZPfa?GSL3(Cx0a#Z>ZUKtQlFT$jV4?U1Bm?#i zvO_9z3*hFWsD}9+tQZ_dRxbI;r6A{dy4Wg#?6*qEPtHuS0y7hn%u-B^k_>ds%?(m@ zO-#)bbuCj&O?6ES&5SJ#jSY>=Et8Rq^2{qPNz6-51sPS5TcDSjnPO#XVw`Gjo|>kc zVs2uoYm$;=uA7vUWUgywl4fa?Vr-CPVr+?IgnvX3= zo9UXQC7bJ-q!^~?TBey=>Kdk*B^jEeq@^Sqr-F@2Nw#v!FUn0Uu~o{?@u03i~PlUS0LUzBUBvGOm6u`;WDlr*i zNpWIXY6{pQ1(;NFMq*xiYKpBAG(o|{GqFULfn~B$N>XZ?uCa-^v95`kd8%$=qKUa~ zqJ^im8z~s_Dh~X(i=}MX8SIsd*)~O75At1z_JOXn=!K6IFG2Mk*+r42(>5 z4UKdS%|Z;#txQd<3=Omm46O_dl=LB9w9yCUYM760^fAH&q5!EFu;WsIhy}U0*>TzE zgUcdNxdbr~R4&jGL*s&$RwyWpT0&Czjt19gaFG-OBq<(EU8BK8QV5Wwcrd@k-o)wd77Lw~$E_vXyI zbJ_Pk&phwWC2#7CXbecl z&9}Da_iNSh?o{l^IiaX$F4NPW=%Ao9d0YPdzUGgHcLZEqbyc?1%uRJsdQ{FT=-kAZ zcsIJ(UqEobc87$}r(|_kh&Z;83A`9^=4RLq|M_+n1rHc*mDkR$?myv~dw+|J?`*T9 zudc4%Y1kn%BREuOmT7jI(Yk#+94>zAYOik(Ja$$5>@3sA#}tx0HpI-gs|~Q8@ZCdL zRBbDB^kV<{c6S6^csQq77Cd11E*7YGyU1hk<;dMabe?oA@29) zaI5g|4W?BqkDd?Rb)$C4vSmtkc5)v-e)RD5b$yxiUAHbzsf|TRNxF^Ml>bHa;^|7J zrlOaZ`=9^5`{m{3%BH4M9UDP5&rwuZQR~3Z%^kdySJ0Vh;;Hx_SyQ`LW(53h*k@X` z^5B6)jfM}lUVUA%;wvw-HgDKq0J6%|)b#uFrQLUr?<#$LBeKUQOwfhz#k_g*oZc%> ze13Z3<%JVFrm`q$*I&H->6gB?SZnj7W)uDwM`?g8((BNH4#j%bOc+&+-j%UpH*UGsyNFEb4q zdL)hAb_g%o_+3fA668sS>?z%EV>c??W;4r{y=Hz%BAjQzc{cu&ywR@d{QVq$6E_R5Kgwf*k@X{Xr6Qsh*hHpe|AM1)~M`1-ht zi)YV2dM=_X9{nkG{g$M}mj@f(h+AE?TCn=8RI-(TW@9&$;imCXwf1N z^M~={JxAl;T-)_;^DFb{%<=QE{?cLTJT>aDJZ7uC5?ejTN@7qb2c zKeWJc+5SD#^kTanr>`$=wms@t?D1st)aJ)U0@|EUwy62eTH-Z(QHbjU8$p*N6C7Nh z^|W6PJX&}`Mro5d$Po6{Mc0Jqg7S42%htTZk4r;7&&iy}ac`a{NVSbjOKYp5scC3> z(acLn-+ivj(JpM8srz%Eg4UgRoH!W*{k{vvJeUBb&X8-*6 z-GhV8JF36y8C_m~;JST-w0G)kvo(kA?k@M;wzKT*tk`XPXT9g1_@1FoJR)8G)c=%+ z{byEN)I3nHYddME&G~Tgjm38-el)ZB9VT3K?%LPy2WJ>2zp2`G?t5!-J9AirM~jGE z)}%9M9iE*NbN%p+<@I}R_nq(7fBT*EujhPa&_jpcee+XK9gJ2usdBIFc|<6yyJXpn z_uS7##l)6*Noz^nD4f{Q5y~QadAWbOX=8+^!e@(e+mE$@6&sjbyf#j9P_sOoTXEz& ztDvFEgt-Ops#<5-2uxiug_r~c`3%VtQigw!kW{9u-I`!(j_{A3=yt=x2 zN9}L3e?K1cFIl$i(Xn3X3mGOm%HB#D85v!8w)S6*`3#Hfk}J1AbiC&d+IBvIQ7KI5 z%Z^>U4qd%^HD~wD&FPPy&#yoB@$vDC>rOlDv;ML##^QNX@#bT?Q)=QZR zwO5zTc=r3yPP@Z%dZ$VV>4=Dlwav9IPjZNej@dos1h12yv_MdBFz>YK%l+r~UAYo+ zwTa0xJYdKF9VRxmwt_M;Jr5ovR8&=MVshc(^kQXWQ!+CX^L-WWd}rDTr*ej!1xfOH zb8Z}Gc2(rtowa4*A!)G)Mx{0u(J!yAav$%L)fSnqkP}#^cKpWqXI3kohKTIA-`Syf z&`D2E@5atz^-XzJ?;jZG*o(``ShROY2)#+Tx+-+X?8<|h*XKS>S5ivy*s$s9>hQR=-N$*qwNA@X&>W&Fp=LL`5tL_*VX%dyMZK52tI;%TIP)`iec$=6v?+;-84D z?|=Gyk*t_S0bgf`;EjVbC4_u5!fr7KFZT=mGNYk^VYy|%hCe;8+c&pfDbok9UUBK`>xZn35}mFe z-{!XX;L&dJiIXNZEm@)xv$N=ErgKo)H|04tQteuys}@9U%~}z+*GemFO-FcKC2Re^ zpX%%4_Rjikx1;DO*QHCB7VP}0eyilwNq!4S+sX$&q>fBbSP*<7;m!U1@@Hq63TM54 z_bzX1=}ocCJiSt;P7x6j_kNijN{Qy@=U-f_a_Q2g4HX}gjvPIzxS;U2)8_Tta$=go z)~sG-5Z@Zn**{G%f`Q?9HCz5noDf9CZ1qgPf2Kg?Itn00x% zzx-9LEyw)Dh_6MuX=y>UnF z-!+%t&VKEAyztwb$eOQL!&lk8FTY>gzVm)jsLG-_vfSgkKeyn zf7_#{r#HhiTdeH;ovtaL+kZBy&s}hU@7$3I32tFw(+X}IrJh=`>CFCjDv$sC{48OR z(BK=lc=c-SYwKdW#n$g_+MzpSh&GHA=pX$D`r_wkkCT7hhp8ERwH+Of7^XIGI z{QLFZ<(MO#!s-{+#age{J$(DN^zCiA&9}S%oC*H7;Q%Nnr5ySD`+MtcyZ?Vai|Kk# zp4b;<_jF^;&!XLXu3fkgAY)y|^Su84>kYq^)6Uv#$^58wul9TFY@PSN-|s)ZpMU-B zw_o=~d=gYr&K19-q@;9VL7%#b+Ae_qB7YFro={HOS-$OG~?!`rM}a5Cmdudd3}vHFEnoJ z%OdUY3bEwlZZ~SIzrVjPzez{=;}z37#tEJ`Z}xw9-I)8~)2F80yRBD+u4XGQFW<3i z*CMZ}T083h+pUY;-4?w)@9DkFsq^ObO_?IH=VO|EoKr zBQ~e`<{h_m-;i}x>&v^l()zo8TAs1;on>(D8KdI%CP86ldwcuYcOG#uEd5u^pM1P7 zcYCgF!U2Yqny;ek?*3_yXIhf{`4%e&508t72gk23FPVeOuQNEzwJPQL7qj?c!oeoi z8+)tG<8p6Ilb)t{&{A_}%Lf$|o5dEL`mwvVI4~xjoTPeVU#+!X%npY9{l9+ZrQVj> zDF}-DK45j2E{}{wK;hMA-TM0+4(VTe|KO+lFUd372mVMF78XYA zF4Og!W6^l#jL)8{&gFcQR-Y6ve0pd7hmRi}3knSEemr2FeBQk5O+?AtTT|niSQtER z$hGdP{r%`yX{p}+ox64={rT}x!oIHN-Q2MLf3=C-5x&xwi3iqk7@E_;34jx$Z;r9}Dncd_wj|y^8$KJ% zGSBbZTm7ADz5DjOyAw*jiLc&w&DQ#w*1QC5EiJCb#>O9s&l7FJw`oajdoxF0fuSKh zG@fPadshGZY&%MHxux?+ivpX|m&pir#IR z_5H3&N}TP&SH<=}=5r?{CAB!6-jaDaXJ769-|ws!mQ9>E(Ttyihm$Gm$_m9h-;VW4 zcg?f@{ED;Y(@FJ1Z~5hHIvN@n?$kxdLDt#bloYr$;ZkD2=VujzX?9FeZ-~Fs_c^Bp7?R_|Xer?+H?+ngO2WGoXaa$~y z_xtd!)*t_Vzi++0K4K%2;N*R7i^F1ds%%r^G`er|cP~!&|Cm_W+|;Cyntm(ww|@M- zI|-)C>;M1TF>7Y-#|O5mUQ=AWjwwHEE}pFBTkzDsWIDE z^wcZo=8hv->vOO7Yi}%Ddn5JE#l`N6-S&o+-R9@vRMVLI&vHer{=%}~kIlH^(>#29 zjvSY-?-5e0-UR+$g&oaltg- z*7mP_yu7Zf%dX8_p!xEbbI#38t@8hr&RpJV;npkl)Zq2z)YD>De=+_2xnjwM-?tAR zcK$jsW^3)=uh*T_JEhI@jws(KZ!vCTQ8rs2yWmCCpG~Q!TW)`Qdpmr`+Yb*9UtIg0 zWA)y1?iGU4=6NDnzYns@DDmKTE9W*)z9=zA3h$J*RG36Nl|ffb`D$}rpscM)hf7H{q>)f zF*}Xos^6Nnw6{P0@$qrj{QBd4vKGIJ*J!OTys{$D>926r*H^9S|8}nYWmZ=Cjd53r zVg1dB#TP{~;#c^BMw_;M_u66e+reUMuY~PQwbvmV*DP5v`BrrPURM73|7Yc@TWt(H z$9PaIc;ma26tZG|F5O?_czYg?f<8& zT5`&0eXN&D&YGY42fD@elRkZEHJjPved=7uoy4n?IOjb(|08Bc?vE9otM;C&ydPD+ zVbzu`CQFtseRyuJ^@R)*&|pbgnwpW35vYCG)5Ejn{jY6z`PsttWe#3j8!c*^c56!} zC=-eH%NM=dZac%GaM7aEJJtQ?x!m5CdvWRWg6{b2S4S5g65agl?Ciy53#^* D2Y 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�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 diff --git a/frontend/src/pages/root/root.html b/frontend/src/pages/root/root.html index 379444b..7d16696 100644 --- a/frontend/src/pages/root/root.html +++ b/frontend/src/pages/root/root.html @@ -101,6 +101,7 @@
+

Embrace the Ball Lifestyle