Cập nhật avatar và chỉnh sửa thông tin người dùng

This commit is contained in:
2026-06-09 15:21:22 +07:00
parent fd1027203d
commit 7f32eb816c
4 changed files with 205 additions and 28 deletions
+23
View File
@@ -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 * @route GET /api/me/scenes
* @desc Lấy danh sách các cảnh mẹ do người dùng tạo * @desc Lấy danh sách các cảnh mẹ do người dùng tạo
+43
View File
@@ -422,6 +422,49 @@ html, body {
color: #ccc; 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 Progress Bar */
.storage-info { .storage-info {
margin-top: 25px; margin-top: 25px;
+19 -1
View File
@@ -93,8 +93,26 @@
<div id="tab-profile" class="dashboard-tab-pane active"> <div id="tab-profile" class="dashboard-tab-pane active">
<h3>Thông tin hồ sơ</h3> <h3>Thông tin hồ sơ</h3>
<form id="profile-form" onsubmit="updateProfile(event)"> <form id="profile-form" onsubmit="updateProfile(event)">
<div class="profile-header-edit">
<div class="avatar-edit-container">
<img id="profile-avatar-preview" src="" alt="Avatar" style="display:none;">
<div id="profile-avatar-placeholder" class="avatar-circle">?</div>
<label for="profile-avatar-input" class="avatar-upload-label">
<i class="fas fa-camera"></i> Thay đổi
</label>
<input type="file" id="profile-avatar-input" name="avatar" accept="image/*" onchange="previewAvatar(this)" style="display:none;">
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>Username</label> <label>Họ và tên</label>
<input type="text" id="profile-fullname" name="fullName" required>
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="profile-email" name="email" required>
</div>
<div class="form-group">
<label>Tên đăng nhập (Username)</label>
<input type="text" id="profile-username" name="username" required> <input type="text" id="profile-username" name="username" required>
</div> </div>
<div class="form-group"> <div class="form-group">
+120 -27
View File
@@ -189,46 +189,90 @@ async function loadMediaStats() {
*/ */
async function updateProfileTabContent() { async function updateProfileTabContent() {
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
const username = localStorage.getItem('username'); if (!token) return;
const role = localStorage.getItem('role');
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 userDisplay = document.getElementById('profile-username-display');
const statusDisplay = document.getElementById('profile-status-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 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 { try {
const res = await fetch(`${API_BASE_URL}/me/profile`, { const res = await fetch(`${API_BASE_URL}/me/profile`, {
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}); });
const data = await res.json(); const data = await res.json();
if (data.storage) { if (data && res.ok) {
const { used, quota } = data.storage; // 1. Cập nhật thông tin text an toàn
const progress = document.getElementById('storage-progress-bar'); if (fullNameInput) fullNameInput.value = data.fullName || '';
const text = document.getElementById('storage-text'); if (emailInput) emailInput.value = data.email || '';
if (userInput) userInput.value = data.username || '';
if (progress && text) { if (userDisplay) userDisplay.innerText = data.username || 'N/A';
const usedMB = (used / (1024 * 1024)).toFixed(1); if (statusDisplay) statusDisplay.innerText = data.role || 'Thành viên';
const quotaMB = quota === -1 ? '∞' : (quota / (1024 * 1024)).toFixed(0); if (sidebarUser) sidebarUser.innerText = data.username || 'N/A';
text.innerText = `${usedMB} MB / ${quotaMB} MB`; if (sidebarStatus) sidebarStatus.innerText = data.role || 'Thành viên';
if (quota !== -1) { // Cập nhật lại localStorage để đồng bộ trạng thái
const percent = Math.min((used / quota) * 100, 100); if (data.username) localStorage.setItem('username', data.username);
progress.style.width = percent + '%'; if (data.role) localStorage.setItem('role', data.role);
// Đổ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) // 2. Xử lý Ảnh đại diện (Avatar)
else if (percent > 75) progress.style.background = '#ffc107'; // Vàng (cảnh báo) if (data.avatarUrl) {
else progress.style.background = '#28a745'; // Xanh (an toàn) const fullAvatarUrl = data.avatarUrl;
} else { if (avatarPreview) {
progress.style.width = '100%'; avatarPreview.src = fullAvatarUrl;
progress.style.background = '#007bff'; // Màu xanh dương cho không giới hạn avatarPreview.style.display = 'block';
}
if (avatarPlaceholder) avatarPlaceholder.style.display = 'none';
// Cập nhật ảnh đại diện ở sidebar nếu có
if (sidebarAvatar) {
sidebarAvatar.innerHTML = `<img src="${fullAvatarUrl}" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">`;
}
} 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 * Hàm bổ trợ định dạng ngày tháng theo múi giờ hệ thống
*/ */