update: friends list + button

This commit is contained in:
Maieul BOYER 2026-01-16 16:47:41 +01:00 committed by Nigel
parent 590604b385
commit c5eea6e29e
8 changed files with 156 additions and 76 deletions

View file

@ -37,6 +37,7 @@
<a href="/ttt" class="hover:bg-gray-700 rounded-md px-3 py-2">⭕ Tic-Tac-Toe</a> <a href="/ttt" class="hover:bg-gray-700 rounded-md px-3 py-2">⭕ Tic-Tac-Toe</a>
<a href="/pong" class="hover:bg-gray-700 rounded-md px-3 py-2">▮•▮ Ping Pong</a> <a href="/pong" class="hover:bg-gray-700 rounded-md px-3 py-2">▮•▮ Ping Pong</a>
<a href="/tour" class="hover:bg-gray-700 rounded-md px-3 py-2">🏆 Tournaments</a> <a href="/tour" class="hover:bg-gray-700 rounded-md px-3 py-2">🏆 Tournaments</a>
<a href="/friends" class="hover:bg-gray-700 rounded-md px-3 py-2">😎 Friends</a>
<a href="/logout" class="hover:bg-gray-700 rounded-md px-3 py-2">🚪 Logout</a> <a href="/logout" class="hover:bg-gray-700 rounded-md px-3 py-2">🚪 Logout</a>
</nav> </nav>
</aside> </aside>

View file

@ -1,7 +1,7 @@
import { showError } from "@app/toast"; import { showError } from "@app/toast";
import client from "@app/api"; import client from "@app/api";
import cookie from "js-cookie"; import cookie from "js-cookie";
import { ensureWindowState, isNullish } from "@app/utils"; import { ensureWindowState, isNullish, updateFriendsList } from "@app/utils";
import { handleRoute, navigateTo } from "./routing"; import { handleRoute, navigateTo } from "./routing";
cookie.remove("pkce"); cookie.remove("pkce");
@ -105,10 +105,11 @@ if (!window.__state._headerProfile) {
window.__state._reloadOnAuthChange ??= false; window.__state._reloadOnAuthChange ??= false;
if (!window.__state._reloadOnAuthChange) { if (!window.__state._reloadOnAuthChange) {
document.addEventListener("ft:userChange", () => { document.addEventListener("ft:userChange", async () => {
// if the last forced auth change is less than 1000 sec old -> we do nothing // if the last forced auth change is less than 1000 sec old -> we do nothing
if (Date.now() - (window.__state.lastAuthChange ?? Date.now()) < 1000) if (Date.now() - (window.__state.lastAuthChange ?? Date.now()) < 1000)
return; return;
await updateFriendsList();
handleRoute(); handleRoute();
}); });
window.__state._reloadOnAuthChange = true; window.__state._reloadOnAuthChange = true;

View file

@ -39,6 +39,7 @@ let keysPressed: Record<string, boolean> = {};
declare module "ft_state" { declare module "ft_state" {
interface State { interface State {
chatSock?: Socket; chatSock?: Socket;
friendList: { id: string; name: string }[];
} }
} }
@ -82,18 +83,20 @@ const sendButton = document.getElementById("b-send") as HTMLButtonElement;
const sendtextbox = document.getElementById( const sendtextbox = document.getElementById(
"t-chat-window", "t-chat-window",
) as HTMLButtonElement; ) as HTMLButtonElement;
const systemWindow = document.getElementById("chat-system-box") as HTMLDivElement; const systemWindow = document.getElementById(
"chat-system-box",
) as HTMLDivElement;
function chatKeyToggle() { function chatKeyToggle() {
let anti_flicker_control = false; let anti_flicker_control = false;
const chat_hide_key = 'escape'; const chat_hide_key = "escape";
const chat_display_key = 'f2'; const chat_display_key = "f2";
const home_display_key = 'f8'; const home_display_key = "f8";
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if (event.repeat && keysPressed[chat_hide_key] === true) { if (event.repeat && keysPressed[chat_hide_key] === true) {
anti_flicker_control = true; anti_flicker_control = true;
return ; return;
}; }
keysPressed[event.key.toLowerCase()] = true; keysPressed[event.key.toLowerCase()] = true;
}); });
document.addEventListener("keyup", (event) => { document.addEventListener("keyup", (event) => {
@ -102,34 +105,33 @@ function chatKeyToggle() {
anti_flicker_control = false; anti_flicker_control = false;
} }
}); });
setInterval( () => { setInterval(() => {
if(keysPressed[chat_hide_key] === true) { if (keysPressed[chat_hide_key] === true) {
overlay.classList.remove("opacity-60"); overlay.classList.remove("opacity-60");
chatBox.classList.add("hidden"); chatBox.classList.add("hidden");
chatMessageIn?.classList.add("hidden"); chatMessageIn?.classList.add("hidden");
chatMessageIn!.textContent = ''; chatMessageIn!.textContent = "";
profilList?.classList.add("hidden"); profilList?.classList.add("hidden");
windowStateHidden(); windowStateHidden();
} }
if (keysPressed[chat_display_key] === true) { if (keysPressed[chat_display_key] === true) {
anti_flicker_control = false; anti_flicker_control = false;
chatBox.classList.remove("hidden"); chatBox.classList.remove("hidden");
overlay.classList.add("opacity-60"); overlay.classList.add("opacity-60");
chatMessageIn?.classList.add("hidden"); chatMessageIn?.classList.add("hidden");
chatMessageIn!.textContent = ''; chatMessageIn!.textContent = "";
let socket = window.__state.chatSock; let socket = window.__state.chatSock;
if (!socket) return; if (!socket) return;
connected(socket); connected(socket);
sendtextbox.focus(); sendtextbox.focus();
windowStateVisable(); windowStateVisable();
} }
if (keysPressed[home_display_key] === true) { if (keysPressed[home_display_key] === true) {
navigateTo('/app/'); navigateTo("/app/");
quitChat(); quitChat();
} }
}, 1000/10); }, 1000 / 10);
}; }
function initChatSocket() { function initChatSocket() {
let socket = getSocket(); let socket = getSocket();
@ -152,9 +154,10 @@ function initChatSocket() {
!profilList || !profilList ||
!sendButton || !sendButton ||
!sendtextbox || !sendtextbox ||
!systemWindow !systemWindow
) return showError("fatal error"); )
return showError("fatal error");
// Listen for the 'connect' event // Listen for the 'connect' event
socket.on("connect", async () => { socket.on("connect", async () => {
await waitSocketConnected(socket); await waitSocketConnected(socket);
@ -202,10 +205,10 @@ function initChatSocket() {
if (socket) { if (socket) {
connected(socket); connected(socket);
} }
if (chatWindow && data.message.destination === "") { if (chatWindow && data.message.destination === "") {
chatMessageIn?.classList.remove("hidden"); chatMessageIn?.classList.remove("hidden");
chatMessageIn!.textContent = '🔵'; chatMessageIn!.textContent = "🔵";
const messageElement = document.createElement("div"); const messageElement = document.createElement("div");
messageElement.textContent = `${data.message.user}: ${data.message.text}`; messageElement.textContent = `${data.message.user}: ${data.message.text}`;
chatWindow.appendChild(messageElement); chatWindow.appendChild(messageElement);
@ -214,7 +217,7 @@ function initChatSocket() {
if (chatWindow && data.message.destination === "privateMsg") { if (chatWindow && data.message.destination === "privateMsg") {
chatMessageIn?.classList.remove("hidden"); chatMessageIn?.classList.remove("hidden");
chatMessageIn!.textContent = '🔴'; chatMessageIn!.textContent = "🔴";
const messageElement = document.createElement("div-private"); const messageElement = document.createElement("div-private");
messageElement.textContent = `🔒${data.message.user}: ${data.message.text}`; messageElement.textContent = `🔒${data.message.user}: ${data.message.text}`;
chatWindow.appendChild(messageElement); chatWindow.appendChild(messageElement);
@ -223,7 +226,7 @@ function initChatSocket() {
if (chatWindow && data.message.destination === "inviteMsg") { if (chatWindow && data.message.destination === "inviteMsg") {
chatMessageIn?.classList.remove("hidden"); chatMessageIn?.classList.remove("hidden");
chatMessageIn!.textContent = '🟢'; chatMessageIn!.textContent = "🟢";
const messageElement = document.createElement("div-private"); const messageElement = document.createElement("div-private");
const chatWindow = document.getElementById( const chatWindow = document.getElementById(
"t-chatbox", "t-chatbox",
@ -236,14 +239,13 @@ function initChatSocket() {
if (systemWindow && data.message.destination === "system-info") { if (systemWindow && data.message.destination === "system-info") {
const messageElement = document.createElement("div"); const messageElement = document.createElement("div");
messageElement.textContent = `${data.message.user}: ${data.message.text}`; messageElement.textContent = `${data.message.user}: ${data.message.text}`;
// keep only last 10 // keep only last 10
while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) {
systemWindow.removeChild(systemWindow.firstChild!); systemWindow.removeChild(systemWindow.firstChild!);
} }
systemWindow.appendChild(messageElement); systemWindow.appendChild(messageElement);
systemWindow.lastElementChild?.scrollIntoView({ block: "end" }); systemWindow.lastElementChild?.scrollIntoView({ block: "end" });
} }
}); });
@ -278,9 +280,9 @@ function initChatSocket() {
if (blockUserBtn) { if (blockUserBtn) {
let message = ""; let message = "";
if (data.userState === "block") { if (data.userState === "block") {
(message = "un-block"); message = "un-block";
} else { } else {
(message = "block"); message = "block";
} }
blockUserBtn.textContent = message; blockUserBtn.textContent = message;
} }
@ -294,17 +296,15 @@ function initChatSocket() {
const htmlBaliseRegex = /<a\b[^>]*>[\s\S]*?<\/a>/; const htmlBaliseRegex = /<a\b[^>]*>[\s\S]*?<\/a>/;
const htmlBaliseMatch = message.match(htmlBaliseRegex); const htmlBaliseMatch = message.match(htmlBaliseRegex);
if (htmlBaliseMatch) if (htmlBaliseMatch) addInviteMessage(message);
addInviteMessage(message); else addMessage(message);
else
addMessage(message);
}); });
//receives broadcast of the next GAME //receives broadcast of the next GAME
socket.on("nextGame", (message: string) => { socket.on("nextGame", (message: string) => {
openMessagePopup(message); openMessagePopup(message);
}); });
//receives broadcast of the next GAME //receives broadcast of the next GAME
socket.on("tourStatus", (message: string) => { socket.on("tourStatus", (message: string) => {
openMessagePopup(message); openMessagePopup(message);
@ -391,20 +391,20 @@ sendButton?.addEventListener("click", () => {
} }
break; break;
case "@pong": case "@pong":
if (msgCommand[1] === "") { if (msgCommand[1] === "") {
navigateTo("/app/pong/games"); navigateTo("/app/pong/games");
quitChat(); quitChat();
} }
break; break;
case "@ttt": case "@ttt":
if (msgCommand[1] === "") { if (msgCommand[1] === "") {
navigateTo("/app/ttt/games"); navigateTo("/app/ttt/games");
quitChat(); quitChat();
} }
break; break;
case "@guest": case "@guest":
if (!userId) { if (!userId) {
return; return;
@ -505,7 +505,6 @@ clearText?.addEventListener("click", () => {
bquit?.addEventListener("click", () => { bquit?.addEventListener("click", () => {
quitChat(); quitChat();
}); });
myGames?.addEventListener("click", () => { myGames?.addEventListener("click", () => {
@ -527,7 +526,7 @@ sendtextbox.addEventListener("keydown", (event) => {
} }
}); });
chatButton!.addEventListener("click",() => { chatButton!.addEventListener("click", () => {
if (chatBox.classList.contains("hidden")) { if (chatBox.classList.contains("hidden")) {
chatBox.classList.toggle("hidden"); chatBox.classList.toggle("hidden");
overlay.classList.add("opacity-60"); overlay.classList.add("opacity-60");
@ -536,14 +535,14 @@ chatButton!.addEventListener("click",() => {
if (!socket) return; if (!socket) return;
connected(socket); connected(socket);
chatMessageIn?.classList.add("hidden"); chatMessageIn?.classList.add("hidden");
chatMessageIn!.textContent = ''; chatMessageIn!.textContent = "";
sendtextbox.focus(); sendtextbox.focus();
} else { } else {
chatBox.classList.toggle("hidden"); chatBox.classList.toggle("hidden");
overlay.classList.remove("opacity-60"); overlay.classList.remove("opacity-60");
windowStateHidden(); windowStateHidden();
chatMessageIn?.classList.add("hidden"); chatMessageIn?.classList.add("hidden");
chatMessageIn!.textContent = ''; chatMessageIn!.textContent = "";
} }
}); });

View file

@ -1,5 +1,8 @@
import client from "@app/api";
import type { ClientProfil } from "../types_front"; import type { ClientProfil } from "../types_front";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { showError, showSuccess } from "@app/toast";
import { getFriendList, updateFriendsList } from "@app/utils";
/** /**
* function listens for a click on the TTT game History button * function listens for a click on the TTT game History button
@ -8,20 +11,25 @@ import { Socket } from "socket.io-client";
**/ **/
export function actionBtnFriend(profile: ClientProfil, senderSocket: Socket) { export function actionBtnFriend(profile: ClientProfil, senderSocket: Socket) {
setTimeout(() => { setTimeout(() => {
const friend = document.querySelector("#btn-friend"); const friend = document.querySelector("#btn-friend");
friend?.addEventListener("click", () => { friend?.addEventListener("click", async () => {
let friendList = getFriendList();
if (friend.textContent = "friend") { if (!friendList.some(v => v.id === profile.userID!)) {
friend.textContent = "not-friend" let req = await client.addFriend({ user: profile.userID! });
console.log('friend'); if (req.kind === 'success')
} showSuccess('Successfully added a new Friend')
else { else
friend.textContent = "not-friend" showError('Failed to add a new Friend');
console.log('Not a friend'); }
else {
} let req = await client.removeFriend({ user: profile.userID! });
if (req.kind === 'success')
}); showSuccess('Successfully removed a Friend')
}, 0) else
}; showError('Failed to remove a Friend');
}
await updateFriendsList();
});
}, 0)
};

View file

@ -0,0 +1,9 @@
<div class="fixed inset-0 flex items-center justify-center bg-[#43536b]">
<div
class="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">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-700">
FriendList
</h2>
<div id="friendList" class="max-w-3xl mx-auto space-y-2 max-h-[50dvh] overflow-scroll"></div>
</div>
</div>

View file

@ -0,0 +1,41 @@
import { addRoute, navigateTo, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
import page from './friendList.html?raw';
import { getFriendList, isNullish, updateFriendsList } from "@app/utils";
import client from "@app/api";
import { updateUser } from "@app/auth";
import { showError } from "@app/toast";
async function friends(_url: string, args: RouteHandlerParams): Promise<RouteHandlerReturn> {
setTitle("Tic Tac Toe Games");
let user = await updateUser();
if (isNullish(user)) {
return { html: '<span> You aren\'t logged in </span>', postInsert: () => { showError("You must be logged in !"); navigateTo("/") } };
}
await updateFriendsList();
let friendList = getFriendList();
friendList.sort();
let friendsElem = friendList.map(g => {
let e = document.createElement('div');
e.className = 'grid grid-cols-[1fr_auto_1fr] items-center bg-zinc-800 rounded-lg px-4 py-3';
e.innerHTML = `
<div class="text-center font-semibold text-white">${g.name}</div>
<a href="/ttt/games/${g.id}" class="text-center">TTT Games</a>
<a href="/pong/games/${g.id}" class="text-center">Pong Games</a>
`;
return e;
}).filter(v => !isNullish(v));
return {
html: page, postInsert: async (app) => {
if (!app) return;
const friendsBox = app.querySelector<HTMLDivElement>("#friendList");
if (!friendsBox) return;
friendsElem.forEach(c => friendsBox.appendChild(c));
}
};
}
addRoute('/friends', friends);

View file

@ -1,6 +1,5 @@
import { setTitle, handleRoute } from '@app/routing'; import { setTitle, handleRoute } from '@app/routing';
import './root/root.ts' import './root/root.ts'
import '../chat/chat.ts'
import './pong/pong.ts' import './pong/pong.ts'
import './login/login.ts' import './login/login.ts'
import './signin/signin.ts' import './signin/signin.ts'
@ -10,6 +9,7 @@ import './logout/logout.ts'
import './pongHistory/pongHistory.ts' import './pongHistory/pongHistory.ts'
import './tttHistory/tttHistory.ts' import './tttHistory/tttHistory.ts'
import './tourHistory/tourHistory.ts' import './tourHistory/tourHistory.ts'
import './friendList/friendList.ts'
// ---- Initial load ---- // ---- Initial load ----
setTitle(""); setTitle("");

View file

@ -1,9 +1,11 @@
import client from "./api";
export function escapeHTML(str: string): string { export function escapeHTML(str: string): string {
const p = document.createElement("p"); const p = document.createElement("p");
p.appendChild(document.createTextNode(str)); p.appendChild(document.createTextNode(str));
return p.innerHTML; return p.innerHTML;
} }
export function isNullish<T>(v: T | undefined | null): v is (null | undefined) { export function isNullish<T>(v: T | undefined | null): v is null | undefined {
return v === null || v === undefined; return v === null || v === undefined;
} }
@ -11,3 +13,22 @@ export function isNullish<T>(v: T | undefined | null): v is (null | undefined) {
export function ensureWindowState() { export function ensureWindowState() {
window.__state = window.__state ?? {}; window.__state = window.__state ?? {};
} }
export async function updateFriendsList() {
window.__state = window.__state ?? {};
window.__state.friendList ??= [];
try {
let req = await client.listFriend();
if (req.kind === "success") {
window.__state.friendList = req.payload.friends;
}
} catch (e: unknown) { }
}
export function getFriendList() {
ensureWindowState();
window.__state.friendList ??= [];
return window.__state.friendList;
}