Login to TimeTrack
+Welcome Back
+Sign in to continue tracking
+diff --git a/app.py b/app.py index 2f97bf7..b8300a7 100644 --- a/app.py +++ b/app.py @@ -19,6 +19,7 @@ from sqlalchemy import func from functools import wraps from flask_mail import Mail, Message from dotenv import load_dotenv +from password_utils import PasswordValidator from werkzeug.security import check_password_hash # Load environment variables from .env file @@ -440,7 +441,7 @@ def home(): history=history, available_projects=available_projects) else: - return render_template('about.html', title='Home') + return render_template('index.html', title='Home') @app.route('/login', methods=['GET', 'POST']) def login(): @@ -573,6 +574,13 @@ def register(): error = 'Passwords do not match' elif not company_code: error = 'Company code is required' + + # Validate password strength + if not error: + validator = PasswordValidator() + is_valid, password_errors = validator.validate(password) + if not is_valid: + error = password_errors[0] # Show first error # Find company by code company = None @@ -685,6 +693,13 @@ def register_freelancer(): error = 'Password is required' elif password != confirm_password: error = 'Passwords do not match' + + # Validate password strength + if not error: + validator = PasswordValidator() + is_valid, password_errors = validator.validate(password) + if not is_valid: + error = password_errors[0] # Show first error # Check for existing users globally (freelancers get unique usernames/emails) if not error: @@ -1286,6 +1301,12 @@ def profile(): error = 'Current password is incorrect' elif new_password != confirm_password: error = 'New passwords do not match' + else: + # Validate password strength + validator = PasswordValidator() + is_valid, password_errors = validator.validate(new_password) + if not is_valid: + error = password_errors[0] # Show first error if error is None: user.email = email diff --git a/password_utils.py b/password_utils.py new file mode 100644 index 0000000..8c2b692 --- /dev/null +++ b/password_utils.py @@ -0,0 +1,89 @@ +"""Password validation utilities for TimeTrack""" +import re + +class PasswordValidator: + """Password strength validator with configurable rules""" + + def __init__(self): + self.min_length = 8 + self.require_uppercase = True + self.require_lowercase = True + self.require_numbers = True + self.require_special_chars = True + self.special_chars = r'!@#$%^&*()_+\-=\[\]{}|;:,.<>?' + + def validate(self, password): + """ + Validate a password against the configured rules. + Returns a tuple (is_valid, list_of_errors) + """ + errors = [] + + # Check minimum length + if len(password) < self.min_length: + errors.append(f'Password must be at least {self.min_length} characters long') + + # Check for uppercase letter + if self.require_uppercase and not re.search(r'[A-Z]', password): + errors.append('Password must contain at least one uppercase letter') + + # Check for lowercase letter + if self.require_lowercase and not re.search(r'[a-z]', password): + errors.append('Password must contain at least one lowercase letter') + + # Check for number + if self.require_numbers and not re.search(r'\d', password): + errors.append('Password must contain at least one number') + + # Check for special character + if self.require_special_chars and not re.search(f'[{re.escape(self.special_chars)}]', password): + errors.append('Password must contain at least one special character') + + return len(errors) == 0, errors + + def get_strength_score(self, password): + """ + Calculate a strength score for the password (0-100). + This matches the JavaScript implementation. + """ + score = 0 + + # Base scoring + if len(password) >= self.min_length: + score += 20 + + if re.search(r'[A-Z]', password): + score += 20 + + if re.search(r'[a-z]', password): + score += 20 + + if re.search(r'\d', password): + score += 20 + + if re.search(f'[{re.escape(self.special_chars)}]', password): + score += 20 + + # Bonus points for extra length + if len(password) >= 12: + score = min(100, score + 10) + if len(password) >= 16: + score = min(100, score + 10) + + return score + + def get_requirements_text(self): + """Get a user-friendly text describing password requirements""" + requirements = [] + requirements.append(f'At least {self.min_length} characters') + + if self.require_uppercase: + requirements.append('One uppercase letter') + if self.require_lowercase: + requirements.append('One lowercase letter') + if self.require_numbers: + requirements.append('One number') + if self.require_special_chars: + requirements.append('One special character (!@#$%^&*()_+-=[]{}|;:,.<>?)') + + return requirements \ No newline at end of file diff --git a/static/css/auth.css b/static/css/auth.css new file mode 100644 index 0000000..e1ce56a --- /dev/null +++ b/static/css/auth.css @@ -0,0 +1,580 @@ +/* Modern Authentication Pages Styles */ + +/* Auth page background */ +body.auth-page { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-attachment: fixed; + min-height: 100vh; + padding: 2rem; + overflow-x: hidden; + overflow-y: auto; /* Allow vertical scrolling */ + display: flex; + align-items: flex-start; /* Align to top */ + justify-content: center; +} + +/* Add min-height to html to ensure full coverage */ +html { + min-height: 100%; +} + +/* Animated background shapes */ +body.auth-page::before, +body.auth-page::after { + content: ''; + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + animation: float 20s infinite ease-in-out; +} + +body.auth-page::before { + width: 300px; + height: 300px; + top: -150px; + right: -150px; +} + +body.auth-page::after { + width: 500px; + height: 500px; + bottom: -250px; + left: -250px; + animation-delay: 10s; +} + +/* Auth container */ +.auth-container { + background: white; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + padding: 2.5rem; + width: 100%; + max-width: 480px; + animation: slideUp 0.6s ease-out; + position: relative; + z-index: 1; + margin: 2rem auto 4rem; /* Top and bottom margin */ +} + +/* Logo/Brand section */ +.auth-brand { + text-align: center; + margin-bottom: 2rem; +} + +.auth-brand h1 { + color: #333; + font-size: 2.5rem; + margin-bottom: 0.5rem; + font-weight: 700; +} + +.auth-brand p { + color: #666; + font-size: 1.1rem; +} + +/* Form styles */ +.auth-form { + margin-top: 2rem; +} + +.form-group { + margin-bottom: 1.25rem; + position: relative; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #333; + font-weight: 500; + font-size: 0.95rem; + transition: all 0.3s ease; +} + +.form-control { + width: 100%; + padding: 0.875rem 1rem; + border: 2px solid #e1e8ed; + border-radius: 10px; + font-size: 1rem; + transition: all 0.3s ease; + background-color: #f8f9fa; + line-height: 1.5; + height: 48px; /* Fixed height for consistent alignment */ +} + +.form-control:focus { + outline: none; + border-color: #667eea; + background-color: white; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1); +} + +/* Floating label effect */ +.form-group.floating-label { + position: relative; + margin-bottom: 2rem; +} + +.form-group.floating-label label { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + transition: all 0.3s ease; + pointer-events: none; + background: white; + padding: 0 0.5rem; + color: #999; +} + +.form-group.floating-label .form-control:focus ~ label, +.form-group.floating-label .form-control:not(:placeholder-shown) ~ label { + top: 0; + font-size: 0.85rem; + color: #667eea; +} + +/* Registration options */ +.registration-options { + margin-bottom: 1.5rem; +} + +.registration-options .alert { + border-radius: 12px; + border: none; + background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); + padding: 1rem; +} + +.registration-options h5 { + color: #333; + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.registration-options p { + margin-bottom: 0.5rem; + color: #555; +} + +.registration-options .btn-outline-primary { + border: 2px solid #667eea; + color: #667eea; + border-radius: 25px; + padding: 0.5rem 1.5rem; + transition: all 0.3s ease; + text-decoration: none; + display: inline-block; + font-weight: 500; +} + +.registration-options .btn-outline-primary:hover { + background: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); +} + +/* Submit button */ +.btn-primary { + width: 100%; + padding: 1rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 10px; + color: white; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); +} + +.btn-primary:active { + transform: translateY(0); +} + +/* Ripple effect for button */ +.btn-primary::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn-primary:active::after { + width: 300px; + height: 300px; +} + +/* Auth links */ +.auth-links { + text-align: center; + margin-top: 2rem; +} + +.auth-links p { + color: #666; + margin-bottom: 0.5rem; +} + +.auth-links a { + color: #667eea; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; +} + +.auth-links a:hover { + color: #764ba2; + text-decoration: underline; +} + +/* Verification notice */ +.verification-notice { + background: #f0f4ff; + border-radius: 10px; + padding: 1rem; + margin-top: 1.5rem; + text-align: center; +} + +.verification-notice p { + color: #555; + font-size: 0.9rem; + margin: 0; +} + +/* Alert messages */ +.alert { + padding: 1rem; + border-radius: 10px; + margin-bottom: 1.5rem; + border: none; + animation: fadeIn 0.3s ease; +} + +.alert-error { + background: #fee; + color: #c33; +} + +.alert-success { + background: #efe; + color: #3c3; +} + +.alert-info { + background: #eef; + color: #33c; +} + +/* Password strength indicator integration */ +.password-strength-container { + margin-top: 0.75rem; + animation: fadeIn 0.3s ease; +} + +.password-strength-indicator { + height: 6px; + background-color: #f0f0f0; + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.5rem; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.password-strength-bar { + height: 100%; + transition: all 0.4s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +/* Icon inputs */ +.input-icon { + position: relative; +} + +.input-icon i { + position: absolute; + left: 1rem; + color: #999; + font-size: 1.2rem; + z-index: 1; + pointer-events: none; +} + +.input-icon .form-control { + padding-left: 3rem; +} + +.input-icon label { + position: absolute; + left: 3rem; /* Start after the icon */ + top: 0.875rem; + color: #999; + pointer-events: none; + transition: all 0.3s ease; + background: white; + padding: 0 0.25rem; +} + +.input-icon .form-control:focus ~ label, +.input-icon .form-control:not(:placeholder-shown) ~ label { + top: -0.5rem; + left: 2.75rem; + font-size: 0.85rem; + color: #667eea; +} + +/* Checkbox styling */ +.form-check { + margin-bottom: 1.5rem; +} + +.form-check-input { + width: 20px; + height: 20px; + margin-right: 0.5rem; + cursor: pointer; +} + +.form-check-label { + cursor: pointer; + color: #555; +} + +/* Company code input special styling */ +.company-code-group { + position: relative; +} + +.company-code-group::before { + content: 'đĸ'; + position: absolute; + left: 1rem; + top: 2.5rem; /* Position below the label */ + font-size: 1.5rem; + z-index: 1; +} + +.company-code-group .form-control { + padding-left: 3.5rem; + font-family: 'Courier New', monospace; + font-size: 1.2rem; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +/* Animations */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 33% { + transform: translateY(-30px) rotate(120deg); + } + 66% { + transform: translateY(30px) rotate(240deg); + } +} + +/* Responsive design */ +@media (max-width: 568px) { + body.auth-page { + padding: 1rem; + align-items: flex-start; /* Align to top on mobile */ + } + + .auth-container { + padding: 2rem 1.5rem; + margin: 1rem auto; + max-height: none; /* Remove height limit on mobile */ + } + + .auth-brand h1 { + font-size: 2rem; + } + + body.auth-page::before, + body.auth-page::after { + display: none; + } +} + +/* Loading state */ +.btn-primary.loading { + color: transparent; +} + +.btn-primary.loading::before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + top: 50%; + left: 50%; + margin-left: -10px; + margin-top: -10px; + border: 2px solid #ffffff; + border-radius: 50%; + border-top-color: transparent; + animation: spinner 0.8s linear infinite; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +/* Social registration options */ +.social-registration { + margin-top: 2rem; + text-align: center; +} + +.social-divider { + position: relative; + text-align: center; + margin: 2rem 0; +} + +.social-divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #e1e8ed; +} + +.social-divider span { + background: white; + padding: 0 1rem; + color: #999; + position: relative; +} + +/* Progress indicator for multi-step forms */ +.registration-progress { + display: flex; + justify-content: space-between; + margin-bottom: 2rem; +} + +.progress-step { + flex: 1; + text-align: center; + position: relative; +} + +.progress-step::after { + content: ''; + position: absolute; + top: 15px; + right: -50%; + width: 100%; + height: 2px; + background: #e1e8ed; + z-index: -1; +} + +.progress-step:last-child::after { + display: none; +} + +.progress-step.active::after { + background: #667eea; +} + +.progress-step-number { + width: 30px; + height: 30px; + background: #e1e8ed; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 0.5rem; + font-weight: 600; + color: #999; +} + +.progress-step.active .progress-step-number { + background: #667eea; + color: white; +} + +.progress-step.completed .progress-step-number { + background: #4caf50; + color: white; +} + +.progress-step-label { + font-size: 0.85rem; + color: #666; +} + +/* Form helper text alignment */ +.form-text { + display: block; + margin-top: 0.25rem; + font-size: 0.875rem; + color: #6c757d; +} + +.input-icon .form-text { + padding-left: 3rem; /* Align with input content */ +} + +/* Fix textarea height */ +textarea.form-control { + height: auto; + min-height: 48px; +} + +/* Better icon vertical centering */ +.input-icon i { + display: flex; + align-items: center; + height: 48px; /* Match input height */ + top: 0; +} \ No newline at end of file diff --git a/static/css/splash.css b/static/css/splash.css new file mode 100644 index 0000000..04a9996 --- /dev/null +++ b/static/css/splash.css @@ -0,0 +1,507 @@ +/* Splash Page Styles */ + +/* Reset for splash page */ +.splash-container { + margin: -2rem -2rem 0 -2rem; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +/* Hero Section */ +.splash-hero { + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + color: white; + padding: 6rem 2rem; + text-align: center; + position: relative; + overflow: hidden; + min-height: 600px; + display: flex; + align-items: center; + justify-content: center; +} + +.hero-content { + max-width: 800px; + margin: 0 auto; + z-index: 2; + position: relative; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + letter-spacing: -1px; + animation: fadeInUp 1s ease-out; +} + +.hero-subtitle { + font-size: 1.5rem; + font-weight: 300; + margin-bottom: 2.5rem; + opacity: 0.9; + animation: fadeInUp 1s ease-out 0.2s both; +} + +.cta-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + animation: fadeInUp 1s ease-out 0.4s both; +} + +.btn-primary, .btn-secondary { + padding: 1rem 2.5rem; + font-size: 1.1rem; + border-radius: 50px; + text-decoration: none; + transition: all 0.3s ease; + font-weight: 500; + display: inline-block; +} + +.btn-primary { + background: #4CAF50; + color: white; + box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); +} + +.btn-primary:hover { + background: #45a049; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); +} + +.btn-secondary { + background: transparent; + color: white; + border: 2px solid white; +} + +.btn-secondary:hover { + background: white; + color: #2a5298; +} + +/* Floating Clock Animation */ +.hero-visual { + position: absolute; + right: 10%; + top: 50%; + transform: translateY(-50%); + opacity: 0.1; +} + +.floating-clock { + width: 300px; + height: 300px; + animation: float 6s ease-in-out infinite; +} + +.clock-face { + width: 100%; + height: 100%; + border: 8px solid white; + border-radius: 50%; + position: relative; +} + +.hour-hand, .minute-hand, .second-hand { + position: absolute; + background: white; + transform-origin: bottom center; + bottom: 50%; + left: 50%; +} + +.hour-hand { + width: 6px; + height: 80px; + margin-left: -3px; + animation: rotate 43200s linear infinite; +} + +.minute-hand { + width: 4px; + height: 100px; + margin-left: -2px; + animation: rotate 3600s linear infinite; +} + +.second-hand { + width: 2px; + height: 110px; + margin-left: -1px; + background: #4CAF50; + animation: rotate 60s linear infinite; +} + +/* Features Grid */ +.features-grid { + padding: 5rem 2rem; + background: #f8f9fa; +} + +.section-title { + text-align: center; + font-size: 2.5rem; + margin-bottom: 3rem; + color: #333; + font-weight: 600; +} + +.feature-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.feature-card { + background: white; + padding: 2.5rem; + border-radius: 12px; + text-align: center; + box-shadow: 0 5px 20px rgba(0,0,0,0.08); + transition: all 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0,0,0,0.12); +} + +.feature-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.feature-card h3 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: #333; +} + +.feature-card p { + color: #666; + line-height: 1.6; +} + +/* Statistics Section */ +.statistics { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 5rem 2rem; + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: 2rem; + position: relative; +} + +/* Add subtle overlay for better text contrast */ +.statistics::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + pointer-events: none; +} + +.statistics .section-title { + color: white; + width: 100%; + text-align: center; + margin-bottom: 3rem; + position: relative; + z-index: 1; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.stat-item { + text-align: center; + color: white; + position: relative; + z-index: 1; +} + +.stat-number { + font-size: 3rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: white; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.stat-label { + font-size: 1.1rem; + color: rgba(255, 255, 255, 1); + font-weight: 500; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} + +/* Testimonials */ +.testimonials { + padding: 5rem 2rem; + background: white; +} + +.testimonial-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.testimonial-card { + background: #f8f9fa; + padding: 2rem; + border-radius: 12px; + text-align: center; +} + +.stars { + font-size: 1.2rem; + margin-bottom: 1rem; +} + +.testimonial-card p { + font-size: 1.1rem; + line-height: 1.6; + color: #555; + margin-bottom: 1.5rem; + font-style: italic; +} + +.testimonial-author { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.testimonial-author strong { + color: #333; +} + +.testimonial-author span { + color: #666; + font-size: 0.9rem; +} + +/* Pricing Section */ +.pricing { + padding: 5rem 2rem; + background: #f8f9fa; +} + +.pricing-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + max-width: 1000px; + margin: 0 auto; +} + +.pricing-card { + background: white; + padding: 2.5rem; + border-radius: 12px; + text-align: center; + position: relative; + box-shadow: 0 5px 20px rgba(0,0,0,0.08); + transition: all 0.3s ease; +} + +.pricing-card.featured { + transform: scale(1.05); + box-shadow: 0 10px 40px rgba(0,0,0,0.15); +} + +.badge { + position: absolute; + top: -15px; + left: 50%; + transform: translateX(-50%); + background: #4CAF50; + color: white; + padding: 0.5rem 1.5rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; +} + +.pricing-card h3 { + font-size: 1.8rem; + margin-bottom: 1rem; + color: #333; +} + +.price { + font-size: 3rem; + font-weight: 700; + color: #2a5298; + margin-bottom: 2rem; +} + +.price span { + font-size: 1rem; + font-weight: 400; + color: #666; +} + +.pricing-features { + list-style: none; + padding: 0; + margin: 0 0 2rem 0; +} + +.pricing-features li { + padding: 0.75rem 0; + color: #555; + border-bottom: 1px solid #eee; +} + +.pricing-features li:last-child { + border-bottom: none; +} + +.btn-pricing { + display: inline-block; + padding: 1rem 2rem; + background: #4CAF50; + color: white; + text-decoration: none; + border-radius: 6px; + transition: all 0.3s ease; + font-weight: 500; +} + +.btn-pricing:hover { + background: #45a049; + transform: translateY(-2px); +} + +.pricing-card.featured .btn-pricing { + background: #2a5298; +} + +.pricing-card.featured .btn-pricing:hover { + background: #1e3c72; +} + +/* Final CTA */ +.final-cta { + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + color: white; + padding: 5rem 2rem; + text-align: center; +} + +.final-cta h2 { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.final-cta p { + font-size: 1.2rem; + margin-bottom: 2rem; + opacity: 0.9; +} + +.btn-primary.large { + font-size: 1.2rem; + padding: 1.25rem 3rem; +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(-50%) translateX(0); + } + 50% { + transform: translateY(-50%) translateX(20px); + } +} + +@keyframes rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.2rem; + } + + .cta-buttons { + flex-direction: column; + align-items: center; + } + + .btn-primary, .btn-secondary { + width: 200px; + } + + .hero-visual { + display: none; + } + + .section-title { + font-size: 2rem; + } + + .stat-number { + font-size: 2.5rem; + } + + .pricing-card.featured { + transform: none; + } +} + +/* Ripple Effect */ +.btn-primary, .btn-secondary, .btn-pricing { + position: relative; + overflow: hidden; +} + +.ripple { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + transform: scale(0); + animation: ripple-animation 0.6s ease-out; +} + +@keyframes ripple-animation { + to { + transform: scale(4); + opacity: 0; + } +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index e6408e7..584ba39 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -85,6 +85,12 @@ body { width: 60px; } +.sidebar.collapsed .sidebar-header { + padding: 1rem 0.5rem; + flex-direction: column; + gap: 0.75rem; +} + .sidebar-header { padding: 1.5rem; background-color: #34495e; @@ -92,6 +98,8 @@ body { justify-content: space-between; align-items: center; border-bottom: 1px solid #3a5269; + position: relative; + gap: 0.5rem; } .sidebar-header h2 { @@ -252,6 +260,11 @@ body { transition: margin-left 0.3s ease; } +/* Full width when no user (no sidebar) */ +body:not(.has-user) .main-content { + margin-left: 0; +} + /* Main content adjustments for collapsed sidebar */ body:has(.sidebar.collapsed) .main-content { margin-left: 60px; @@ -476,6 +489,11 @@ footer { transition: margin-left 0.3s ease; } +/* Full width footer when no user (no sidebar) */ +body:not(.has-user) footer { + margin-left: 0; +} + /* Footer adjustments for collapsed sidebar */ body:has(.sidebar.collapsed) footer { margin-left: 60px; @@ -2296,4 +2314,245 @@ input[type="time"]::-webkit-datetime-edit { gap: 1rem; align-items: stretch; } +} + +/* User Dropdown Styles */ +.user-dropdown-toggle { + display: block; + padding: 0.75rem 1.25rem; + color: white; + text-decoration: none; + transition: background-color 0.3s ease; + cursor: pointer; + width: 100%; + text-align: left; +} + +.user-dropdown-toggle:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.user-dropdown-toggle .nav-icon { + margin-right: 1rem; +} + +.sidebar.collapsed .user-dropdown-toggle .nav-text { + display: none; +} + +.sidebar.collapsed .user-dropdown-toggle { + padding: 0.75rem 0.5rem; +} + +.sidebar.collapsed .user-dropdown-toggle .nav-icon { + margin-right: 0; +} + +.user-dropdown-toggle .dropdown-arrow { + float: right; + transition: transform 0.3s ease; +} + +.user-dropdown-toggle.active .dropdown-arrow { + transform: rotate(180deg); +} + +/* User Dropdown Context Menu */ +.user-dropdown-modal { + position: absolute; + top: 100%; + right: 10px; + background-color: white; + border-radius: 4px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + z-index: 2000; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; + min-width: 200px; + margin-top: 5px; +} + +.user-dropdown-modal.active { + opacity: 1; + visibility: visible; +} + +.user-dropdown-header { + padding: 1rem; + background-color: #f8f8f8; + color: #333; + border-radius: 4px 4px 0 0; + border-bottom: 1px solid #e0e0e0; +} + +.user-dropdown-header h3 { + margin: 0; + font-size: 1rem; + font-weight: normal; +} + +.user-dropdown-header .user-info { + margin-top: 0.25rem; + font-size: 0.85rem; + color: #666; +} + +.user-dropdown-menu { + padding: 0.5rem 0; +} + +.user-dropdown-menu ul { + list-style: none; + margin: 0; + padding: 0; +} + +.user-dropdown-menu li { + margin: 0; +} + +.user-dropdown-menu a { + display: flex; + align-items: center; + padding: 0.5rem 1rem; + color: #333; + text-decoration: none; + transition: background-color 0.2s ease; + font-size: 0.9rem; +} + +.user-dropdown-menu a:hover { + background-color: #f0f0f0; +} + +.user-dropdown-menu .nav-icon { + margin-right: 0.5rem; + font-size: 1rem; +} + +.user-dropdown-divider { + height: 1px; + background-color: #e0e0e0; + margin: 0.5rem 0; +} + +/* User Dropdown Overlay - Not needed for context menu style */ +/* .user-dropdown-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1999; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.user-dropdown-overlay.active { + opacity: 1; + visibility: visible; +} */ + +/* Mobile adjustments */ +@media (max-width: 1024px) { + .user-dropdown-modal { + width: 90%; + max-width: 400px; + } +} + +/* Password Strength Indicator Styles */ +.password-strength-container { + margin-top: 0.5rem; +} + +.password-strength-indicator { + height: 5px; + background-color: #e9ecef; + border-radius: 3px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.password-strength-bar { + height: 100%; + width: 0%; + transition: width 0.3s ease, background-color 0.3s ease; + border-radius: 3px; +} + +.password-strength-bar.strength-weak { + background-color: #dc3545; +} + +.password-strength-bar.strength-fair { + background-color: #ffc107; +} + +.password-strength-bar.strength-good { + background-color: #17a2b8; +} + +.password-strength-bar.strength-strong { + background-color: #28a745; +} + +.password-strength-text { + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.password-strength-text.text-weak { + color: #dc3545; +} + +.password-strength-text.text-fair { + color: #ffc107; +} + +.password-strength-text.text-good { + color: #17a2b8; +} + +.password-strength-text.text-strong { + color: #28a745; +} + +.password-requirements { + list-style: none; + padding-left: 0; + margin: 0.5rem 0 0 0; + font-size: 0.85rem; +} + +.password-requirements li { + color: #6c757d; + padding: 0.25rem 0; + position: relative; + padding-left: 1.5rem; +} + +.password-requirements li:before { + content: "â"; + position: absolute; + left: 0; + color: #dc3545; +} + +.password-match-indicator { + font-size: 0.875rem; + margin-top: 0.5rem; + font-weight: 500; +} + +.password-match-indicator.match { + color: #28a745; +} + +.password-match-indicator.no-match { + color: #dc3545; } \ No newline at end of file diff --git a/static/js/auth-animations.js b/static/js/auth-animations.js new file mode 100644 index 0000000..f213f3a --- /dev/null +++ b/static/js/auth-animations.js @@ -0,0 +1,224 @@ +// Authentication Page Animations and Interactions + +document.addEventListener('DOMContentLoaded', function() { + // Add loading state to submit button + const form = document.querySelector('.auth-form'); + const submitBtn = document.querySelector('.btn-primary'); + + if (form && submitBtn) { + form.addEventListener('submit', function(e) { + // Check if form is valid + if (form.checkValidity()) { + submitBtn.classList.add('loading'); + submitBtn.disabled = true; + } + }); + } + + // Animate form fields on focus + const formInputs = document.querySelectorAll('.form-control'); + formInputs.forEach(input => { + input.addEventListener('focus', function() { + this.parentElement.classList.add('focused'); + }); + + input.addEventListener('blur', function() { + if (!this.value) { + this.parentElement.classList.remove('focused'); + } + }); + + // Check if input has value on load (for browser autofill) + if (input.value) { + input.parentElement.classList.add('focused'); + } + }); + + // Company code formatting + const companyCodeInput = document.querySelector('#company_code'); + if (companyCodeInput) { + companyCodeInput.addEventListener('input', function(e) { + // Convert to uppercase and remove non-alphanumeric characters + let value = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, ''); + + // Add dashes every 4 characters + if (value.length > 4 && !value.includes('-')) { + value = value.match(/.{1,4}/g).join('-'); + } + + e.target.value = value; + }); + } + + // Smooth scroll to alert messages + const alerts = document.querySelectorAll('.alert'); + if (alerts.length > 0) { + alerts[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + + // Add ripple effect to buttons + const buttons = document.querySelectorAll('.btn-primary, .btn-outline-primary'); + buttons.forEach(button => { + button.addEventListener('click', function(e) { + const ripple = document.createElement('span'); + const rect = this.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height); + const x = e.clientX - rect.left - size / 2; + const y = e.clientY - rect.top - size / 2; + + ripple.style.width = ripple.style.height = size + 'px'; + ripple.style.left = x + 'px'; + ripple.style.top = y + 'px'; + ripple.classList.add('ripple'); + + // Remove any existing ripples + const existingRipple = this.querySelector('.ripple'); + if (existingRipple) { + existingRipple.remove(); + } + + this.appendChild(ripple); + + setTimeout(() => ripple.remove(), 600); + }); + }); + + // Animate registration options + const registrationOptions = document.querySelector('.registration-options'); + if (registrationOptions) { + registrationOptions.style.opacity = '0'; + registrationOptions.style.transform = 'translateY(20px)'; + + setTimeout(() => { + registrationOptions.style.transition = 'all 0.6s ease'; + registrationOptions.style.opacity = '1'; + registrationOptions.style.transform = 'translateY(0)'; + }, 300); + } + + // Password visibility toggle + const passwordInputs = document.querySelectorAll('input[type="password"]'); + passwordInputs.forEach(input => { + const wrapper = input.parentElement; + const toggleBtn = document.createElement('button'); + toggleBtn.type = 'button'; + toggleBtn.className = 'password-toggle'; + toggleBtn.innerHTML = 'đī¸'; + toggleBtn.style.cssText = ` + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + font-size: 1.2rem; + opacity: 0.6; + transition: opacity 0.3s ease; + `; + + toggleBtn.addEventListener('click', function() { + if (input.type === 'password') { + input.type = 'text'; + this.innerHTML = 'đ'; + } else { + input.type = 'password'; + this.innerHTML = 'đī¸'; + } + }); + + toggleBtn.addEventListener('mouseenter', function() { + this.style.opacity = '1'; + }); + + toggleBtn.addEventListener('mouseleave', function() { + this.style.opacity = '0.6'; + }); + + wrapper.style.position = 'relative'; + wrapper.appendChild(toggleBtn); + }); + + // Form validation feedback + const requiredInputs = document.querySelectorAll('.form-control[required]'); + requiredInputs.forEach(input => { + input.addEventListener('blur', function() { + if (this.value.trim() === '') { + this.classList.add('invalid'); + this.classList.remove('valid'); + } else { + this.classList.add('valid'); + this.classList.remove('invalid'); + } + }); + }); + + // Email validation + const emailInput = document.querySelector('input[type="email"]'); + if (emailInput) { + emailInput.addEventListener('blur', function() { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (this.value && !emailRegex.test(this.value)) { + this.classList.add('invalid'); + this.classList.remove('valid'); + + // Add error message if not exists + if (!this.parentElement.querySelector('.error-message')) { + const errorMsg = document.createElement('small'); + errorMsg.className = 'error-message'; + errorMsg.style.color = '#dc3545'; + errorMsg.textContent = 'Please enter a valid email address'; + this.parentElement.appendChild(errorMsg); + } + } else if (this.value) { + // Remove error message if exists + const errorMsg = this.parentElement.querySelector('.error-message'); + if (errorMsg) { + errorMsg.remove(); + } + } + }); + } + + // Animate auth container on load + const authContainer = document.querySelector('.auth-container'); + if (authContainer) { + authContainer.style.opacity = '0'; + authContainer.style.transform = 'scale(0.95)'; + + setTimeout(() => { + authContainer.style.transition = 'all 0.5s ease'; + authContainer.style.opacity = '1'; + authContainer.style.transform = 'scale(1)'; + }, 100); + } +}); + +// Add CSS for valid/invalid states +const style = document.createElement('style'); +style.textContent = ` + .form-control.valid { + border-color: #28a745 !important; + } + + .form-control.invalid { + border-color: #dc3545 !important; + } + + .ripple { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.6); + transform: scale(0); + animation: ripple-animation 0.6s ease-out; + pointer-events: none; + } + + @keyframes ripple-animation { + to { + transform: scale(4); + opacity: 0; + } + } +`; +document.head.appendChild(style); \ No newline at end of file diff --git a/static/js/password-strength.js b/static/js/password-strength.js new file mode 100644 index 0000000..f5e0f8b --- /dev/null +++ b/static/js/password-strength.js @@ -0,0 +1,221 @@ +// Password Strength Indicator +document.addEventListener('DOMContentLoaded', function() { + // Password strength rules + const passwordRules = { + minLength: 8, + requireUppercase: true, + requireLowercase: true, + requireNumbers: true, + requireSpecialChars: true, + specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?' + }; + + // Function to check password strength + function checkPasswordStrength(password) { + let strength = 0; + const feedback = []; + + // Check minimum length + if (password.length >= passwordRules.minLength) { + strength += 20; + } else { + feedback.push(`At least ${passwordRules.minLength} characters`); + } + + // Check for uppercase letters + if (passwordRules.requireUppercase && /[A-Z]/.test(password)) { + strength += 20; + } else if (passwordRules.requireUppercase) { + feedback.push('One uppercase letter'); + } + + // Check for lowercase letters + if (passwordRules.requireLowercase && /[a-z]/.test(password)) { + strength += 20; + } else if (passwordRules.requireLowercase) { + feedback.push('One lowercase letter'); + } + + // Check for numbers + if (passwordRules.requireNumbers && /\d/.test(password)) { + strength += 20; + } else if (passwordRules.requireNumbers) { + feedback.push('One number'); + } + + // Check for special characters + const specialCharRegex = new RegExp(`[${passwordRules.specialChars.replace(/[\[\]\\]/g, '\\$&')}]`); + if (passwordRules.requireSpecialChars && specialCharRegex.test(password)) { + strength += 20; + } else if (passwordRules.requireSpecialChars) { + feedback.push('One special character'); + } + + // Bonus points for length + if (password.length >= 12) { + strength = Math.min(100, strength + 10); + } + if (password.length >= 16) { + strength = Math.min(100, strength + 10); + } + + return { + score: strength, + feedback: feedback, + isValid: strength >= 100 + }; + } + + // Function to update the strength indicator UI + function updateStrengthIndicator(input, result) { + let container = input.parentElement.querySelector('.password-strength-container'); + + // Create container if it doesn't exist + if (!container) { + container = document.createElement('div'); + container.className = 'password-strength-container'; + + const indicator = document.createElement('div'); + indicator.className = 'password-strength-indicator'; + + const bar = document.createElement('div'); + bar.className = 'password-strength-bar'; + + const text = document.createElement('div'); + text.className = 'password-strength-text'; + + const requirements = document.createElement('ul'); + requirements.className = 'password-requirements'; + + indicator.appendChild(bar); + container.appendChild(indicator); + container.appendChild(text); + container.appendChild(requirements); + + input.parentElement.appendChild(container); + } + + const bar = container.querySelector('.password-strength-bar'); + const text = container.querySelector('.password-strength-text'); + const requirements = container.querySelector('.password-requirements'); + + // Update bar width and color + bar.style.width = result.score + '%'; + + // Remove all strength classes + bar.className = 'password-strength-bar'; + + // Add appropriate class based on score + if (result.score < 40) { + bar.classList.add('strength-weak'); + text.textContent = 'Weak'; + text.className = 'password-strength-text text-weak'; + } else if (result.score < 70) { + bar.classList.add('strength-fair'); + text.textContent = 'Fair'; + text.className = 'password-strength-text text-fair'; + } else if (result.score < 100) { + bar.classList.add('strength-good'); + text.textContent = 'Good'; + text.className = 'password-strength-text text-good'; + } else { + bar.classList.add('strength-strong'); + text.textContent = 'Strong'; + text.className = 'password-strength-text text-strong'; + } + + // Update requirements list + requirements.innerHTML = ''; + if (result.feedback.length > 0) { + requirements.innerHTML = '
Track your work hours easily and efficiently
-Experience the future of time management with TimeTrack's intelligent tracking system
+ +Start tracking in seconds with our intuitive one-click interface
+Gain insights with comprehensive reports and visual dashboards
+Organize work into sprints with agile project tracking
+Manage teams, projects, and resources all in one place
+Bank-level encryption with role-based access control
+Perfect for agencies managing multiple client accounts
+Create your free account in seconds. No credit card required.
+Add your company, teams, and projects to organize your time tracking.
+Click "Arrive" to start tracking, "Leave" when done. It's that simple!
++ TimeTrack is open source software. Host it yourself or use our free hosted version. +
+Start tracking your time effectively today - no strings attached
+ Create Free Account +Simply click "Arrive" when you start working and "Leave" when you're done.
-Use the Pause button when taking breaks. Your break time is tracked separately.
-View your complete work history with precise timestamps and durations.
-No complicated setup or configuration needed. Start tracking right away!
-