Cập nhật avatar và chỉnh sửa thông tin người dùng
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+19
-1
@@ -93,8 +93,26 @@
|
||||
<div id="tab-profile" class="dashboard-tab-pane active">
|
||||
<h3>Thông tin hồ sơ</h3>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
+120
-27
@@ -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 = `<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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user