Files
TimeTrack/templates/profile.html

605 lines
19 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block content %}
<div class="profile-container">
<h1>My Profile</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="profile-grid">
<!-- Avatar Card -->
<div class="profile-card avatar-card">
<h3>Profile Picture</h3>
<div class="avatar-section">
<img src="{{ user.get_avatar_url(128) }}" alt="{{ user.username }}" class="profile-avatar" id="avatar-preview">
<div class="avatar-info">
<p><strong>{{ user.username }}</strong></p>
<p class="text-muted">{{ user.role.value if user.role else 'Team Member' }}</p>
</div>
</div>
<div class="avatar-controls">
<h4>Change Avatar</h4>
<div class="avatar-options">
<div class="avatar-option">
<input type="radio" id="avatar-default" name="avatar-type" value="default" checked>
<label for="avatar-default">Default Avatar</label>
</div>
<div class="avatar-option">
<input type="radio" id="avatar-upload" name="avatar-type" value="upload">
<label for="avatar-upload">Upload Image</label>
</div>
<div class="avatar-option">
<input type="radio" id="avatar-url" name="avatar-type" value="url">
<label for="avatar-url">Custom URL</label>
</div>
</div>
<!-- Default Avatar Options -->
<div id="default-avatar-options" class="avatar-option-panel">
<p class="help-text">Your default avatar is generated based on your username.</p>
<button type="button" class="btn btn-secondary" onclick="resetAvatar()">Use Default Avatar</button>
</div>
<!-- Upload Avatar Options -->
<div id="upload-avatar-options" class="avatar-option-panel" style="display: none;">
<form method="POST" action="{{ url_for('upload_avatar') }}" enctype="multipart/form-data" class="avatar-upload-form">
<div class="form-group">
<label for="avatar_file" class="file-upload-label">
<span class="upload-icon">📁</span>
<span class="upload-text">Choose an image file</span>
<span class="file-name" id="file-name">No file selected</span>
</label>
<input type="file" id="avatar_file" name="avatar_file" class="file-input"
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" required>
<small>Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP</small>
</div>
<div class="upload-preview" id="upload-preview" style="display: none;">
<img id="upload-preview-img" src="" alt="Preview">
</div>
<button type="submit" class="btn btn-primary" id="upload-btn" disabled>Upload Avatar</button>
</form>
</div>
<!-- URL Avatar Options -->
<div id="url-avatar-options" class="avatar-option-panel" style="display: none;">
<form method="POST" action="{{ url_for('update_avatar') }}" class="avatar-form">
<div class="form-group">
<label for="avatar_url">Avatar URL</label>
<input type="url" id="avatar_url" name="avatar_url" class="form-control"
placeholder="https://example.com/avatar.jpg"
value="{{ user.avatar_url or '' }}">
<small>Enter a direct link to an image (PNG, JPG, GIF)</small>
</div>
<button type="submit" class="btn btn-primary">Update Avatar</button>
</form>
</div>
</div>
</div>
<!-- Account Info Card -->
<div class="profile-card">
<h3>Account Information</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Username</span>
<span class="info-value">{{ user.username }}</span>
</div>
<div class="info-item">
<span class="info-label">Email</span>
<span class="info-value">
{% if user.email %}
{{ user.email }}
{% if not user.is_verified %}
<span class="badge badge-warning">Unverified</span>
{% endif %}
{% else %}
<span class="text-muted">Not provided</span>
{% endif %}
</span>
</div>
<div class="info-item">
<span class="info-label">Role</span>
<span class="info-value">{{ user.role.value if user.role else 'Team Member' }}</span>
</div>
<div class="info-item">
<span class="info-label">Company</span>
<span class="info-value">{{ user.company.name if user.company else 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">Team</span>
<span class="info-value">{{ user.team.name if user.team else 'No Team' }}</span>
</div>
<div class="info-item">
<span class="info-label">Member Since</span>
<span class="info-value">{{ user.created_at.strftime('%B %d, %Y') }}</span>
</div>
</div>
</div>
<!-- Email Settings Card -->
<div class="profile-card">
<h3>Email Settings</h3>
<form method="POST" action="{{ url_for('profile') }}" class="profile-form">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" class="form-control" value="{{ user.email or '' }}" placeholder="your@email.com">
<small>This email address is used for account recovery and notifications.</small>
</div>
{% if user.email and not user.is_verified %}
<div class="alert alert-warning">
<i>⚠️</i> Your email address is not verified.
<a href="{{ url_for('profile') }}" class="btn btn-sm btn-warning">Send Verification Email</a>
</div>
{% elif not user.email %}
<div class="alert alert-info">
<i></i> Adding an email address enables account recovery and important notifications.
</div>
{% endif %}
<button type="submit" class="btn btn-primary">{% if user.email %}Update{% else %}Add{% endif %} Email</button>
</form>
</div>
<!-- Password Settings Card -->
<div class="profile-card">
<h3>Change Password</h3>
<form method="POST" action="{{ url_for('profile') }}" class="password-form">
<!-- Hidden email field to maintain current email -->
<input type="hidden" name="email" value="{{ user.email or '' }}">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" class="form-control" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" class="form-control" required>
<small>Choose a strong password with at least 8 characters.</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
</div>
<button type="submit" class="btn btn-warning">Change Password</button>
</form>
</div>
<!-- Security Settings Card -->
<div class="profile-card security-card">
<h3>Two-Factor Authentication</h3>
<div class="security-status">
{% if user.two_factor_enabled %}
<div class="status-badge enabled">
<span class="status-icon"></span>
<span>Enabled</span>
</div>
<p>Two-factor authentication adds an extra layer of security to your account.</p>
<form method="POST" action="{{ url_for('disable_2fa') }}" class="disable-2fa-form"
onsubmit="return confirm('Are you sure you want to disable two-factor authentication?');">
<div class="form-group">
<label for="password_disable">Enter your password to disable 2FA:</label>
<input type="password" id="password_disable" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-danger">Disable 2FA</button>
</form>
{% else %}
<div class="status-badge disabled">
<span class="status-icon"></span>
<span>Disabled</span>
</div>
<p>Enable two-factor authentication to add an extra layer of security to your account.</p>
<a href="{{ url_for('setup_2fa') }}" class="btn btn-success">Enable 2FA</a>
{% endif %}
</div>
</div>
</div>
</div>
<style>
.profile-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.profile-card {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.profile-card h3 {
color: #333;
margin-bottom: 1.5rem;
font-size: 1.25rem;
font-weight: 600;
padding-bottom: 0.75rem;
border-bottom: 1px solid #e9ecef;
}
.profile-card h4 {
color: #495057;
font-size: 1rem;
font-weight: 600;
margin: 1.5rem 0 1rem;
}
/* Avatar Section */
.avatar-card {
grid-column: span 2;
}
.avatar-section {
display: flex;
align-items: center;
gap: 2rem;
margin-bottom: 2rem;
}
.profile-avatar {
width: 128px;
height: 128px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #e9ecef;
}
.avatar-info {
flex: 1;
}
.avatar-info p {
margin: 0.25rem 0;
}
.text-muted {
color: #6c757d;
font-size: 0.9rem;
}
.avatar-controls {
border-top: 1px solid #e9ecef;
padding-top: 1.5rem;
}
.avatar-options {
display: flex;
gap: 2rem;
margin-bottom: 1rem;
}
.avatar-option {
display: flex;
align-items: center;
gap: 0.5rem;
}
.avatar-option input[type="radio"] {
cursor: pointer;
}
.avatar-option label {
cursor: pointer;
margin-bottom: 0;
}
.avatar-option-panel {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 4px;
}
.help-text {
color: #6c757d;
font-size: 0.9rem;
margin-bottom: 1rem;
}
/* File Upload Styles */
.file-upload-label {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
border: 2px dashed #dee2e6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background: #f8f9fa;
}
.file-upload-label:hover {
border-color: #007bff;
background: #e7f3ff;
}
.upload-icon {
font-size: 1.5rem;
}
.upload-text {
flex: 1;
font-weight: 500;
color: #495057;
}
.file-name {
font-size: 0.875rem;
color: #6c757d;
}
.file-input {
display: none;
}
.upload-preview {
margin: 1rem 0;
text-align: center;
}
.upload-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
border: 2px solid #dee2e6;
}
/* Account Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.875rem;
color: #6c757d;
font-weight: 500;
}
.info-value {
font-size: 1rem;
color: #333;
}
/* Security Status */
.security-status {
text-align: center;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 600;
margin-bottom: 1rem;
}
.status-badge.enabled {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-badge.disabled {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-icon {
font-size: 1.25rem;
}
.disable-2fa-form {
margin-top: 1rem;
padding: 1rem;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
}
/* Form Styles */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-group small {
display: block;
margin-top: 0.25rem;
color: #6c757d;
font-size: 0.875rem;
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.profile-grid {
grid-template-columns: 1fr;
}
.avatar-card {
grid-column: span 1;
}
.avatar-section {
flex-direction: column;
text-align: center;
}
.avatar-options {
justify-content: center;
}
}
</style>
<script>
// Avatar type toggle
document.addEventListener('DOMContentLoaded', function() {
const avatarTypeRadios = document.querySelectorAll('input[name="avatar-type"]');
const defaultPanel = document.getElementById('default-avatar-options');
const uploadPanel = document.getElementById('upload-avatar-options');
const urlPanel = document.getElementById('url-avatar-options');
const avatarUrlInput = document.getElementById('avatar_url');
const avatarPreview = document.getElementById('avatar-preview');
const fileInput = document.getElementById('avatar_file');
const fileName = document.getElementById('file-name');
const uploadPreview = document.getElementById('upload-preview');
const uploadPreviewImg = document.getElementById('upload-preview-img');
const uploadBtn = document.getElementById('upload-btn');
avatarTypeRadios.forEach(radio => {
radio.addEventListener('change', function() {
// Hide all panels
defaultPanel.style.display = 'none';
uploadPanel.style.display = 'none';
urlPanel.style.display = 'none';
// Show selected panel
if (this.value === 'default') {
defaultPanel.style.display = 'block';
} else if (this.value === 'upload') {
uploadPanel.style.display = 'block';
} else if (this.value === 'url') {
urlPanel.style.display = 'block';
}
});
});
// File input handling
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// Update file name display
fileName.textContent = file.name;
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
this.value = '';
fileName.textContent = 'No file selected';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
return;
}
// Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
alert('Please select a valid image file (JPG, PNG, GIF, or WebP)');
this.value = '';
fileName.textContent = 'No file selected';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
return;
}
// Preview the image
const reader = new FileReader();
reader.onload = function(e) {
uploadPreviewImg.src = e.target.result;
uploadPreview.style.display = 'block';
uploadBtn.disabled = false;
};
reader.readAsDataURL(file);
} else {
fileName.textContent = 'No file selected';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
}
});
// Preview avatar URL
avatarUrlInput.addEventListener('input', function() {
const url = this.value.trim();
if (url && isValidUrl(url)) {
// Test if image loads
const img = new Image();
img.onload = function() {
avatarPreview.src = url;
};
img.onerror = function() {
// Keep current avatar if URL is invalid
avatarPreview.src = '{{ user.get_avatar_url(128) }}';
};
img.src = url;
}
});
});
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function resetAvatar() {
if (confirm('This will remove your custom avatar and use the default generated avatar. Continue?')) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("update_avatar") }}';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'avatar_url';
input.value = '';
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
</script>
{% endblock %}