Initial clean commit

This commit is contained in:
Christian Krakau-Louis
2025-05-13 08:59:02 +00:00
commit 03b982e7c2
113 changed files with 22973 additions and 0 deletions
@@ -0,0 +1,461 @@
{% extends 'base.html' %}
{% block title %}Audio Settings - Quizzical Beats{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-navy-800 font-montserrat mb-2">Custom Audio Settings</h1>
<p class="text-gray-600">Upload or generate your own intro, outro, and replay announcements for quizzes.</p>
</div>
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="mb-6 p-4 rounded {% if category == 'danger' %}bg-red-50 text-red-700 border border-red-300{% elif category == 'success' %}bg-green-50 text-green-700 border border-green-300{% else %}bg-blue-50 text-blue-700 border border-blue-300{% endif %}">
{{ message }}
</div>
{% endfor %}
<!-- Intro Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Intro Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays at the beginning of your music quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.intro_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom intro audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/intro.mp3') if not current_user.intro_mp3 else url_for('core.serve_user_audio', filepath=current_user.intro_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="intro">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default intro audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/intro.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="intro">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="intro">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="intro">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for intro announcement...">{{ default_texts.intro }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Outro Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Outro Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays at the end of your music quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.outro_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom outro audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/outro.mp3') if not current_user.outro_mp3 else url_for('core.serve_user_audio', filepath=current_user.outro_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="outro">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default outro audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/outro.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="outro">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="outro">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="outro">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for outro announcement...">{{ default_texts.outro }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Replay Audio Section -->
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="bg-navy-50 p-4 border-b">
<h2 class="text-xl font-semibold text-navy-800">Replay Audio</h2>
<p class="text-gray-600 text-sm mt-1">This audio plays before replaying all songs at the end of the quiz</p>
</div>
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2">Current Setting</h3>
{% if current_user.replay_mp3 %}
<div class="p-4 bg-navy-50 rounded-lg">
<p class="font-medium text-navy-800 mb-2">Custom replay audio is active</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/replay.mp3') if not current_user.replay_mp3 else url_for('core.serve_user_audio', filepath=current_user.replay_mp3) }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<form method="POST" class="mt-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="reset">
<input type="hidden" name="mp3_type" value="replay">
<button type="submit" class="text-red-600 text-sm hover:underline">Reset to default</button>
</form>
</div>
{% else %}
<div class="p-4 bg-gray-50 rounded-lg">
<p class="font-medium text-gray-700 mb-2">Using default replay audio</p>
<audio controls class="w-full">
<source src="{{ url_for('static', filename='audio/replay.mp3') }}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
{% endif %}
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-700 mb-4">Customize</h3>
<div class="mb-8">
<h4 class="font-medium text-gray-700 mb-2">Option 1: Upload MP3</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="upload">
<input type="hidden" name="mp3_type" value="replay">
<div class="flex items-center space-x-4">
<div class="flex-grow">
<input type="file" name="audio_file" accept=".mp3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500">
</div>
<button type="submit" class="bg-orange-500 hover:bg-orange-600 text-white py-2 px-4 rounded transition-colors">
Upload
</button>
</div>
</form>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2">Option 2: Generate with Text-to-Speech</h4>
{% if has_tts_services %}
<form method="GET" class="mb-4 flex items-end space-x-2">
<input type="hidden" name="mp3_type" value="replay">
<label class="block text-sm font-medium text-gray-700 mb-1">TTS Service</label>
<select name="tts_service" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for svc in tts_services %}
<option value="{{ svc.id }}" {% if selected_service and svc.id == selected_service.id %}selected{% endif %}>{{ svc.name }}</option>
{% endfor %}
</select>
<button type="submit" class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-2 rounded">Change</button>
</form>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="generate">
<input type="hidden" name="mp3_type" value="replay">
<input type="hidden" name="tts_service" value="{{ selected_service.id }}">
<p class="mt-1 text-sm text-gray-500">{{ selected_service.description if selected_service else '' }}</p>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Voice</label>
<select name="tts_voice" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for voice in selected_service.voices %}
<option value="{{ voice.id }}">{{ voice.name }} ({{ voice.gender }}, {{ voice.language }})</option>
{% endfor %}
</select>
</div>
{% if selected_service.id == 'openai' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model Quality</label>
<select name="openai_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.models %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<select name="elevenlabs_model" class="px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500 w-full">
{% for model in selected_service.models %}
<option value="{{ model.id }}">{{ model.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if selected_service.id == 'elevenlabs' and selected_service.settings %}
<div class="mb-4 bg-gray-50 p-4 rounded-md">
<h5 class="font-medium text-gray-700 mb-2">Voice Settings</h5>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Stability <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="stability" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More variable</span>
<span>More stable</span>
</div>
</div>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Similarity Boost <span class="text-xs text-gray-500">(0.0 - 1.0)</span></label>
<input type="range" name="similarity_boost" min="0" max="1" step="0.05" value="0.75" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>More unique</span>
<span>More similar</span>
</div>
</div>
</div>
{% endif %}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Text to Convert</label>
<textarea name="tts_text" rows="3" class="w-full px-4 py-2 border rounded-md focus:ring-orange-500 focus:border-orange-500"
placeholder="Enter text for replay announcement...">{{ default_texts.replay }}</textarea>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded transition-colors">
Generate Audio
</button>
</div>
</form>
{% else %}
<div class="p-4 bg-yellow-50 text-yellow-700 border border-yellow-300 rounded-md">
<p>Text-to-speech generation requires API credentials (AWS Polly, OpenAI, or ElevenLabs). Contact your administrator to enable this feature.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="mt-8 text-center">
<a href="{{ url_for('users.profile') }}" class="inline-block bg-gray-500 hover:bg-gray-600 text-white py-2 px-4 rounded transition-colors">
Back to Profile
</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- No Alpine.js needed -->
{% endblock %}
@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Change Password{% 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">Change Password</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.change_password') }}">
<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="current_password">
Current Password
</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="current_password" name="current_password" type="password" required>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="new_password">
New Password
</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_password" name="new_password" type="password" required>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="confirm_password">
Confirm New Password
</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="confirm_password" name="confirm_password" type="password" required>
</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">
Change Password
</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>
{% endblock %}
@@ -0,0 +1,566 @@
{% 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 %}
@@ -0,0 +1,56 @@
{% extends 'base.html' %}
{% block title %}Forgot Password - Quizzical Beats{% endblock %}
{% block content %}
<div class="flex min-h-screen items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div class="text-center">
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-navy-800 font-montserrat">
Forgot Password
</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Enter your email address and we'll send a reset link
</p>
</div>
<div class="mt-8 bg-white py-8 px-6 shadow-md rounded-lg">
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="mb-4 p-4 rounded-md {% if category == 'danger' %}bg-red-50 text-red-700{% elif category == 'success' %}bg-green-50 text-green-700{% else %}bg-blue-50 text-blue-700{% endif %}">
{{ message }}
</div>
{% endfor %}
<form class="space-y-6" action="{{ url_for('users.forgot_password') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
<div class="mt-1">
<input id="email" name="email" type="email" autocomplete="email" required
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500">
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500">
Send Password Reset Link
</button>
</div>
</form>
<div class="mt-6">
<div class="text-center">
<a href="{{ url_for('users.login') }}" class="text-sm text-orange-600 hover:text-orange-500">
Remember your password? Log in
</a>
</div>
</div>
</div>
<div class="text-center mt-4 text-sm text-gray-500">
<p>Need help? Contact support</p>
</div>
</div>
</div>
{% endblock %}
+92
View File
@@ -0,0 +1,92 @@
{% extends 'base.html' %}
{% block title %}Login{% 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">Login to Quizzical Beats</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.login') }}">
<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 or 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="username" name="username" type="text" placeholder="Enter your username or email">
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
Password
</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="password" name="password" type="password" placeholder="Enter your password">
</div>
<div class="mb-6 flex items-center">
<input class="mr-2" type="checkbox" id="remember" name="remember">
<label class="text-sm text-gray-700" for="remember">
Remember me
</label>
</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">
Login
</button>
<a class="inline-block align-baseline font-bold text-sm text-teal-500 hover:text-teal-800" href="{{ url_for('users.forgot_password') }}">
Forgot Password?
</a>
</div>
</form>
<!-- Social Login Options -->
{% if oauth_providers and (oauth_providers.google or oauth_providers.authentik) %}
<div class="mt-6 pt-6 border-t border-gray-200">
<div class="flex flex-col space-y-3">
{% if oauth_providers.google %}
<a href="{{ url_for('users.google_login') }}"
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<svg class="h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path>
<path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path>
<path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path>
<path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path>
</svg>
Sign in with Google
</a>
{% endif %}
{% if oauth_providers.authentik %}
<a href="{{ url_for('users.authentik_login') }}"
class="flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg" class="h-5 w-5 mr-2" alt="Authentik logo" />
Sign in with Authentik
</a>
{% endif %}
</div>
</div>
{% endif %}
<div class="mt-6 pt-6 border-t border-gray-200 text-center">
<p class="text-gray-700">
Don't have an account?
<a href="{{ url_for('users.register') }}" class="font-bold text-teal-500 hover:text-teal-800">
Register
</a>
</p>
</div>
</div>
{% endblock %}
+832
View File
@@ -0,0 +1,832 @@
{% extends 'base.html' %}
{% block title %}My Profile{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-6 text-navy-800">My 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 %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Profile Info -->
<div class="md:col-span-2">
<div class="bg-gray-50 p-4 rounded-lg mb-6">
<h3 class="text-lg font-semibold mb-3 text-navy-700">Account Information</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm text-gray-600">Username</p>
<p class="font-medium">{{ current_user.username }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Email</p>
<p class="font-medium">{{ current_user.email }}</p>
</div>
<div>
<p class="text-sm text-gray-600">Name</p>
<p class="font-medium">
{% if current_user.first_name or current_user.last_name %}
{{ current_user.first_name }} {{ current_user.last_name }}
{% else %}
<span class="text-gray-400">Not provided</span>
{% endif %}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Member Since</p>
<p class="font-medium">{{ current_user.created_at.strftime('%B %d, %Y') }}</p>
</div>
<!-- Add Admin Status -->
<div>
<p class="text-sm text-gray-600">Admin Status</p>
<p class="font-medium">
{% if current_user.is_admin() %}
<span class="text-green-600">Administrator</span>
<a href="{{ url_for('admin.index') }}" class="ml-2 text-xs text-blue-600 hover:underline">
Admin Dashboard
</a>
{% else %}
<span class="text-gray-400">Regular User</span>
{% if not admin_exists %}
<a href="{{ url_for('users.setup') }}" class="ml-2 text-xs text-blue-600 hover:underline">
Setup Admin
</a>
{% endif %}
{% endif %}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Dropbox Export Folder</p>
<p class="font-medium">
{{ current_user.dropbox_export_path or '/QuizzicalBeats' }}
</p>
</div>
</div>
</div>
<h3 class="text-lg font-semibold mb-3 text-navy-700">Account Actions</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<a href="{{ url_for('users.edit_profile') }}" class="bg-teal-500 hover:bg-teal-600 text-white py-2 px-4 rounded-md text-center transition-colors">
<i class="fas fa-user-edit mr-2"></i> Edit Profile
</a>
<a href="{{ url_for('users.change_password') }}" class="bg-navy-600 hover:bg-navy-700 text-white py-2 px-4 rounded-md text-center transition-colors">
<i class="fas fa-key mr-2"></i> Change Password
</a>
</div>
<!-- Spotify Connection Debug - Only for Admins -->
{% if current_user.is_admin() %}
<div class="mt-6 p-4 border border-gray-300 rounded-lg bg-gray-50">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-navy-700">Spotify Connection Debug</h3>
<button id="showSpotifyDebugBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
<i class="fas fa-bug mr-1"></i> Show Details
</button>
</div>
<div class="flex items-center text-sm">
<div class="w-3 h-3 rounded-full mr-2
{% if spotify_status == 'user' %}
bg-green-500
{% elif spotify_status == 'system' %}
bg-yellow-500
{% elif spotify_status == 'bearer' %}
bg-blue-500
{% else %}
bg-red-500
{% endif %}"></div>
<span>
{% if spotify_status == 'user' %}
Using your Spotify account
{% elif spotify_status == 'system' %}
Using system Spotify account
{% elif spotify_status == 'bearer' %}
Using manual bearer token
{% else %}
No Spotify connection active
{% endif %}
</span>
</div>
</div>
<!-- Dropbox Connection Debug - Only for Admins -->
<div class="mt-4 p-4 border border-gray-300 rounded-lg bg-gray-50">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-navy-700">Dropbox Connection Debug</h3>
<button id="showDropboxDebugBtn" class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
<i class="fas fa-bug mr-1"></i> Show Details
</button>
</div>
<div class="flex items-center text-sm">
<div class="w-3 h-3 rounded-full mr-2
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
bg-green-500
{% elif current_user.dropbox_token %}
bg-yellow-500
{% else %}
bg-red-500
{% endif %}"></div>
<span>
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
Active Dropbox connection
{% elif current_user.dropbox_token %}
Dropbox token expired or expiring soon
{% else %}
No Dropbox connection active
{% endif %}
</span>
</div>
</div>
{% endif %}
</div>
<!-- Connected Services -->
<div>
<h3 class="text-lg font-semibold mb-4 text-navy-700">Connected Services</h3>
<!-- Spotify Connection - Only shown for admins -->
{% if current_user.is_admin() %}
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
<div class="flex items-center mb-3">
<i class="fab fa-spotify text-[#1DB954] text-2xl mr-3"></i>
<div>
<h4 class="font-medium">Spotify</h4>
<p class="text-sm text-gray-600">
{% if current_user.spotify_token %}
Connected
{% if current_user.oauth_id %}
as {{ current_user.oauth_id }}
{% endif %}
{% else %}
Not connected
{% endif %}
</p>
</div>
</div>
<a href="{{ url_for('users.spotify_link') }}" class="inline-block w-full bg-{% if current_user.spotify_token %}gray-200 hover:bg-gray-300{% else %}[#1DB954] hover:bg-[#1ed760]{% endif %} text-{% if current_user.spotify_token %}gray-700{% else %}white{% endif %} text-center py-1 px-3 rounded-md text-sm transition-colors">
{% if current_user.spotify_token %}
Manage Connection
{% else %}
Connect Spotify
{% endif %}
</a>
</div>
{% endif %}
<!-- Dropbox Connection -->
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
<div class="flex items-center mb-3">
<i class="fab fa-dropbox text-[#0061FF] text-2xl mr-3"></i>
<div>
<h4 class="font-medium">Dropbox</h4>
<p class="text-sm text-gray-600">
{% if current_user.dropbox_id %}
Connected
{% if current_user.dropbox_id %}
as {{ current_user.dropbox_id[:10] }}...
{% endif %}
{% else %}
Not connected
{% endif %}
</p>
</div>
</div>
{% if current_user.dropbox_id %}
<div class="grid grid-cols-1 gap-2">
<p class="text-xs text-gray-600">
{% if current_user.dropbox_token_expiry %}
Token expires: {{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M') }}
{% endif %}
</p>
<form action="{{ url_for('users.dropbox_disconnect') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="inline-block w-full bg-gray-200 hover:bg-gray-300 text-gray-700 text-center py-1 px-3 rounded-md text-sm transition-colors">
Disconnect Dropbox
</button>
</form>
</div>
{% else %}
<a href="{{ url_for('users.dropbox_auth') }}" class="inline-block w-full bg-[#0061FF] hover:bg-[#0055DF] text-white text-center py-1 px-3 rounded-md text-sm transition-colors">
Connect Dropbox
</a>
{% endif %}
</div>
<!-- Manual Bearer Token Entry - Only for Admins -->
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
<h4 class="font-medium mb-2">Direct Spotify Access</h4>
<p class="text-xs text-gray-600 mb-3">
Enter a Spotify bearer token for direct API access.
This bypasses OAuth and allows immediate access to Spotify features.
</p>
<form action="{{ url_for('users.update_bearer_token') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="mb-2">
<textarea name="bearer_token" rows="2"
placeholder="BQBcQ1foQG0x14axu2kQVz..."
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm focus:outline-none focus:ring-teal-500 focus:border-teal-500">{{ session.get('access_token', '') }}</textarea>
<p class="mt-1 text-xs text-gray-500">Get a token from <a href="https://developer.spotify.com/console/" target="_blank" class="text-teal-600 hover:underline">Spotify Developer Console</a></p>
</div>
<div class="flex gap-2">
<button type="submit" class="px-4 py-2 bg-teal-600 text-white text-sm font-medium rounded-md hover:bg-teal-700 transition-colors">
Apply Token
</button>
{% if session.get('access_token') %}
<button type="submit" name="clear_token" value="1" class="px-4 py-2 bg-gray-500 text-white text-sm font-medium rounded-md hover:bg-gray-600 transition-colors">
Clear Token
</button>
{% endif %}
</div>
</form>
</div>
<!-- Audio Settings -->
<div class="bg-gray-50 p-4 rounded-lg mb-4 shadow-sm">
<h4 class="font-medium mb-2">Audio Settings</h4>
<p class="text-xs text-gray-600 mb-3">
Customize your audio preferences for quizzes.
</p>
<a href="{{ url_for('users.audio_settings') }}" class="inline-block w-full bg-orange-500 hover:bg-orange-600 text-white text-center py-2 px-4 rounded-md text-sm transition-colors">
Manage Audio Settings
</a>
</div>
<!-- Last Login Info -->
<div class="mt-6">
<h4 class="text-md font-medium mb-2">Last Login</h4>
<p class="text-sm text-gray-600">
{% if current_user.last_login %}
{{ current_user.last_login.strftime('%d %b %Y, %H:%M') }}
{% else %}
<span class="text-gray-400">No login history</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
<!-- Spotify Debug Modal - Only for Admins -->
{% if current_user.is_admin() %}
<div id="spotifyDebugModal" 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-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-navy-800">Spotify Connection Details</h3>
<button id="closeSpotifyDebugBtn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Connection Status -->
<div class="mb-4 p-3 rounded-lg
{% if spotify_status == 'user' %}
bg-green-100 border border-green-300
{% elif spotify_status == 'system' %}
bg-yellow-100 border border-yellow-300
{% elif spotify_status == 'bearer' %}
bg-blue-100 border border-blue-300
{% else %}
bg-red-100 border border-red-300
{% endif %}">
<div class="flex items-center font-semibold mb-2">
<div class="w-4 h-4 rounded-full mr-2
{% if spotify_status == 'user' %}
bg-green-500
{% elif spotify_status == 'system' %}
bg-yellow-500
{% elif spotify_status == 'bearer' %}
bg-blue-500
{% else %}
bg-red-500
{% endif %}"></div>
<span>
{% if spotify_status == 'user' %}
Using your Spotify account
{% elif spotify_status == 'system' %}
Using system Spotify account
{% elif spotify_status == 'bearer' %}
Using manual bearer token
{% else %}
No Spotify credentials available
{% endif %}
</span>
{% if token_source %}
<span class="ml-2 text-xs py-1 px-2 rounded-full bg-gray-200">
Source: {{ token_source }}
</span>
{% endif %}
</div>
<p class="text-sm">
{% if spotify_status == 'user' %}
Your personal Spotify account is connected and authenticated
{% elif spotify_status == 'system' %}
Using the fallback system Spotify account (refresh token from settings)
{% elif spotify_status == 'bearer' %}
Using a manual bearer token from session
{% else %}
No Spotify credentials available
{% endif %}
</p>
{% if token_source %}
<p class="mt-2 text-xs text-gray-600">
<i class="fas fa-info-circle mr-1"></i>
{% if token_source == 'user' %}
This token was automatically generated from your user account refresh token
{% elif token_source == 'system' %}
This token was automatically generated from the system fallback refresh token
{% else %}
Token source: {{ token_source }}
{% endif %}
</p>
{% endif %}
</div>
<!-- Debug Information -->
<div class="space-y-4">
<!-- Personal Account Details -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Your Spotify Account</h4>
<table class="w-full text-sm">
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Status:</td>
<td class="py-2">
{% if current_user.spotify_token %}
<span class="text-green-600">Connected</span>
{% else %}
<span class="text-red-600">Not connected</span>
{% endif %}
</td>
</tr>
{% if current_user.spotify_token %}
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Spotify ID:</td>
<td class="py-2">{{ current_user.oauth_id or 'Unknown' }}</td>
</tr>
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Token Expiry:</td>
<td class="py-2">
{% if current_user.spotify_token_expiry %}
{% if current_user.spotify_token_expiry > now %}
<span class="text-green-600">{{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
{% else %}
<span class="text-red-600">Expired ({{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
{% endif %}
{% else %}
Unknown
{% endif %}
</td>
</tr>
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Has Refresh Token:</td>
<td class="py-2">
{% if current_user.spotify_refresh_token %}
<span class="text-green-600">Yes</span>
{% else %}
<span class="text-red-600">No</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
</div>
<!-- System Account Details -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">System Spotify Account</h4>
<table class="w-full text-sm">
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Has Refresh Token:</td>
<td class="py-2">
{% if system_refresh_token %}
<span class="text-green-600">Yes</span>
{% else %}
<span class="text-red-600">No</span>
{% endif %}
</td>
</tr>
{% if system_refresh_token %}
<tr>
<td class="py-2 font-medium">Token Preview:</td>
<td class="py-2 font-mono">{{ system_refresh_token[:10] }}...{{ system_refresh_token[-5:] }}</td>
</tr>
{% endif %}
</table>
</div>
<!-- Manual Bearer Token -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Manual Bearer Token</h4>
<table class="w-full text-sm">
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Status:</td>
<td class="py-2">
{% if session_bearer and token_source == 'manual' %}
<span class="text-green-600">Active manual token</span>
{% elif session_bearer %}
<span class="text-gray-600">Present but not active</span>
{% else %}
<span class="text-gray-600">Not present</span>
{% endif %}
</td>
</tr>
{% if session_bearer %}
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Token Preview:</td>
<td class="py-2 font-mono">{{ session_bearer[:10] }}...{{ session_bearer[-5:] }}</td>
</tr>
{% if token_source == 'manual' and active_token_expiry %}
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Estimated Expiry:</td>
<td class="py-2">
{% if active_token_expiry > now %}
<span class="text-green-600">{{ active_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
<span class="text-xs text-gray-500">(Manual tokens typically expire after 1 hour)</span>
{% else %}
<span class="text-red-600">Likely expired ({{ active_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
{% endif %}
</td>
</tr>
{% endif %}
{% if active_username and token_source == 'manual' %}
<tr>
<td class="py-2 font-medium">Associated User:</td>
<td class="py-2">
<div class="flex items-center">
{% if active_user_image %}
<img src="{{ active_user_image }}" alt="User" class="w-6 h-6 rounded-full mr-2">
{% endif %}
<span>{{ active_username }} ({{ active_user_id }})</span>
</div>
</td>
</tr>
{% endif %}
{% endif %}
</table>
</div>
<!-- Current Active Token -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Current Active Spotify User</h4>
{% if active_username %}
<div class="flex items-center mb-3">
{% if active_user_image %}
<img src="{{ active_user_image }}" alt="Profile" class="w-10 h-10 rounded-full mr-3">
{% else %}
<div class="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center mr-3">
<i class="fab fa-spotify text-gray-600"></i>
</div>
{% endif %}
<div>
<p class="font-medium">{{ active_username }}</p>
<p class="text-sm text-gray-600">ID: {{ active_user_id }}</p>
</div>
</div>
{% elif token_source == 'client_credentials' %}
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3">
<i class="fab fa-spotify text-purple-600"></i>
</div>
<div>
<p class="font-medium">App Client Credentials</p>
<p class="text-sm text-gray-600">Using application's credentials (limited access)</p>
</div>
</div>
{% else %}
<p class="text-gray-600 italic">No active Spotify user detected</p>
{% endif %}
<div class="mt-3">
<h5 class="text-sm font-medium mb-1">Token Source</h5>
<div class="text-sm">
{% if token_source %}
<span class="px-2 py-1 rounded-full text-xs
{% if token_source == 'user' %}
bg-green-100 text-green-800
{% elif token_source == 'system' %}
bg-yellow-100 text-yellow-800
{% elif token_source == 'client_credentials' %}
bg-purple-100 text-purple-800
{% else %}
bg-blue-100 text-blue-800
{% endif %}">
{{ token_source }}
</span>
{% if token_source == 'client_credentials' and client_token_expiry %}
<span class="text-xs text-gray-600 ml-2">
Expires: {{ client_token_expiry|timestamp_to_datetime|format_datetime }}
</span>
{% endif %}
{% else %}
<span class="text-gray-600 italic">Unknown source</span>
{% endif %}
</div>
</div>
</div>
<!-- Priority Order -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Precedence</h4>
<p class="text-sm mb-2">Connection priority: Manual Bearer → Your Account → Client Credentials</p>
<div class="flex flex-col space-y-2 text-sm">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full
{% if spotify_status == 'bearer' %}bg-blue-500{% else %}bg-gray-300{% endif %}
mr-2"></div>
<span>1. Manual Bearer Token {% if spotify_status == 'bearer' %}(active){% endif %}</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full
{% if spotify_status == 'user' %}bg-green-500{% else %}bg-gray-300{% endif %}
mr-2"></div>
<span>2. User Account {% if spotify_status == 'user' %}(active){% endif %}</span>
</div>
<div class="flex items-center">
<div class="w-3 h-3 rounded-full
{% if spotify_status == 'client_credentials' %}bg-purple-500{% else %}bg-gray-300{% endif %}
mr-2"></div>
<span>3. App Client Credentials {% if spotify_status == 'client_credentials' %}(active){% endif %}</span>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="closeModalBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-md">
Close
</button>
</div>
</div>
</div>
</div>
<!-- Dropbox Debug Modal - Only for Admins -->
<div id="dropboxDebugModal" 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-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-navy-800">Dropbox Connection Details</h3>
<button id="closeDropboxDebugBtn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Connection Status -->
<div class="mb-4 p-3 rounded-lg
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
bg-green-100 border border-green-300
{% elif current_user.dropbox_token %}
bg-yellow-100 border border-yellow-300
{% else %}
bg-red-100 border border-red-300
{% endif %}">
<div class="flex items-center font-semibold mb-2">
<div class="w-4 h-4 rounded-full mr-2
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
bg-green-500
{% elif current_user.dropbox_token %}
bg-yellow-500
{% else %}
bg-red-500
{% endif %}"></div>
<span>
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
Active Dropbox connection
{% elif current_user.dropbox_token %}
Dropbox token expired or expiring soon
{% else %}
No Dropbox connection
{% endif %}
</span>
</div>
<p class="text-sm">
{% if current_user.dropbox_token and current_user.dropbox_token_expiry and current_user.dropbox_token_expiry > now %}
Your Dropbox account is connected and has a valid access token
{% elif current_user.dropbox_token %}
Your Dropbox account is connected but the token needs to be refreshed
{% else %}
No Dropbox credentials available
{% endif %}
</p>
</div>
<!-- Debug Information -->
<div class="space-y-4">
<!-- Dropbox Account Details -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Your Dropbox Account</h4>
<table class="w-full text-sm">
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Status:</td>
<td class="py-2">
{% if current_user.dropbox_id %}
<span class="text-green-600">Connected</span>
{% else %}
<span class="text-red-600">Not connected</span>
{% endif %}
</td>
</tr>
{% if current_user.dropbox_id %}
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">User Info:</td>
<td class="py-2">
<div class="flex items-center">
{% if session.get('dropbox_user_info') and session.get('dropbox_user_info').get('picture') %}
<img src="{{ session.get('dropbox_user_info').get('picture') }}" alt="Profile" class="w-8 h-8 rounded-full mr-2">
{% else %}
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-2">
<i class="fab fa-dropbox text-blue-600"></i>
</div>
{% endif %}
<div>
{% if session.get('dropbox_user_info') %}
<div class="font-medium">{{ session.get('dropbox_user_info').get('name') }}</div>
<div class="text-xs text-gray-500">{{ session.get('dropbox_user_info').get('email') }}</div>
{% else %}
<div class="text-gray-500 italic">User info not available</div>
{% endif %}
</div>
</div>
</td>
</tr>
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Dropbox ID:</td>
<td class="py-2">{{ current_user.dropbox_id }}</td>
</tr>
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Token Expiry:</td>
<td class="py-2">
{% if current_user.dropbox_token_expiry %}
{% if current_user.dropbox_token_expiry > now %}
<span class="text-green-600">{{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
{% else %}
<span class="text-red-600">Expired ({{ current_user.dropbox_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }})</span>
{% endif %}
{% else %}
Unknown
{% endif %}
</td>
</tr>
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Has Refresh Token:</td>
<td class="py-2">
{% if current_user.dropbox_refresh_token %}
<span class="text-green-600">Yes</span>
{% else %}
<span class="text-red-600">No</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
</div>
<!-- Dropbox Token Details -->
{% if current_user.dropbox_token %}
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Token Information</h4>
<table class="w-full text-sm">
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Access Token Preview:</td>
<td class="py-2 font-mono">
{{ current_user.dropbox_token[:10] }}...{{ current_user.dropbox_token[-5:] if current_user.dropbox_token|length > 15 else '' }}
</td>
</tr>
{% if current_user.dropbox_refresh_token %}
<tr class="border-b border-gray-200">
<td class="py-2 font-medium">Refresh Token Preview:</td>
<td class="py-2 font-mono">
{{ current_user.dropbox_refresh_token[:10] }}...{{ current_user.dropbox_refresh_token[-5:] if current_user.dropbox_refresh_token|length > 15 else '' }}
</td>
</tr>
{% endif %}
<tr>
<td class="py-2 font-medium">Token Status:</td>
<td class="py-2">
{% if current_user.dropbox_token_expiry %}
{% if current_user.dropbox_token_expiry > now %}
<span class="text-green-600">Valid for {{ ((current_user.dropbox_token_expiry - now).total_seconds() / 60)|int }} more minutes</span>
{% else %}
<span class="text-red-600">Expired {{ ((now - current_user.dropbox_token_expiry).total_seconds() / 60)|int }} minutes ago</span>
{% endif %}
{% else %}
<span class="text-gray-600">Unknown expiry time</span>
{% endif %}
</td>
</tr>
</table>
</div>
{% endif %}
<!-- Test Actions -->
<div class="p-3 bg-gray-50 rounded-lg">
<h4 class="font-semibold mb-2">Connection Test</h4>
<p class="text-sm text-gray-600 mb-3">
The following actions can help test your Dropbox connection:
</p>
<div class="space-y-2">
<a href="{{ url_for('users.dropbox_auth') }}" class="inline-block bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
Reconnect Dropbox
</a>
{% if current_user.dropbox_id %}
<form action="{{ url_for('users.dropbox_disconnect') }}" method="POST" class="inline-block">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white py-1 px-3 rounded-md text-sm transition-colors">
Disconnect Dropbox
</button>
</form>
{% endif %}
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="closeDropboxModalBtn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 py-2 px-4 rounded-md">
Close
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{% if current_user.is_admin() %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('spotifyDebugModal');
const showBtn = document.getElementById('showSpotifyDebugBtn');
const closeBtn = document.getElementById('closeSpotifyDebugBtn');
const closeModalBtn = document.getElementById('closeModalBtn');
// Show modal
showBtn.addEventListener('click', function() {
modal.classList.remove('hidden');
});
// Close modal
closeBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
closeModalBtn.addEventListener('click', function() {
modal.classList.add('hidden');
});
// Close modal when clicking outside
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.classList.add('hidden');
}
});
const dropboxModal = document.getElementById('dropboxDebugModal');
const showDropboxBtn = document.getElementById('showDropboxDebugBtn');
const closeDropboxBtn = document.getElementById('closeDropboxDebugBtn');
const closeDropboxModalBtn = document.getElementById('closeDropboxModalBtn');
// Show Dropbox modal
showDropboxBtn.addEventListener('click', function() {
dropboxModal.classList.remove('hidden');
});
// Close Dropbox modal
closeDropboxBtn.addEventListener('click', function() {
dropboxModal.classList.add('hidden');
});
closeDropboxModalBtn.addEventListener('click', function() {
dropboxModal.classList.add('hidden');
});
// Close Dropbox modal when clicking outside
dropboxModal.addEventListener('click', function(event) {
if (event.target === dropboxModal) {
dropboxModal.classList.add('hidden');
}
});
});
</script>
{% endif %}
{% endblock %}
+71
View File
@@ -0,0 +1,71 @@
{% extends 'base.html' %}
{% block title %}Register{% 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">Create an Account</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.register') }}">
<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="Choose a 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="Enter your email">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="password">
Password
</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="password" name="password" type="password" placeholder="Enter your password">
<p class="text-xs text-gray-500 mt-1">Must be at least 8 characters long.</p>
</div>
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="confirm_password">
Confirm Password
</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="confirm_password" name="confirm_password" type="password" placeholder="Confirm your password">
</div>
<div class="mb-6">
<button class="w-full bg-teal-500 hover:bg-teal-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
Register
</button>
</div>
</form>
<div class="mt-4 text-center">
<p class="text-gray-700">
Already have an account?
<a href="{{ url_for('users.login') }}" class="font-bold text-teal-500 hover:text-teal-800">
Login
</a>
</p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,67 @@
{% extends 'base.html' %}
{% block title %}Reset Password - Quizzical Beats{% endblock %}
{% block content %}
<div class="flex min-h-screen items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div class="text-center">
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-navy-800 font-montserrat">
Reset Your Password
</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Please enter your new password below
</p>
</div>
<div class="mt-8 bg-white py-8 px-6 shadow-md rounded-lg">
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="mb-4 p-4 rounded-md {% if category == 'danger' %}bg-red-50 text-red-700{% elif category == 'success' %}bg-green-50 text-green-700{% else %}bg-blue-50 text-blue-700{% endif %}">
{{ message }}
</div>
{% endfor %}
<form class="space-y-6" action="{{ url_for('users.reset_password', token=token) }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">New Password</label>
<div class="mt-1">
<input id="password" name="password" type="password" autocomplete="new-password" required
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500"
minlength="8">
<p class="text-xs text-gray-500 mt-1">Password must be at least 8 characters</p>
</div>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700">Confirm Password</label>
<div class="mt-1">
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" required
class="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500"
minlength="8">
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-orange-500 hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500">
Reset Password
</button>
</div>
</form>
<div class="mt-6">
<div class="text-center">
<a href="{{ url_for('users.login') }}" class="text-sm text-orange-600 hover:text-orange-500">
Return to login page
</a>
</div>
</div>
</div>
<div class="text-center mt-4 text-sm text-gray-500">
<p>Need a new reset link? <a href="{{ url_for('users.forgot_password') }}" class="text-orange-600 hover:text-orange-500">Request password reset again</a></p>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,143 @@
{% extends 'base.html' %}
{% block title %}Connect Spotify - Quizzical Beats{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-6 text-navy-800">Connect Spotify Account</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 %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Left column: Current status -->
<div class="bg-gray-50 p-6 rounded-lg">
<h3 class="text-lg font-semibold mb-4 text-navy-700">Spotify Connection Status</h3>
{% if current_user.spotify_token %}
<div class="mb-6 flex items-center">
<div class="mr-4 bg-green-100 p-3 rounded-full">
<i class="fab fa-spotify text-2xl text-green-600"></i>
</div>
<div>
<p class="font-medium text-green-700">Connected to Spotify</p>
<p class="text-sm text-gray-600">Your account is linked to Spotify</p>
{% if current_user.oauth_id %}
<p class="text-sm text-gray-600 mt-1">Spotify ID: {{ current_user.oauth_id }}</p>
{% endif %}
</div>
</div>
<div>
<h4 class="text-md font-medium mb-2">Token Information</h4>
<div class="bg-white p-4 rounded-lg border border-gray-200 text-sm">
<p class="flex justify-between mb-2">
<span class="font-medium">Token Status:</span>
<span class="{% if current_user.spotify_token_expiry and current_user.spotify_token_expiry > now %}text-green-600{% else %}text-red-600{% endif %}">
{% if current_user.spotify_token_expiry and current_user.spotify_token_expiry > now %}
Valid
{% else %}
Expired
{% endif %}
</span>
</p>
{% if current_user.spotify_token_expiry %}
<p class="flex justify-between">
<span class="font-medium">Expires:</span>
<span>{{ current_user.spotify_token_expiry.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</p>
{% endif %}
</div>
</div>
<form method="POST" action="{{ url_for('users.spotify_link') }}" class="mt-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="action" value="disconnect">
<button type="submit" class="bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded-md font-medium">
<i class="fas fa-unlink mr-2"></i> Disconnect Spotify
</button>
</form>
{% else %}
<div class="mb-6 flex items-center">
<div class="mr-4 bg-gray-200 p-3 rounded-full">
<i class="fab fa-spotify text-2xl text-gray-500"></i>
</div>
<div>
<p class="font-medium text-gray-700">Not Connected</p>
<p class="text-sm text-gray-600">Your account is not linked to Spotify</p>
</div>
</div>
<a href="{{ url_for('users.spotify_auth') }}" class="inline-block bg-[#1DB954] hover:bg-[#1ed760] text-white font-medium py-2 px-4 rounded-md">
<i class="fab fa-spotify mr-2"></i> Connect with Spotify
</a>
{% endif %}
</div>
<!-- Right column: Info and benefits -->
<div>
<h3 class="text-lg font-semibold mb-4 text-navy-700">Why Connect Spotify?</h3>
<div class="bg-teal-50 border-l-4 border-teal-500 p-4 rounded mb-6">
<p class="text-teal-700">
Connecting your Spotify account enhances your music quiz creation experience
</p>
</div>
<div class="space-y-4">
<div class="flex">
<div class="mr-3 text-teal-500">
<i class="fas fa-music"></i>
</div>
<div>
<h4 class="font-medium">Access Your Playlists</h4>
<p class="text-gray-600 text-sm">Import songs from your personal Spotify playlists</p>
</div>
</div>
<div class="flex">
<div class="mr-3 text-teal-500">
<i class="fas fa-search"></i>
</div>
<div>
<h4 class="font-medium">Search the Spotify Catalog</h4>
<p class="text-gray-600 text-sm">Find and import any track from Spotify's huge library</p>
</div>
</div>
<div class="flex">
<div class="mr-3 text-teal-500">
<i class="fas fa-chart-line"></i>
</div>
<div>
<h4 class="font-medium">Trending Playlists</h4>
<p class="text-gray-600 text-sm">Access Spotify's official and trending playlists</p>
</div>
</div>
</div>
<div class="mt-8 bg-gray-50 p-4 rounded-lg">
<h4 class="font-medium mb-2">Privacy Note</h4>
<p class="text-sm text-gray-600">
We only access your Spotify data to help you create music quizzes.
We don't share your information or post to your account.
</p>
</div>
</div>
</div>
<div class="mt-6 pt-6 border-t border-gray-200">
<a href="{{ url_for('users.profile') }}" class="text-teal-600 hover:text-teal-800">
<i class="fas fa-arrow-left mr-2"></i> Back to Profile
</a>
</div>
</div>
{% endblock %}