From 7f32eb816c52926c5ddfcc69248cc35dddb79d38 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 9 Jun 2026 15:21:22 +0700 Subject: [PATCH] =?UTF-8?q?C=E1=BA=ADp=20nh=E1=BA=ADt=20avatar=20v=C3=A0?= =?UTF-8?q?=20ch=E1=BB=89nh=20s=E1=BB=ADa=20th=C3=B4ng=20tin=20ng=C6=B0?= =?UTF-8?q?=E1=BB=9Di=20d=C3=B9ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/apiRoutes.js | 23 ++++++ frontend/css/style.css | 43 +++++++++++ frontend/index.html | 20 ++++- frontend/js/main_map.js | 147 +++++++++++++++++++++++++++++------- 4 files changed, 205 insertions(+), 28 deletions(-) diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 43af070..42ccc7c 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -907,6 +907,29 @@ router.put('/me/profile', protect, async (req, res) => { } }); +/** + * @route GET /api/assets/view_avatar/:filename + * @desc Securely stream user avatar images + * @access Public (No auth needed for avatars) + */ +router.get('/assets/view_avatar/:filename', (req, res) => { + try { + const filename = req.params.filename; + const avatarPath = path.join(uploadDir, filename); + + if (!fs.existsSync(avatarPath)) { + return res.status(404).json({ message: 'Avatar not found' }); + } + + res.sendFile(avatarPath, { + maxAge: 2592000000, // 30 ngày + headers: { 'Content-Type': 'image/jpeg' } + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + /** * @route GET /api/me/scenes * @desc Lấy danh sách các cảnh mẹ do người dùng tạo diff --git a/frontend/css/style.css b/frontend/css/style.css index ca413dc..ea10c07 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -422,6 +422,49 @@ html, body { color: #ccc; } +/* Profile Styles */ +.profile-header-edit { + display: flex; + justify-content: center; + margin-bottom: 25px; +} + +.avatar-edit-container { + position: relative; + width: 100px; + height: 100px; +} + +.avatar-edit-container img, #profile-avatar-placeholder { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + border: 3px solid #007bff; +} + +.avatar-upload-label { + position: absolute; + bottom: 0; + right: 0; + background: #007bff; + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 12px; + border: 2px solid #1e1e1e; + transition: transform 0.2s; +} + +.avatar-upload-label:hover { + transform: scale(1.1); +} + /* Storage Progress Bar */ .storage-info { margin-top: 25px; diff --git a/frontend/index.html b/frontend/index.html index 3ad2811..ee547e7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -93,8 +93,26 @@

Thông tin hồ sơ

+
+
+ +
?
+ + +
+
- + + +
+
+ + +
+
+
diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index 9a76bcb..2bd9283 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -189,46 +189,90 @@ async function loadMediaStats() { */ async function updateProfileTabContent() { const token = localStorage.getItem('jwt'); - const username = localStorage.getItem('username'); - const role = localStorage.getItem('role'); + if (!token) return; - const avatar = document.getElementById('profile-avatar-initials'); + // Các phần tử hiển thị chung + const topAvatar = document.getElementById('avatar-initials'); + const sidebarAvatar = document.getElementById('sidebar-avatar'); const userDisplay = document.getElementById('profile-username-display'); const statusDisplay = document.getElementById('profile-status-display'); + const sidebarUser = document.getElementById('sidebar-username'); + const sidebarStatus = document.getElementById('sidebar-status'); + + // Các phần tử trong Form + const fullNameInput = document.getElementById('profile-fullname'); + const emailInput = document.getElementById('profile-email'); const userInput = document.getElementById('profile-username'); + const avatarPreview = document.getElementById('profile-avatar-preview'); + const avatarPlaceholder = document.getElementById('profile-avatar-placeholder'); - if (avatar && username) avatar.innerText = username.charAt(0).toUpperCase(); - if (userDisplay) userDisplay.innerText = username; - if (statusDisplay) statusDisplay.innerText = role || 'Thành viên'; - if (userInput) userInput.value = username; - - // Lấy dữ liệu dung lượng thực tế từ server try { const res = await fetch(`${API_BASE_URL}/me/profile`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); - if (data.storage) { - const { used, quota } = data.storage; - const progress = document.getElementById('storage-progress-bar'); - const text = document.getElementById('storage-text'); + if (data && res.ok) { + // 1. Cập nhật thông tin text an toàn + if (fullNameInput) fullNameInput.value = data.fullName || ''; + if (emailInput) emailInput.value = data.email || ''; + if (userInput) userInput.value = data.username || ''; - if (progress && text) { - const usedMB = (used / (1024 * 1024)).toFixed(1); - const quotaMB = quota === -1 ? '∞' : (quota / (1024 * 1024)).toFixed(0); - text.innerText = `${usedMB} MB / ${quotaMB} MB`; + if (userDisplay) userDisplay.innerText = data.username || 'N/A'; + if (statusDisplay) statusDisplay.innerText = data.role || 'Thành viên'; + if (sidebarUser) sidebarUser.innerText = data.username || 'N/A'; + if (sidebarStatus) sidebarStatus.innerText = data.role || 'Thành viên'; + + // Cập nhật lại localStorage để đồng bộ trạng thái + if (data.username) localStorage.setItem('username', data.username); + if (data.role) localStorage.setItem('role', data.role); + + // 2. Xử lý Ảnh đại diện (Avatar) + if (data.avatarUrl) { + const fullAvatarUrl = data.avatarUrl; + if (avatarPreview) { + avatarPreview.src = fullAvatarUrl; + avatarPreview.style.display = 'block'; + } + if (avatarPlaceholder) avatarPlaceholder.style.display = 'none'; - if (quota !== -1) { - const percent = Math.min((used / quota) * 100, 100); - progress.style.width = percent + '%'; - // Đổi màu thanh tiến trình dựa trên mức độ sử dụng - if (percent > 90) progress.style.background = '#dc3545'; // Đỏ (sắp hết) - else if (percent > 75) progress.style.background = '#ffc107'; // Vàng (cảnh báo) - else progress.style.background = '#28a745'; // Xanh (an toàn) - } else { - progress.style.width = '100%'; - progress.style.background = '#007bff'; // Màu xanh dương cho không giới hạn + // Cập nhật ảnh đại diện ở sidebar nếu có + if (sidebarAvatar) { + sidebarAvatar.innerHTML = ``; + } + } else { + // Fallback về chữ cái đầu nếu không có ảnh + const initial = (data.username || "?").charAt(0).toUpperCase(); + if (avatarPreview) avatarPreview.style.display = 'none'; + if (avatarPlaceholder) { + avatarPlaceholder.style.display = 'flex'; + avatarPlaceholder.innerText = initial; + } + if (topAvatar) topAvatar.innerText = initial; + if (sidebarAvatar) sidebarAvatar.innerText = initial; + } + + // 3. Xử lý thông tin dung lượng + if (data.storage) { + const { used, quota } = data.storage; + const progress = document.getElementById('storage-progress-bar'); + const text = document.getElementById('storage-text'); + + if (progress && text) { + const usedMB = (used / (1024 * 1024)).toFixed(1); + const quotaMB = quota === -1 ? '∞' : (quota / (1024 * 1024)).toFixed(0); + text.innerText = `${usedMB} MB / ${quotaMB} MB`; + + if (quota !== -1 && quota > 0) { + const percent = Math.min((used / quota) * 100, 100); + progress.style.width = percent + '%'; + if (percent > 90) progress.style.background = '#dc3545'; + else if (percent > 75) progress.style.background = '#ffc107'; + else progress.style.background = '#28a745'; + } else { + progress.style.width = '100%'; + progress.style.background = '#007bff'; + } } } } @@ -237,6 +281,55 @@ async function updateProfileTabContent() { } } +/** + * Xem trước ảnh đại diện khi chọn file + */ +window.previewAvatar = function(input) { + if (input.files && input.files[0]) { + const reader = new FileReader(); + reader.onload = function(e) { + const preview = document.getElementById('profile-avatar-preview'); + const placeholder = document.getElementById('profile-avatar-placeholder'); + preview.src = e.target.result; + preview.style.display = 'block'; + placeholder.style.display = 'none'; + }; + reader.readAsDataURL(input.files[0]); + } +}; + +/** + * Cập nhật hồ sơ người dùng + */ +async function updateProfile(e) { + e.preventDefault(); + const token = localStorage.getItem('jwt'); + const form = document.getElementById('profile-form'); + const formData = new FormData(form); + + try { + const res = await fetch(`${API_BASE_URL}/me/profile`, { + method: 'PUT', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.message); + + showNotification("Hồ sơ đã được cập nhật thành công!", 'success'); + + // Cập nhật lại localStorage nếu username thay đổi + if (data.user && data.user.username) { + localStorage.setItem('username', data.user.username); + } + + updateProfileTabContent(); // Tải lại thông tin mới + } catch (err) { + showNotification("Lỗi cập nhật: " + err.message, 'error'); + } +} + /** * Hàm bổ trợ định dạng ngày tháng theo múi giờ hệ thống */