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
|
* @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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user