support for multiple users
This commit is contained in:
688
src/scripts/users.js
Normal file
688
src/scripts/users.js
Normal file
@@ -0,0 +1,688 @@
|
||||
const API_BASE = '/api';
|
||||
|
||||
let users = [];
|
||||
let selectedFile = null;
|
||||
let currentUserId = null;
|
||||
|
||||
const usersGrid = document.getElementById('usersGrid');
|
||||
const btnAddUser = document.getElementById('btnAddUser');
|
||||
|
||||
const modalCreateUser = document.getElementById('modalCreateUser');
|
||||
const closeCreateModal = document.getElementById('closeCreateModal');
|
||||
const cancelCreate = document.getElementById('cancelCreate');
|
||||
const createUserForm = document.getElementById('createUserForm');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const avatarInput = document.getElementById('avatarInput');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
const avatarUploadArea = document.getElementById('avatarUploadArea');
|
||||
|
||||
const modalUserActions = document.getElementById('modalUserActions');
|
||||
const closeActionsModal = document.getElementById('closeActionsModal');
|
||||
const actionsModalTitle = document.getElementById('actionsModalTitle');
|
||||
|
||||
const modalEditUser = document.getElementById('modalEditUser');
|
||||
const closeEditModal = document.getElementById('closeEditModal');
|
||||
const cancelEdit = document.getElementById('cancelEdit');
|
||||
const editUserForm = document.getElementById('editUserForm');
|
||||
const editUsernameInput = document.getElementById('editUsername');
|
||||
const editAvatarPreview = document.getElementById('editAvatarPreview');
|
||||
const editAvatarUploadArea = document.getElementById('editAvatarUploadArea');
|
||||
const editAvatarInput = document.getElementById('editAvatarInput');
|
||||
|
||||
const modalAniList = document.getElementById('modalAniList');
|
||||
const closeAniListModal = document.getElementById('closeAniListModal');
|
||||
const aniListContent = document.getElementById('aniListContent');
|
||||
|
||||
const toastContainer = document.getElementById('userToastContainer');
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const anilistStatus = params.get("anilist");
|
||||
|
||||
if (anilistStatus === "success") {
|
||||
showUserToast("✅ AniList connected successfully!");
|
||||
}
|
||||
|
||||
if (anilistStatus === "error") {
|
||||
showUserToast("❌ Failed to connect AniList");
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initAvatarUpload('avatarUploadArea', 'avatarInput', 'avatarPreview');
|
||||
initAvatarUpload('editAvatarUploadArea', 'editAvatarInput', 'editAvatarPreview');
|
||||
loadUsers();
|
||||
attachEventListeners();
|
||||
});
|
||||
|
||||
function authFetch(url, options = {}) {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers = {
|
||||
...(options.headers || {}),
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
return fetch(url, { ...options, headers });
|
||||
}
|
||||
|
||||
function showUserToast(message, type = 'info') {
|
||||
if (!toastContainer) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `wb-toast ${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
toastContainer.prepend(toast);
|
||||
|
||||
setTimeout(() => toast.classList.add('show'), 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
toast.addEventListener('transitionend', () => toast.remove());
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function attachEventListeners() {
|
||||
if (btnAddUser) btnAddUser.addEventListener('click', openCreateModal);
|
||||
|
||||
if (closeCreateModal) closeCreateModal.addEventListener('click', closeModal);
|
||||
if (cancelCreate) cancelCreate.addEventListener('click', closeModal);
|
||||
if (closeAniListModal) closeAniListModal.addEventListener('click', closeModal);
|
||||
if (closeActionsModal) closeActionsModal.addEventListener('click', closeModal);
|
||||
if (closeEditModal) closeEditModal.addEventListener('click', closeModal);
|
||||
if (cancelEdit) cancelEdit.addEventListener('click', closeModal);
|
||||
|
||||
if (createUserForm) createUserForm.addEventListener('submit', handleCreateUser);
|
||||
if (editUserForm) editUserForm.addEventListener('submit', handleEditUser);
|
||||
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-overlay')) closeModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initAvatarUpload(uploadAreaId, fileInputId, previewId) {
|
||||
const uploadArea = document.getElementById(uploadAreaId);
|
||||
const fileInput = document.getElementById(fileInputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (!uploadArea || !fileInput) return;
|
||||
|
||||
uploadArea.addEventListener('click', () => fileInput.click());
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) handleFileSelect(file, previewId);
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
handleFileSelect(file, previewId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileSelect(file, previewId) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showUserToast('Please select an image file', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
showUserToast('Image size must be less than 5MB', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (preview) preview.innerHTML = `<img src="${e.target.result}" alt="Avatar preview">`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (err) => reject(err);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users`);
|
||||
if (!res.ok) throw new Error('Failed to fetch users');
|
||||
const data = await res.json();
|
||||
users = data.users || [];
|
||||
renderUsers();
|
||||
} catch (err) {
|
||||
console.error('Error loading users:', err);
|
||||
showEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser(username, profilePictureUrl) {
|
||||
try {
|
||||
const body = { username };
|
||||
if (profilePictureUrl) body.profilePictureUrl = profilePictureUrl;
|
||||
|
||||
const res = await fetch(`${API_BASE}/users`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Error creating user');
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser(userId, username, profilePictureUrl) {
|
||||
try {
|
||||
const updates = { username };
|
||||
if (profilePictureUrl !== undefined) updates.profilePictureUrl = profilePictureUrl;
|
||||
|
||||
const res = await fetch(`${API_BASE}/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Error updating user');
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(userId) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/users/${userId}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Error deleting user');
|
||||
}
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
if (!usersGrid) return;
|
||||
|
||||
if (users.length === 0) {
|
||||
showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
usersGrid.innerHTML = '';
|
||||
users.forEach(user => {
|
||||
const userCard = createUserCard(user);
|
||||
usersGrid.appendChild(userCard);
|
||||
});
|
||||
}
|
||||
|
||||
function createUserCard(user) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'user-card';
|
||||
|
||||
card.addEventListener('click', (e) => {
|
||||
|
||||
if (!e.target.closest('.user-config-btn')) {
|
||||
loginUser(user.id);
|
||||
}
|
||||
});
|
||||
|
||||
const avatarContent = user.profile_picture_url
|
||||
? `<img src="${user.profile_picture_url}" alt="${user.username}">`
|
||||
: `<div class="user-avatar-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="user-avatar">${avatarContent}</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">${user.username}</div>
|
||||
<div class="user-status">
|
||||
<span class="status-dot"></span>
|
||||
<span>Available</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="user-config-btn" title="Manage User" data-user-id="${user.id}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0-.33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1.51-1V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const configBtn = card.querySelector('.user-config-btn');
|
||||
configBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
openUserActionsModal(user.id);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
if (!usersGrid) return;
|
||||
usersGrid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1/-1;">
|
||||
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
<h2 class="empty-title">No Users Yet</h2>
|
||||
<p class="empty-text">Create your first profile to get started</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
modalCreateUser.classList.add('active');
|
||||
if (usernameInput) usernameInput.focus();
|
||||
selectedFile = null;
|
||||
if (avatarPreview) {
|
||||
avatarPreview.innerHTML = `
|
||||
<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalCreateUser.classList.remove('active');
|
||||
modalAniList.classList.remove('active');
|
||||
modalUserActions.classList.remove('active');
|
||||
modalEditUser.classList.remove('active');
|
||||
if (createUserForm) createUserForm.reset();
|
||||
if (editUserForm) editUserForm.reset();
|
||||
selectedFile = null;
|
||||
const modalHeader = modalAniList.querySelector('.modal-header h2');
|
||||
if (modalHeader) modalHeader.textContent = 'AniList Integration';
|
||||
}
|
||||
|
||||
async function handleCreateUser(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
if (!username) {
|
||||
showUserToast('Please enter a username', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = createUserForm.querySelector('button[type="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Creating...';
|
||||
|
||||
try {
|
||||
let profilePictureUrl = null;
|
||||
if (selectedFile) profilePictureUrl = await fileToBase64(selectedFile);
|
||||
|
||||
await createUser(username, profilePictureUrl);
|
||||
|
||||
closeModal();
|
||||
await loadUsers();
|
||||
showUserToast(`User ${username} created successfully!`, 'success');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast(err.message || 'Error creating user', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Create User';
|
||||
}
|
||||
}
|
||||
|
||||
function openUserActionsModal(userId) {
|
||||
currentUserId = userId;
|
||||
const user = users.find(u => u.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
modalAniList.classList.remove('active');
|
||||
modalEditUser.classList.remove('active');
|
||||
|
||||
actionsModalTitle.textContent = `Manage ${user.username}`;
|
||||
|
||||
const content = document.getElementById('actionsModalContent');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="manage-actions-modal">
|
||||
<button class="btn-action edit" onclick="openEditModal(${user.id})">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button class="btn-action anilist" onclick="openAniListModal(${user.id})">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
AniList Integration
|
||||
</button>
|
||||
<button class="btn-action delete" onclick="handleDeleteConfirmation(${user.id})">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="18" y1="9" x2="12" y2="15"></line><line x1="12" y1="9" x2="18" y2="15"></line></svg>
|
||||
Delete Profile
|
||||
</button>
|
||||
<button class="btn-action cancel" onclick="closeModal()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalUserActions.classList.add('active');
|
||||
}
|
||||
|
||||
window.openEditModal = function(userId) {
|
||||
currentUserId = userId;
|
||||
modalUserActions.classList.remove('active');
|
||||
const user = users.find(u => u.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
editUsernameInput.value = user.username || '';
|
||||
selectedFile = null;
|
||||
|
||||
const avatarPlaceholder = `
|
||||
<svg class="avatar-preview-placeholder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
if (user.profile_picture_url) {
|
||||
editAvatarPreview.innerHTML = `<img src="${user.profile_picture_url}" alt="Avatar preview">`;
|
||||
} else {
|
||||
editAvatarPreview.innerHTML = avatarPlaceholder;
|
||||
}
|
||||
|
||||
if (editAvatarInput) editAvatarInput.value = '';
|
||||
modalEditUser.classList.add('active');
|
||||
};
|
||||
|
||||
async function handleEditUser(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const user = users.find(u => u.id === currentUserId);
|
||||
if (!user) return;
|
||||
|
||||
const username = editUsernameInput.value.trim();
|
||||
if (!username) {
|
||||
showUserToast('Please enter a username', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = editUserForm.querySelector('.btn-primary');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
let profilePictureUrl;
|
||||
if (selectedFile) profilePictureUrl = await fileToBase64(selectedFile);
|
||||
|
||||
await updateUser(currentUserId, username, profilePictureUrl);
|
||||
|
||||
closeModal();
|
||||
await loadUsers();
|
||||
showUserToast('Profile updated successfully!', 'success');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast(err.message || 'Error updating user', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Save Changes';
|
||||
}
|
||||
}
|
||||
|
||||
window.handleDeleteConfirmation = function(userId) {
|
||||
const user = users.find(u => u.id === userId);
|
||||
if (!user) return;
|
||||
|
||||
closeModal();
|
||||
|
||||
showConfirmationModal(
|
||||
'Confirm Deletion',
|
||||
`Are you absolutely sure you want to delete profile ${user.username}? This action cannot be undone.`,
|
||||
`handleConfirmedDeleteUser(${userId})`
|
||||
);
|
||||
};
|
||||
|
||||
window.handleConfirmedDeleteUser = async function(userId) {
|
||||
closeModal();
|
||||
showUserToast('Deleting user...', 'info');
|
||||
|
||||
try {
|
||||
await deleteUser(userId);
|
||||
await loadUsers();
|
||||
showUserToast('User deleted successfully!', 'success');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast('Error deleting user', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
function showConfirmationModal(title, message, confirmAction) {
|
||||
closeModal();
|
||||
|
||||
const modalHeader = modalAniList.querySelector('.modal-header h2');
|
||||
if (modalHeader) modalHeader.textContent = title;
|
||||
|
||||
aniListContent.innerHTML = `
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" style="opacity: 0.8; margin: 0 auto 1rem; display: block;">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 2rem; font-size: 1rem;">
|
||||
${message}
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<button class="btn-secondary" style="flex: 1;" onclick="closeModal()">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-disconnect" style="flex: 1; background: #ef4444; color: white;" onclick="window.${confirmAction}">
|
||||
Confirm Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalAniList.classList.add('active');
|
||||
}
|
||||
|
||||
function openAniListModal(userId) {
|
||||
currentUserId = userId;
|
||||
|
||||
aniListContent.innerHTML = `<div style="text-align: center; padding: 2rem;">Loading integration status...</div>`;
|
||||
modalUserActions.classList.remove('active');
|
||||
modalEditUser.classList.remove('active');
|
||||
|
||||
getIntegrationStatus(userId).then(integration => {
|
||||
aniListContent.innerHTML = `
|
||||
<div class="anilist-status">
|
||||
${integration.connected ? `
|
||||
<div class="anilist-connected">
|
||||
<div class="anilist-icon">
|
||||
<img
|
||||
src="https://anilist.co/img/icons/icon.svg"
|
||||
alt="AniList"
|
||||
style="width:40px; height:40px;"
|
||||
>
|
||||
</div>
|
||||
<div class="anilist-info">
|
||||
<h3>Connected to AniList</h3>
|
||||
<p>User ID: ${integration.anilistUserId}</p>
|
||||
<p style="font-size: 0.75rem;">Expires: ${new Date(integration.expiresAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-disconnect" onclick="handleDisconnectAniList()">
|
||||
Disconnect AniList
|
||||
</button>
|
||||
` : `
|
||||
<div style="text-align: center; padding: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">Connect with AniList</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">
|
||||
Sync your anime list by logging in with AniList.
|
||||
</p>
|
||||
|
||||
<div style="display:flex; justify-content:center;">
|
||||
<button class="btn-connect" onclick="redirectToAniListLogin()">
|
||||
Login with AniList
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style="font-size:0.85rem; margin-top:1rem; color:var(--text-secondary);">You will be redirected and then returned here.</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalAniList.classList.add('active');
|
||||
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
aniListContent.innerHTML = `<div style="text-align:center;padding:1rem;color:var(--text-secondary)">Error loading integration status.</div>`;
|
||||
modalAniList.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
async function redirectToAniListLogin() {
|
||||
try {
|
||||
|
||||
const res = await fetch(`/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: currentUserId })
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Login failed before AniList redirect');
|
||||
|
||||
const data = await res.json();
|
||||
localStorage.setItem('token', data.token);
|
||||
token = data.token;
|
||||
|
||||
localStorage.setItem('anilist_link_user', currentUserId);
|
||||
|
||||
const clientId = 32898;
|
||||
const redirectUri = encodeURIComponent(
|
||||
window.location.origin + '/api/anilist'
|
||||
);
|
||||
|
||||
const state = encodeURIComponent(currentUserId);
|
||||
|
||||
window.location.href =
|
||||
`https://anilist.co/api/v2/oauth/authorize` +
|
||||
`?client_id=${clientId}` +
|
||||
`&response_type=code` +
|
||||
`&redirect_uri=${redirectUri}` +
|
||||
`&state=${state}`;
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast('Error starting AniList login', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getIntegrationStatus(userId) {
|
||||
try {
|
||||
|
||||
const res = await fetch(`${API_BASE}/users/${userId}/integration`);
|
||||
if (!res.ok) {
|
||||
return { connected: false };
|
||||
}
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error('getIntegrationStatus error', err);
|
||||
return { connected: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectAniList(userId) {
|
||||
try {
|
||||
|
||||
const res = await authFetch(`${API_BASE}/users/${userId}/integration`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.error || 'Error disconnecting AniList');
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
window.handleDisconnectAniList = async function() {
|
||||
if (!confirm('Are you sure you want to disconnect AniList?')) return;
|
||||
|
||||
try {
|
||||
await disconnectAniList(currentUserId);
|
||||
showUserToast('AniList disconnected successfully', 'success');
|
||||
await openAniListModal(currentUserId);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showUserToast('Error disconnecting AniList', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
async function loginUser(userId) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
localStorage.setItem('token', data.token);
|
||||
|
||||
window.location.href = '/anime';
|
||||
} catch (err) {
|
||||
console.error('Login error', err);
|
||||
showUserToast('Login failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
window.openAniListModal = openAniListModal;
|
||||
window.openEditModal = window.openEditModal;
|
||||
|
||||
window.handleDeleteConfirmation = window.handleDeleteConfirmation;
|
||||
window.handleConfirmedDeleteUser = window.handleConfirmedDeleteUser;
|
||||
window.redirectToAniListLogin = redirectToAniListLogin;
|
||||
Reference in New Issue
Block a user