183 lines
7.3 KiB
JavaScript
183 lines
7.3 KiB
JavaScript
|
|
// ── Profile page logic ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
let rigs = [];
|
||
|
|
|
||
|
|
// ── Toast ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function showToast(msg, type = 'success') {
|
||
|
|
const el = document.createElement('div');
|
||
|
|
el.className = `toast align-items-center text-bg-${type} border-0 show`;
|
||
|
|
el.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div>
|
||
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
||
|
|
document.getElementById('toastContainer').appendChild(el);
|
||
|
|
setTimeout(() => el.remove(), 3500);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showAlert(id, msg, type = 'danger') {
|
||
|
|
const el = document.getElementById(id);
|
||
|
|
el.className = `alert alert-${type} mt-3`;
|
||
|
|
el.textContent = msg;
|
||
|
|
el.classList.remove('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideAlert(id) {
|
||
|
|
document.getElementById(id).classList.add('d-none');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Load profile ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
async function loadProfile() {
|
||
|
|
try {
|
||
|
|
const user = await apiGet('/users/profile/');
|
||
|
|
document.getElementById('firstName').value = user.first_name || '';
|
||
|
|
document.getElementById('lastName').value = user.last_name || '';
|
||
|
|
document.getElementById('email').value = user.email;
|
||
|
|
document.getElementById('profileUsername').textContent = '@' + user.username;
|
||
|
|
|
||
|
|
if (user.avatar_url) {
|
||
|
|
document.getElementById('avatarWrap').innerHTML =
|
||
|
|
`<img src="${user.avatar_url}" class="avatar-img mx-auto d-block" alt="avatar">`;
|
||
|
|
} else {
|
||
|
|
const initials = ((user.first_name?.[0] || '') + (user.last_name?.[0] || '') ||
|
||
|
|
user.username?.[0] || '?').toUpperCase();
|
||
|
|
document.getElementById('avatarInitials').textContent = initials;
|
||
|
|
}
|
||
|
|
} catch(e) {
|
||
|
|
showToast('Failed to load profile', 'danger');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Save profile info ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
document.getElementById('profileForm').addEventListener('submit', async e => {
|
||
|
|
e.preventDefault();
|
||
|
|
hideAlert('profileAlert');
|
||
|
|
const btn = document.getElementById('saveProfileBtn');
|
||
|
|
btn.disabled = true;
|
||
|
|
try {
|
||
|
|
await apiPatch('/users/profile/', {
|
||
|
|
first_name: document.getElementById('firstName').value.trim(),
|
||
|
|
last_name: document.getElementById('lastName').value.trim(),
|
||
|
|
});
|
||
|
|
showAlert('profileAlert', 'Profile saved!', 'success');
|
||
|
|
showToast('Profile updated!');
|
||
|
|
} catch(e) {
|
||
|
|
showAlert('profileAlert', formatErrors(e.data));
|
||
|
|
} finally {
|
||
|
|
btn.disabled = false;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Avatar upload ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
document.getElementById('avatarInput').addEventListener('change', async e => {
|
||
|
|
const file = e.target.files[0];
|
||
|
|
if (!file) return;
|
||
|
|
const alertEl = document.getElementById('avatarAlert');
|
||
|
|
alertEl.classList.add('d-none');
|
||
|
|
try {
|
||
|
|
// Step 1: upload image to /api/photos/upload/
|
||
|
|
const form = new FormData();
|
||
|
|
form.append('file', file);
|
||
|
|
const photo = await apiPost('/photos/upload/', form);
|
||
|
|
|
||
|
|
// Step 2: set it as the user's avatar
|
||
|
|
const updated = await apiPatch('/users/profile/', { avatar: photo.id });
|
||
|
|
if (updated.avatar_url) {
|
||
|
|
document.getElementById('avatarWrap').innerHTML =
|
||
|
|
`<img src="${updated.avatar_url}" class="avatar-img mx-auto d-block" alt="avatar">`;
|
||
|
|
}
|
||
|
|
showToast('Avatar updated!');
|
||
|
|
} catch(e) {
|
||
|
|
alertEl.textContent = (e.data && formatErrors(e.data)) || e.message || 'Upload failed. Please try a smaller image.';
|
||
|
|
alertEl.classList.remove('d-none');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Change password ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
document.getElementById('passwordForm').addEventListener('submit', async e => {
|
||
|
|
e.preventDefault();
|
||
|
|
hideAlert('passwordAlert');
|
||
|
|
const p1 = document.getElementById('newPassword1').value;
|
||
|
|
const p2 = document.getElementById('newPassword2').value;
|
||
|
|
if (p1 !== p2) {
|
||
|
|
showAlert('passwordAlert', 'New passwords do not match.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
await apiPost('/auth/password/change/', {
|
||
|
|
old_password: document.getElementById('oldPassword').value,
|
||
|
|
new_password1: p1,
|
||
|
|
new_password2: p2,
|
||
|
|
});
|
||
|
|
showAlert('passwordAlert', 'Password changed successfully!', 'success');
|
||
|
|
document.getElementById('passwordForm').reset();
|
||
|
|
showToast('Password changed!');
|
||
|
|
} catch(e) {
|
||
|
|
showAlert('passwordAlert', formatErrors(e.data));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Rigs table ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
function renderRigs() {
|
||
|
|
const tbody = document.getElementById('rigsBody');
|
||
|
|
const wrap = document.getElementById('rigsTableWrap');
|
||
|
|
const empty = document.getElementById('rigsEmpty');
|
||
|
|
document.getElementById('rigsSpinner').classList.add('d-none');
|
||
|
|
|
||
|
|
if (!rigs.length) {
|
||
|
|
empty.classList.remove('d-none');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
wrap.classList.remove('d-none');
|
||
|
|
|
||
|
|
tbody.innerHTML = rigs.map(rig => `
|
||
|
|
<tr>
|
||
|
|
<td class="fw-semibold">${rig.name}</td>
|
||
|
|
<td>${rig.rig_items.length} item${rig.rig_items.length === 1 ? '' : 's'}</td>
|
||
|
|
<td>
|
||
|
|
<span class="badge ${rig.is_public ? 'bg-success' : 'bg-secondary'}">
|
||
|
|
${rig.is_public ? 'Public' : 'Private'}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<button class="btn btn-sm ${rig.is_public ? 'btn-outline-secondary' : 'btn-outline-success'}"
|
||
|
|
onclick="toggleRig(${rig.id})">
|
||
|
|
<i class="bi bi-${rig.is_public ? 'lock' : 'globe2'} me-1"></i>
|
||
|
|
${rig.is_public ? 'Make private' : 'Make public'}
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadRigs() {
|
||
|
|
try {
|
||
|
|
rigs = await apiGet('/rigs/');
|
||
|
|
renderRigs();
|
||
|
|
} catch(e) {
|
||
|
|
document.getElementById('rigsSpinner').classList.add('d-none');
|
||
|
|
showToast('Failed to load rigs', 'danger');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleRig(id) {
|
||
|
|
const rig = rigs.find(r => r.id === id);
|
||
|
|
if (!rig) return;
|
||
|
|
try {
|
||
|
|
const updated = await apiPatch(`/rigs/${id}/`, { is_public: !rig.is_public });
|
||
|
|
const idx = rigs.findIndex(r => r.id === id);
|
||
|
|
if (idx >= 0) rigs[idx] = { ...rigs[idx], is_public: updated.is_public };
|
||
|
|
renderRigs();
|
||
|
|
showToast(`Rig is now ${updated.is_public ? 'public' : 'private'}.`);
|
||
|
|
} catch(e) {
|
||
|
|
showToast('Failed to update rig.', 'danger');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||
|
|
loadProfile();
|
||
|
|
loadRigs();
|