Files
gh-christianlouis-quizzical…/musicround/templates/users/edit_profile.html
T
Christian Krakau-Louis 03b982e7c2 Initial clean commit
2025-05-13 08:59:02 +00:00

566 lines
25 KiB
HTML

{% extends 'base.html' %}
{% block title %}Edit Profile{% endblock %}
{% block content %}
<div class="max-w-md mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-6 text-navy-800">Edit Profile</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-3 {% if category == 'danger' %}bg-red-100 text-red-700{% elif category == 'success' %}bg-green-100 text-green-700{% else %}bg-blue-100 text-blue-700{% endif %} rounded">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('users.edit_profile') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="username">
Username
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username" name="username" type="text" placeholder="Username" value="{{ current_user.username }}">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="email">
Email
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="email" name="email" type="email" placeholder="Email address" value="{{ current_user.email }}">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="first_name">
First Name
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="first_name" name="first_name" type="text" placeholder="First name" value="{{ current_user.first_name or '' }}">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="last_name">
Last Name
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="last_name" name="last_name" type="text" placeholder="Last name" value="{{ current_user.last_name or '' }}">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="dropbox_export_path">
Dropbox Export Folder
</label>
<div class="flex space-x-2">
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="dropbox_export_path" name="dropbox_export_path" type="text" placeholder="/QuizzicalBeats"
value="{{ current_user.dropbox_export_path or '/QuizzicalBeats' }}">
<button type="button" id="browse-dropbox-btn"
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded flex items-center"
{% if not current_user.dropbox_token %}disabled{% endif %}>
<i class="fab fa-dropbox mr-1"></i> Browse
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Set the Dropbox folder where your exported rounds will be saved.</p>
{% if not current_user.dropbox_token %}
<p class="text-xs text-red-500 mt-1">
<i class="fas fa-exclamation-circle mr-1"></i>
<a href="{{ url_for('users.dropbox_auth') }}" class="underline">Connect your Dropbox account</a> to browse folders.
</p>
{% endif %}
<div id="dropbox-error-container" class="hidden mt-3 p-3 bg-red-100 text-sm rounded">
<div class="flex justify-between">
<h4 class="font-semibold mb-1">Dropbox API Error</h4>
<button id="close-error-btn" class="text-red-700">&times;</button>
</div>
<div id="dropbox-error-details"></div>
<div class="mt-2">
<button id="dropbox-debug-btn" type="button" class="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded">
Show Technical Details
</button>
<div id="dropbox-debug-info" class="hidden mt-2 bg-gray-100 p-2 rounded font-mono text-xs overflow-x-auto"></div>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<button class="bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
Save Changes
</button>
<a class="inline-block align-baseline font-bold text-sm text-teal-500 hover:text-teal-800" href="{{ url_for('users.profile') }}">
Cancel
</a>
</div>
</form>
</div>
<!-- Dropbox Folder Browser Modal -->
<div id="dropbox-folder-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-bold text-navy-800">Select Dropbox Folder</h3>
<button id="close-dropbox-modal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4 border-b border-gray-200 flex items-center bg-gray-50">
<div class="flex-1 flex items-center">
<button id="parent-folder-btn" class="bg-gray-200 hover:bg-gray-300 rounded p-1 mr-2">
<i class="fas fa-arrow-up"></i>
</button>
<span id="current-path" class="text-gray-600 font-mono text-sm">/</span>
</div>
<div class="flex items-center space-x-2">
<button id="create-folder-btn" class="bg-teal-500 hover:bg-teal-600 text-white rounded p-1 px-2 text-sm flex items-center">
<i class="fas fa-folder-plus mr-1"></i> Create Folder
</button>
<button id="refresh-folders-btn" class="bg-gray-200 hover:bg-gray-300 rounded p-1">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="overflow-y-auto flex-1 p-2" style="max-height: 60vh;">
<div id="folder-list" class="space-y-1">
<div class="text-center p-8">
<div class="animate-spin inline-block w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mb-2"></div>
<p>Loading folders...</p>
</div>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-2">
<button id="select-folder-btn" class="bg-teal-500 hover:bg-teal-600 text-white px-4 py-2 rounded">
Select This Folder
</button>
<button id="cancel-selection-btn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded">
Cancel
</button>
</div>
</div>
</div>
<!-- Create Folder Modal -->
<div id="create-folder-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-lg w-full max-w-md">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-bold text-navy-800">Create New Folder</h3>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="new-folder-name">
Folder Name
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="new-folder-name" type="text" placeholder="My New Folder">
<p id="folder-name-error" class="hidden mt-1 text-xs text-red-500"></p>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
Parent Folder
</label>
<p class="py-2 px-3 bg-gray-100 rounded text-gray-700 font-mono text-sm" id="parent-folder-path"></p>
</div>
<div class="flex justify-end space-x-2 mt-6">
<button id="create-folder-submit" class="bg-teal-500 hover:bg-teal-600 text-white px-4 py-2 rounded">
Create
</button>
<button id="create-folder-cancel" class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded">
Cancel
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const browseDropboxBtn = document.getElementById('browse-dropbox-btn');
const dropboxFolderModal = document.getElementById('dropbox-folder-modal');
const closeDropboxModal = document.getElementById('close-dropbox-modal');
const cancelSelectionBtn = document.getElementById('cancel-selection-btn');
const selectFolderBtn = document.getElementById('select-folder-btn');
const folderList = document.getElementById('folder-list');
const currentPathDisplay = document.getElementById('current-path');
const parentFolderBtn = document.getElementById('parent-folder-btn');
const refreshFoldersBtn = document.getElementById('refresh-folders-btn');
const dropboxExportPath = document.getElementById('dropbox_export_path');
const createFolderBtn = document.getElementById('create-folder-btn');
const createFolderModal = document.getElementById('create-folder-modal');
const createFolderSubmit = document.getElementById('create-folder-submit');
const createFolderCancel = document.getElementById('create-folder-cancel');
const newFolderNameInput = document.getElementById('new-folder-name');
const folderNameError = document.getElementById('folder-name-error');
const parentFolderPathDisplay = document.getElementById('parent-folder-path');
// Error display elements
const errorContainer = document.getElementById('dropbox-error-container');
const errorDetails = document.getElementById('dropbox-error-details');
const closeErrorBtn = document.getElementById('close-error-btn');
const debugBtn = document.getElementById('dropbox-debug-btn');
const debugInfo = document.getElementById('dropbox-debug-info');
let currentPath = '';
let lastApiResponse = null;
// Open the Dropbox folder browser modal
browseDropboxBtn.addEventListener('click', function() {
dropboxFolderModal.classList.remove('hidden');
// Always start at root when opening the browser
currentPath = '/';
loadFolders(currentPath);
});
// Close the modal when clicking close button or cancel
closeDropboxModal.addEventListener('click', closeModal);
cancelSelectionBtn.addEventListener('click', closeModal);
// Close modal when clicking outside
dropboxFolderModal.addEventListener('click', function(event) {
if (event.target === dropboxFolderModal) {
closeModal();
}
});
// Close error display
closeErrorBtn.addEventListener('click', function() {
errorContainer.classList.add('hidden');
});
// Toggle debug info
debugBtn.addEventListener('click', function() {
if (debugInfo.classList.contains('hidden')) {
debugInfo.classList.remove('hidden');
debugBtn.textContent = 'Hide Technical Details';
} else {
debugInfo.classList.add('hidden');
debugBtn.textContent = 'Show Technical Details';
}
});
// Select the current folder
selectFolderBtn.addEventListener('click', function() {
dropboxExportPath.value = currentPath;
closeModal();
});
// Navigate to parent folder
parentFolderBtn.addEventListener('click', function() {
if (currentPath === '/' || currentPath === '') {
return; // Already at root
}
// Get parent path
const parts = currentPath.split('/').filter(p => p);
parts.pop(); // Remove last folder
const parentPath = '/' + parts.join('/');
loadFolders(parentPath);
});
// Refresh current folder
refreshFoldersBtn.addEventListener('click', function() {
loadFolders(currentPath);
});
// Open the Create Folder modal
createFolderBtn.addEventListener('click', function() {
createFolderModal.classList.remove('hidden');
parentFolderPathDisplay.textContent = currentPath;
newFolderNameInput.value = '';
folderNameError.classList.add('hidden');
// Focus the input field for better UX
setTimeout(() => {
newFolderNameInput.focus();
}, 100);
});
// Close the Create Folder modal when clicking outside
createFolderModal.addEventListener('click', function(event) {
if (event.target === createFolderModal) {
createFolderModal.classList.add('hidden');
}
});
// Close the Create Folder modal
createFolderCancel.addEventListener('click', function() {
createFolderModal.classList.add('hidden');
});
// Handle Enter key in the folder name input
newFolderNameInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
createFolderSubmit.click();
}
});
// Submit new folder creation
createFolderSubmit.addEventListener('click', function() {
const folderName = newFolderNameInput.value.trim();
if (!folderName) {
folderNameError.textContent = 'Folder name cannot be empty.';
folderNameError.classList.remove('hidden');
return;
}
// Check for invalid characters
if (/[\/\\:*?"<>|]/.test(folderName)) {
folderNameError.textContent = 'Folder name contains invalid characters.';
folderNameError.classList.remove('hidden');
return;
}
// Show loading state
createFolderSubmit.disabled = true;
createFolderSubmit.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Creating...';
folderNameError.classList.add('hidden');
// Call API to create folder
fetch('/api/dropbox/create-folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value
},
body: JSON.stringify({
parent_path: currentPath,
folder_name: folderName
})
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw {
status: response.status,
statusText: response.statusText,
data: errorData
};
});
}
return response.json();
})
.then(data => {
if (data.error) {
throw {
status: 500,
statusText: 'Server Error',
data: { error: data.error }
};
}
// Close modal and refresh folder list
createFolderModal.classList.add('hidden');
// If the folder was created successfully, navigate to it
if (data.path) {
loadFolders(data.path);
} else {
// Just refresh the current folder
loadFolders(currentPath);
}
// Show success message
const successDiv = document.createElement('div');
successDiv.className = 'p-2 mb-3 bg-green-100 text-green-800 rounded text-sm';
successDiv.textContent = `Folder "${folderName}" created successfully.`;
successDiv.style.opacity = '1';
successDiv.style.transition = 'opacity 0.5s ease-in-out';
folderList.insertBefore(successDiv, folderList.firstChild);
// Fade out success message after 3 seconds
setTimeout(() => {
successDiv.style.opacity = '0';
setTimeout(() => {
if (successDiv.parentNode) {
successDiv.parentNode.removeChild(successDiv);
}
}, 500);
}, 3000);
})
.catch(error => {
console.error('Error creating folder:', error);
// Show error message in the modal
folderNameError.textContent = error.data?.error ||
(error.status === 409 ? 'A folder with this name already exists.' :
error.statusText || 'Error creating folder');
folderNameError.classList.remove('hidden');
// Also show detailed error in the main error display if it's a server error
if (error.status >= 500) {
showError(error);
}
})
.finally(() => {
// Reset button state
createFolderSubmit.disabled = false;
createFolderSubmit.innerHTML = 'Create';
});
});
// Function to load folders from Dropbox
function loadFolders(path) {
// Hide any previous errors
errorContainer.classList.add('hidden');
// Update UI
currentPath = path;
currentPathDisplay.textContent = path || '/';
// Show loading indicator
folderList.innerHTML = `
<div class="text-center p-8">
<div class="animate-spin inline-block w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full mb-2"></div>
<p>Loading folders...</p>
</div>
`;
// Make API request to get folders
fetch('/api/dropbox/folders?path=' + encodeURIComponent(path))
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw {
status: response.status,
statusText: response.statusText,
data: errorData
};
});
}
return response.json();
})
.then(data => {
// Save the response for debugging
lastApiResponse = data;
if (data.error) {
throw {
status: 500,
statusText: 'Server Error',
data: data
};
}
// Check if there's a warning message (returned when a path doesn't exist)
if (data.warning) {
// Display warning message
const warningDiv = document.createElement('div');
warningDiv.className = 'p-2 mb-3 bg-yellow-100 text-yellow-800 rounded text-sm';
warningDiv.textContent = data.warning;
folderList.innerHTML = '';
folderList.appendChild(warningDiv);
// Update the current path
currentPath = data.path;
currentPathDisplay.textContent = data.path;
} else {
// Clear folder list
folderList.innerHTML = '';
}
if (data.folders.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.className = 'text-center p-8 text-gray-500';
emptyDiv.innerHTML = `
<i class="fas fa-folder-open text-4xl mb-2"></i>
<p>No folders found in this location</p>
`;
folderList.appendChild(emptyDiv);
return;
}
// Sort folders alphabetically
data.folders.sort((a, b) => a.name.localeCompare(b.name));
// Add each folder to the list
data.folders.forEach(folder => {
const folderItem = document.createElement('div');
folderItem.className = 'p-2 hover:bg-gray-100 rounded cursor-pointer flex items-center';
folderItem.innerHTML = `
<i class="fas fa-folder text-blue-500 mr-2"></i>
<span>${folder.name}</span>
`;
folderItem.addEventListener('click', function() {
loadFolders(folder.path_display);
});
folderList.appendChild(folderItem);
});
})
.catch(error => {
console.error('Error loading folders:', error);
// Display error in the folder list
folderList.innerHTML = `
<div class="text-center p-8 text-red-500">
<i class="fas fa-exclamation-circle text-4xl mb-2"></i>
<p>${error.data?.error || error.statusText || 'Error loading folders'}</p>
<p class="text-sm mt-2">Make sure your Dropbox account is connected</p>
</div>
`;
// Also show detailed error in the profile page
showError(error);
});
}
function showError(error) {
// Format and display the error information
errorContainer.classList.remove('hidden');
let errorMessage = '';
if (error.status) {
errorMessage += `HTTP ${error.status}: `;
}
if (error.data && error.data.error) {
errorMessage += error.data.error;
} else if (error.statusText) {
errorMessage += error.statusText;
} else {
errorMessage += 'Unknown error occurred';
}
errorDetails.textContent = errorMessage;
// Show technical details
let debugText = '';
if (error.data) {
if (error.data.details) {
debugText += "Error Details:\n" + JSON.stringify(error.data.details, null, 2) + "\n\n";
}
if (error.data.traceback) {
debugText += "Traceback:\n" + error.data.traceback + "\n\n";
}
if (error.data.raw_response) {
debugText += "Raw Response:\n" + error.data.raw_response + "\n\n";
}
}
if (!debugText) {
debugText = JSON.stringify(error, null, 2);
}
debugInfo.textContent = debugText;
}
function closeModal() {
dropboxFolderModal.classList.add('hidden');
}
});
</script>
{% endblock %}