diff --git a/srcs/backend/avatar/default.png b/srcs/backend/avatar/default.png deleted file mode 100644 index 28f6a85..0000000 Binary files a/srcs/backend/avatar/default.png and /dev/null differ diff --git a/srcs/backend/index.js b/srcs/backend/index.js index e8c91f7..c7a9be8 100644 --- a/srcs/backend/index.js +++ b/srcs/backend/index.js @@ -51,4 +51,4 @@ async function startServer() }); } -startServer(); +startServer(); \ No newline at end of file diff --git a/srcs/backend/routes/avatar.js b/srcs/backend/routes/avatar.js index 6df63ae..321fed1 100644 --- a/srcs/backend/routes/avatar.js +++ b/srcs/backend/routes/avatar.js @@ -47,4 +47,4 @@ router.get('/user/:userId', async(req, res) => res.status(result.status).json(result.data); }); -export default router; +export default router; \ No newline at end of file diff --git a/srcs/frontend/src/app.js b/srcs/frontend/src/app.js index 555e56c..19d705a 100644 --- a/srcs/frontend/src/app.js +++ b/srcs/frontend/src/app.js @@ -1,8 +1,7 @@ import {Element, MenuElement} from "./element.js"; -import {fenetre} from "./windows.js"; import {LoginWindow} from "./login.js"; import { GlobalChat } from "./global_chat.js"; -import {avatarWindows} from "./avatarWindows.js"; +import { AvatarWindow } from "./avatar.js"; function direBonjour() { alert("clicked !"); @@ -17,9 +16,9 @@ const accueilElement = new MenuElement("accueil"); const globalChatElement = new MenuElement("global_chat"); const avatarElement = new MenuElement("avatar"); // Windows and screens +export const avatarWindow = new AvatarWindow(); const loginWindow = new LoginWindow(); const global_chat = new GlobalChat(); -const avatar_windows = new avatarWindows(); // Actions UI @@ -43,10 +42,10 @@ document.getElementById("global_chat").addEventListener("click", () => { document.getElementById("avatar").addEventListener("click", () => { - // Toggle global chat visibility - if (avatarWindows.main && avatarWindows.main.style.display !== "none") { - avatarWindows.hide(); + // Toggle avatar window visibility + if (avatarWindow.main && avatarWindow.main.style.display !== "none") { + avatarWindow.hide(); } else { - avatarWindows.show(); + avatarWindow.show(); } -}); \ No newline at end of file +}); diff --git a/srcs/frontend/src/avatar.js b/srcs/frontend/src/avatar.js new file mode 100644 index 0000000..81777ae --- /dev/null +++ b/srcs/frontend/src/avatar.js @@ -0,0 +1,165 @@ +import {fenetre} from "./windows.js"; + +export class AvatarWindow extends fenetre { + constructor() { + super(360, 320, "Avatar"); + // Avatar preview + this.avatarPreview = document.createElement("img"); + this.avatarPreview.style.width = "120px"; + this.avatarPreview.style.height = "120px"; + this.avatarPreview.style.objectFit = "cover"; + this.avatarPreview.style.borderRadius = "50%"; + this.avatarPreview.style.border = "2px solid #fff"; + + + this.fileInput = document.createElement("input"); + this.fileInput.type = "file"; + this.fileInput.accept = "image/*"; + // Hide the raw file input to keep only one visible control + this.fileInput.style.display = "none"; + + this.chooseBtn = document.createElement("button"); + this.chooseBtn.textContent = "Choisir image"; + + this.saveBtn = document.createElement("button"); + this.saveBtn.textContent = "Enregistrer avatar"; + + // Refresh button to re-fetch avatar from server + this.refreshBtn = document.createElement("button"); + this.refreshBtn.textContent = "Rafraîchir photo"; + + this.message = document.createElement("div"); + this.message.style.fontSize = "0.9em"; + + this.body.append( + this.avatarPreview, + this.fileInput, + this.chooseBtn, + this.saveBtn, + this.refreshBtn, + this.message + ); + + this.applyStyles(); + this.bindEvents(); + // Load current avatar on initialization + this.getPhoto(); + } + + applyStyles() { + // Center avatar in the window body + this.body.style.display = "flex"; + this.body.style.flexDirection = "column"; + this.body.style.alignItems = "center"; + this.body.style.gap = "12px"; + // Style helpers + this.avatarPreview.style.boxShadow = "0 0 8px rgba(0,0,0,0.5)"; + this.chooseBtn.style.padding = "6px 12px"; + this.chooseBtn.style.cursor = "pointer"; + this.saveBtn.style.padding = "6px 12px"; + this.saveBtn.style.cursor = "pointer"; + } + + bindEvents() { + this.fileInput.addEventListener("change", (e) => { + const file = e.target.files && e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + this.avatarPreview.src = ev.target.result; + }; + reader.readAsDataURL(file); + }); + + this.chooseBtn.addEventListener("click", () => { + // trigger file input + this.fileInput.click(); + }); + + this.saveBtn.addEventListener("click", () => { + // Send the selected photo to the server + this.postPhoto(); + }); + + // Bind refresh button to re-fetch avatar from server + this.refreshBtn.addEventListener("click", () => { + this.getPhoto(); + }); + } + async getPhoto(){ + console.log("getPhoto launched..."); + const token = localStorage.getItem("auth_token"); + if (!token) { + console.log("No auth token found; skipping avatar fetch"); + return; + } + try { + const response = await fetch("/api/avatar/me", { + method: "GET", + headers: { + "Authorization": `Bearer ${token}` + } + }); + if (!response.ok) { + console.warn("Failed to fetch avatar (status", response.status, ")"); + return; + } + const data = await response.json(); + console.log(data); + if (data && data.avatar_url) { + this.avatarPreview.src = data.avatar_url; + } else { + console.warn("Avatar URL not found in response"); + } + } catch (err) { + console.error("Error while fetching avatar:", err); + } + } + + async postPhoto(){ + console.log("postPhoto launched..."); + const token = localStorage.getItem("auth_token"); + if (!token) { + this.message.textContent = "No auth. plz connect."; + this.message.style.color = "#f00"; + return; + } + const file = this.fileInput.files && this.fileInput.files[0]; + if (!file) { + this.message.textContent = "take image before"; + this.message.style.color = "#f00"; + return; + } + + const formData = new FormData(); + formData.append('avatar', file); + + try { + const response = await fetch('/api/avatar/upload', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}` + }, + body: formData + }); + const data = await response.json(); + if (!response.ok) { + const err = data?.error || data?.message || 'Upload failed'; + this.message.textContent = err; + this.message.style.color = '#f00'; + return; + } + if (data && data.avatar_url) { + this.avatarPreview.src = data.avatar_url; + } + this.message.textContent = 'Avatar enregistré !'; + this.message.style.color = '#3cff01'; + } catch (err) { + console.error('Avatar upload error:', err); + this.message.textContent = 'Erreur lors de l’envoi'; + this.message.style.color = '#f00'; + } + } + + +} diff --git a/srcs/frontend/src/avatarWindows.js b/srcs/frontend/src/avatarWindows.js deleted file mode 100644 index f4cc3f4..0000000 --- a/srcs/frontend/src/avatarWindows.js +++ /dev/null @@ -1,7 +0,0 @@ -import {fenetre} from "./windows.js"; - -export class avatarWindows extends fenetre { - constructor(){ - super(320, 240, "Avatar"); - } -} \ No newline at end of file diff --git a/srcs/frontend/src/login.js b/srcs/frontend/src/login.js index 29c82ac..52b614b 100644 --- a/srcs/frontend/src/login.js +++ b/srcs/frontend/src/login.js @@ -1,5 +1,5 @@ import {fenetre} from "./windows.js"; - +import {avatarWindow} from "./app.js"; export class LoginWindow extends fenetre { constructor() { super(320, 240, "Connexion"); @@ -50,6 +50,7 @@ export class LoginWindow extends fenetre { if (ev.data && ev.data.token) { localStorage.setItem('auth_token', ev.data.token); this.message.innerText = 'Connexion GitHub réussie ! Bienvenue.'; + avatarWindow.getPhoto(); this.message.style.color = '#3cff01'; window.removeEventListener('message', listener); if (popup) popup.close(); @@ -98,7 +99,7 @@ export class LoginWindow extends fenetre { localStorage.setItem("auth_token", data.token); this.message.innerText = "Connexion réussie ! Bienvenue."; this.message.style.color = "#3cff01"; - + avatarWindow.getPhoto(); // mask the window after 1.5s setTimeout(() => this.hide(), 1500);