281 lines
12 KiB
JavaScript
281 lines
12 KiB
JavaScript
// ── Friends page ──────────────────────────────────────────────────────────────
|
|
|
|
const addFriendModal = new bootstrap.Modal('#addFriendModal');
|
|
let _currentTab = 'friends';
|
|
|
|
// ── Tab switching ─────────────────────────────────────────────────────────────
|
|
|
|
document.querySelectorAll('#friendTabs .nav-link').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('#friendTabs .nav-link').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
_currentTab = btn.dataset.tab;
|
|
document.getElementById('tabFriends').classList.toggle('d-none', _currentTab !== 'friends');
|
|
document.getElementById('tabRequests').classList.toggle('d-none', _currentTab !== 'requests');
|
|
document.getElementById('tabSent').classList.toggle('d-none', _currentTab !== 'sent');
|
|
});
|
|
});
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
function initials(detail) {
|
|
const name = detail?.display_name || detail?.username || '?';
|
|
return name.slice(0, 2).toUpperCase();
|
|
}
|
|
|
|
function displayName(detail) {
|
|
return esc(detail?.display_name || detail?.username || '—');
|
|
}
|
|
|
|
// ── Friends ───────────────────────────────────────────────────────────────────
|
|
|
|
async function loadFriends() {
|
|
try {
|
|
const data = await apiGet('/social/friends/');
|
|
const grid = document.getElementById('friendsGrid');
|
|
const count = document.getElementById('friendsCount');
|
|
count.textContent = data.length;
|
|
|
|
if (!data.length) {
|
|
grid.innerHTML = '<div class="col-12 text-center text-muted py-5"><i class="bi bi-people" style="font-size:2.5rem;opacity:.2;"></i><p class="mt-2 mb-0">No friends yet. Use "Add friend" to get started.</p></div>';
|
|
return;
|
|
}
|
|
|
|
// We need to know which side is "us" — resolved via unread-count trick:
|
|
// actually we can figure it out since from_user or to_user is the current user.
|
|
// But we don't have current user id here. Let's show both sides gracefully.
|
|
grid.innerHTML = data.map(f => {
|
|
// Show the "other" person — API returns both sides; we show both names
|
|
const fromD = f.from_user_detail;
|
|
const toD = f.to_user_detail;
|
|
return `
|
|
<div class="col-sm-6 col-md-4 col-lg-3">
|
|
<div class="card border-0 shadow-sm friend-card h-100">
|
|
<div class="card-body d-flex flex-column gap-2">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="avatar-lg">${initials(fromD)}</div>
|
|
<div class="overflow-hidden">
|
|
<div class="fw-semibold text-truncate">${displayName(fromD)}</div>
|
|
<div class="text-muted small">@${esc(fromD?.username || '—')}</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="avatar-lg" style="background:#e8f5e9;color:#2e7d32;">${initials(toD)}</div>
|
|
<div class="overflow-hidden">
|
|
<div class="fw-semibold text-truncate">${displayName(toD)}</div>
|
|
<div class="text-muted small">@${esc(toD?.username || '—')}</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-auto d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-danger flex-grow-1"
|
|
onclick="unfriend(${f.id})">
|
|
<i class="bi bi-person-dash me-1"></i>Unfriend
|
|
</button>
|
|
<a class="btn btn-sm btn-outline-primary" href="/messages.html"
|
|
title="Send message"><i class="bi bi-envelope"></i></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
} catch (e) {
|
|
document.getElementById('friendsGrid').innerHTML =
|
|
'<div class="col-12 text-danger text-center py-3 small">Failed to load friends.</div>';
|
|
}
|
|
}
|
|
|
|
async function unfriend(id) {
|
|
if (!confirm('Remove this friendship?')) return;
|
|
try {
|
|
await apiDelete(`/social/friends/${id}/`);
|
|
showToast('Friendship removed.');
|
|
loadFriends();
|
|
} catch (e) {
|
|
showToast('Failed to remove friendship.', 'danger');
|
|
}
|
|
}
|
|
|
|
// ── Incoming requests ─────────────────────────────────────────────────────────
|
|
|
|
async function loadRequests() {
|
|
try {
|
|
const data = await apiGet('/social/friends/requests/');
|
|
const el = document.getElementById('requestsList');
|
|
const badge = document.getElementById('requestsCount');
|
|
|
|
if (data.length > 0) {
|
|
badge.textContent = data.length;
|
|
badge.classList.remove('d-none');
|
|
} else {
|
|
badge.classList.add('d-none');
|
|
}
|
|
|
|
if (!data.length) {
|
|
el.innerHTML = '<div class="text-center text-muted py-5"><p class="mb-0">No pending requests.</p></div>';
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = data.map(f => `
|
|
<div class="req-card d-flex align-items-center gap-3 mb-2" id="req-${f.id}">
|
|
<div class="avatar-lg">${initials(f.from_user_detail)}</div>
|
|
<div class="flex-grow-1 overflow-hidden">
|
|
<div class="fw-semibold text-truncate">${displayName(f.from_user_detail)}</div>
|
|
<div class="text-muted small">@${esc(f.from_user_detail?.username || '—')}</div>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-shrink-0">
|
|
<button class="btn btn-sm btn-success" onclick="acceptRequest(${f.id})">
|
|
<i class="bi bi-check-lg"></i> Accept
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="declineRequest(${f.id})">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>`
|
|
).join('');
|
|
} catch (e) {
|
|
document.getElementById('requestsList').innerHTML =
|
|
'<div class="text-danger text-center py-3 small">Failed to load requests.</div>';
|
|
}
|
|
}
|
|
|
|
async function acceptRequest(id) {
|
|
try {
|
|
await apiPost(`/social/friends/${id}/accept/`, {});
|
|
showToast('Friend request accepted!');
|
|
document.getElementById(`req-${id}`)?.remove();
|
|
loadFriends();
|
|
loadRequests();
|
|
} catch (e) {
|
|
showToast('Failed to accept request.', 'danger');
|
|
}
|
|
}
|
|
|
|
async function declineRequest(id) {
|
|
try {
|
|
await apiPost(`/social/friends/${id}/decline/`, {});
|
|
showToast('Request declined.');
|
|
document.getElementById(`req-${id}`)?.remove();
|
|
loadRequests();
|
|
} catch (e) {
|
|
showToast('Failed to decline request.', 'danger');
|
|
}
|
|
}
|
|
|
|
// ── Sent requests ─────────────────────────────────────────────────────────────
|
|
|
|
async function loadSentRequests() {
|
|
try {
|
|
const data = await apiGet('/social/friends/sent-requests/');
|
|
const el = document.getElementById('sentList');
|
|
const badge = document.getElementById('sentCount');
|
|
|
|
if (data.length > 0) {
|
|
badge.textContent = data.length;
|
|
badge.classList.remove('d-none');
|
|
} else {
|
|
badge.classList.add('d-none');
|
|
}
|
|
|
|
if (!data.length) {
|
|
el.innerHTML = '<div class="text-center text-muted py-5"><p class="mb-0">No sent requests.</p></div>';
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = data.map(f => `
|
|
<div class="req-card d-flex align-items-center gap-3 mb-2" id="sent-${f.id}">
|
|
<div class="avatar-lg" style="background:#fff3cd;color:#856404;">${initials(f.to_user_detail)}</div>
|
|
<div class="flex-grow-1 overflow-hidden">
|
|
<div class="fw-semibold text-truncate">${displayName(f.to_user_detail)}</div>
|
|
<div class="text-muted small">@${esc(f.to_user_detail?.username || '—')}</div>
|
|
</div>
|
|
<span class="badge bg-warning text-dark me-2">Pending</span>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="cancelRequest(${f.id})">
|
|
Cancel
|
|
</button>
|
|
</div>`
|
|
).join('');
|
|
} catch (e) {
|
|
document.getElementById('sentList').innerHTML =
|
|
'<div class="text-danger text-center py-3 small">Failed to load sent requests.</div>';
|
|
}
|
|
}
|
|
|
|
async function cancelRequest(id) {
|
|
if (!confirm('Cancel this friend request?')) return;
|
|
try {
|
|
await apiPost(`/social/friends/${id}/decline/`, {});
|
|
showToast('Request cancelled.');
|
|
document.getElementById(`sent-${id}`)?.remove();
|
|
loadSentRequests();
|
|
} catch (e) {
|
|
showToast('Failed to cancel request.', 'danger');
|
|
}
|
|
}
|
|
|
|
// ── Add friend modal ──────────────────────────────────────────────────────────
|
|
|
|
document.getElementById('addFriendBtn').addEventListener('click', () => {
|
|
document.getElementById('friendSearch').value = '';
|
|
document.getElementById('friendSearchResults').innerHTML = '';
|
|
document.getElementById('addAlert').classList.add('d-none');
|
|
addFriendModal.show();
|
|
setTimeout(() => document.getElementById('friendSearch').focus(), 300);
|
|
});
|
|
|
|
let _friendSearchTimer = null;
|
|
document.getElementById('friendSearch').addEventListener('input', function () {
|
|
clearTimeout(_friendSearchTimer);
|
|
const q = this.value.trim();
|
|
const resultsEl = document.getElementById('friendSearchResults');
|
|
if (q.length < 2) { resultsEl.innerHTML = ''; return; }
|
|
|
|
_friendSearchTimer = setTimeout(async () => {
|
|
try {
|
|
const results = await apiGet(`/social/members/?q=${encodeURIComponent(q)}`);
|
|
if (!results.length) {
|
|
resultsEl.innerHTML = '<div class="list-group-item text-muted small">No users found.</div>';
|
|
return;
|
|
}
|
|
resultsEl.innerHTML = results.map(u => `
|
|
<div class="list-group-item list-group-item-action d-flex align-items-center justify-content-between py-2">
|
|
<div>
|
|
<strong>${esc(u.display_name || u.username)}</strong>
|
|
<span class="text-muted ms-1 small">@${esc(u.username)}</span>
|
|
</div>
|
|
<button class="btn btn-sm btn-primary" onclick="sendRequest(${u.id}, this)">
|
|
<i class="bi bi-person-plus-fill me-1"></i>Add
|
|
</button>
|
|
</div>`
|
|
).join('');
|
|
} catch (e) { /* ignore */ }
|
|
}, 300);
|
|
});
|
|
|
|
async function sendRequest(userId, btn) {
|
|
const alertEl = document.getElementById('addAlert');
|
|
alertEl.classList.add('d-none');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
|
try {
|
|
await apiPost('/social/friends/', { to_user: userId });
|
|
btn.outerHTML = '<span class="badge bg-success">Request sent</span>';
|
|
showToast('Friend request sent!');
|
|
loadSentRequests();
|
|
} catch (e) {
|
|
if (e.status === 409) {
|
|
btn.outerHTML = '<span class="badge bg-secondary">Already connected</span>';
|
|
} else {
|
|
alertEl.textContent = 'Failed to send request.';
|
|
alertEl.classList.remove('d-none');
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="bi bi-person-plus-fill me-1"></i>Add';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Boot ──────────────────────────────────────────────────────────────────────
|
|
|
|
loadFriends();
|
|
loadRequests();
|
|
loadSentRequests();
|