diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..73e0929 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# Email Configuration +# Copy this file to .env and fill in your actual values + +# Email server settings +MAIL_SERVER=smtppro.zoho.eu +MAIL_PORT=587 +MAIL_USE_TLS=True +MAIL_USE_SSL=False + +# Email credentials +MAIL_USERNAME=contact@kobelly.be +MAIL_PASSWORD=Surface125300!? + +# Default sender +MAIL_DEFAULT_SENDER=contact@kobelly.be + +# Flask secret key (change this in production) +SECRET_KEY=thisisasecretkeylol + +# Flask environment +FLASK_ENV=development +FLASK_DEBUG=1 \ No newline at end of file diff --git a/README.md b/README.md index 8d04e26..e018f4f 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,61 @@ make deploy The build process creates cache-busted versions with MD5 hashes in the filename to ensure browsers always load the latest version. +## Contact Form Setup + +The website includes a fully functional contact form that sends email notifications. To set up email functionality: + +### 1. Email Configuration + +1. **Copy the environment template:** + ```bash + cp .env.example .env + ``` + +2. **Configure your email settings in `.env`:** + ```bash + # For Gmail (recommended) + MAIL_SERVER=smtp.gmail.com + MAIL_PORT=587 + MAIL_USE_TLS=True + MAIL_USERNAME=contact@kobelly.be + MAIL_PASSWORD=your-app-password-here + MAIL_DEFAULT_SENDER=contact@kobelly.be + ``` + +3. **For Gmail users:** + - Enable 2-factor authentication on your Google account + - Generate an "App Password" in Google Account settings + - Use the app password as `MAIL_PASSWORD` + +### 2. Alternative Email Providers + +**For other SMTP providers, update the settings accordingly:** +```bash +# Example for Outlook/Hotmail +MAIL_SERVER=smtp-mail.outlook.com +MAIL_PORT=587 +MAIL_USE_TLS=True + +# Example for custom SMTP server +MAIL_SERVER=your-smtp-server.com +MAIL_PORT=587 +MAIL_USE_TLS=True +``` + +### 3. Testing the Contact Form + +1. Start the application +2. Navigate to the Contact page +3. Fill out and submit the form +4. Check your email for the notification + +The form includes: +- **Form validation** (required fields, email format, phone format) +- **Loading states** (button shows spinner during submission) +- **Success/error notifications** (toast notifications) +- **Email notifications** (HTML and plain text formats) + ## Configuration ### Environment Variables diff --git a/app.py b/app.py index 6296105..ff1b8e1 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,93 @@ -from flask import Flask, render_template, request, session, redirect, url_for, make_response +from flask import Flask, render_template, request, session, redirect, url_for, make_response, jsonify, flash from flask_assets import Environment, Bundle +from flask_mail import Mail, Message from translation_manager import init_app, translate, get_current_language, create_language_selector, set_language import os +import re from datetime import datetime from xml.etree import ElementTree as ET app = Flask(__name__) app.secret_key = os.environ.get('SECRET_KEY', 'your-secret-key-change-in-production') +# Email configuration +app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.gmail.com') +app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587)) +app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'True').lower() == 'true' +app.config['MAIL_USE_SSL'] = os.environ.get('MAIL_USE_SSL', 'False').lower() == 'true' +app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'contact@kobelly.be') +app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', '') +app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'contact@kobelly.be') + +# Initialize Flask-Mail +mail = Mail(app) + +def validate_email(email): + """Validate email format""" + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + return re.match(pattern, email) is not None + +def validate_phone(phone): + """Validate phone number format (basic validation)""" + if not phone: + return True # Phone is optional + # Remove all non-digit characters + digits_only = re.sub(r'\D', '', phone) + return len(digits_only) >= 8 + +def send_contact_email(form_data): + """Send contact form email""" + try: + # Create email content + subject = f"New Contact Form Submission from {form_data['firstName']} {form_data['lastName']}" + + # HTML email body + html_body = f""" + + +

New Contact Form Submission

+

Name: {form_data['firstName']} {form_data['lastName']}

+

Email: {form_data['email']}

+

Phone: {form_data.get('phone', 'Not provided')}

+

Company: {form_data.get('company', 'Not provided')}

+

Service Interested In: {form_data['service']}

+

Budget: {form_data.get('budget', 'Not specified')}

+

Message:

+

{form_data['message'].replace(chr(10), '
')}

+ + + """ + + # Plain text email body + text_body = f""" + New Contact Form Submission + + Name: {form_data['firstName']} {form_data['lastName']} + Email: {form_data['email']} + Phone: {form_data.get('phone', 'Not provided')} + Company: {form_data.get('company', 'Not provided')} + Service Interested In: {form_data['service']} + Budget: {form_data.get('budget', 'Not specified')} + + Message: + {form_data['message']} + """ + + # Create message + msg = Message( + subject=subject, + recipients=[app.config['MAIL_DEFAULT_SENDER']], + html=html_body, + body=text_body + ) + + # Send email + mail.send(msg) + return True, "Email sent successfully" + + except Exception as e: + return False, f"Failed to send email: {str(e)}" + # Initialize Flask-Assets assets = Environment(app) assets.url = app.static_url_path @@ -109,6 +189,68 @@ def services(): def contact(): return render_template('contact.html') +@app.route('/contact', methods=['POST']) +def submit_contact(): + """Handle contact form submission""" + try: + # Get form data + data = request.get_json() + + # Validate required fields + required_fields = ['firstName', 'lastName', 'email', 'service', 'message'] + for field in required_fields: + if not data.get(field) or not data[field].strip(): + return jsonify({ + 'success': False, + 'message': f'{field.replace("firstName", "First name").replace("lastName", "Last name").title()} is required' + }), 400 + + # Validate email format + if not validate_email(data['email']): + return jsonify({ + 'success': False, + 'message': 'Please enter a valid email address' + }), 400 + + # Validate phone if provided + if data.get('phone') and not validate_phone(data['phone']): + return jsonify({ + 'success': False, + 'message': 'Please enter a valid phone number' + }), 400 + + # Prepare form data + form_data = { + 'firstName': data['firstName'].strip(), + 'lastName': data['lastName'].strip(), + 'email': data['email'].strip(), + 'phone': data.get('phone', '').strip(), + 'company': data.get('company', '').strip(), + 'service': data['service'].strip(), + 'budget': data.get('budget', '').strip(), + 'message': data['message'].strip() + } + + # Send email + success, message = send_contact_email(form_data) + + if success: + return jsonify({ + 'success': True, + 'message': 'Thank you for your message! I will get back to you within 48 hours.' + }) + else: + return jsonify({ + 'success': False, + 'message': 'Sorry, there was an error sending your message. Please try again or contact me directly.' + }), 500 + + except Exception as e: + return jsonify({ + 'success': False, + 'message': 'An unexpected error occurred. Please try again.' + }), 500 + @app.route('/about') def about(): return render_template('about.html') diff --git a/requirements.txt b/requirements.txt index 35408cb..ee0ec5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,6 @@ cssmin==0.2.0 jsmin==3.0.1 click==8.1.7 blinker==1.6.3 -# Translation system - JSON-based, no compilation needed \ No newline at end of file +# Translation system - JSON-based, no compilation needed +# Email functionality +Flask-Mail==0.9.1 \ No newline at end of file diff --git a/static/css/main.css b/static/css/main.css index efcb39d..3ffe181 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -4633,3 +4633,73 @@ body.theme-ecommerce { color: var(--secondary-color); background: rgba(52, 152, 219, 0.15); } + +/* Notification System */ +.notification { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + max-width: 400px; + transform: translateX(100%); + transition: transform 0.3s ease-in-out; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.notification.show { + transform: translateX(0); +} + +.notification-content { + display: flex; + align-items: center; + padding: 16px 20px; + border-radius: 8px; + font-weight: 500; +} + +.notification-success { + background: linear-gradient(135deg, #28a745, #20c997); + color: white; +} + +.notification-error { + background: linear-gradient(135deg, #dc3545, #e74c3c); + color: white; +} + +.notification-info { + background: linear-gradient(135deg, #17a2b8, #3498db); + color: white; +} + +.notification-close { + background: none; + border: none; + color: inherit; + margin-left: auto; + padding: 0; + cursor: pointer; + opacity: 0.8; + transition: opacity 0.2s; +} + +.notification-close:hover { + opacity: 1; +} + +/* Contact form button states */ +.contact-submit-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.contact-submit-btn .fa-spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/static/js/main.js b/static/js/main.js index e409e93..1b1f380 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -361,3 +361,105 @@ document.addEventListener('DOMContentLoaded', function() { `; document.head.appendChild(style); }); + +// Contact Form Handling +document.addEventListener('DOMContentLoaded', function() { + const contactForm = document.querySelector('.contact-form'); + const submitBtn = document.querySelector('.contact-submit-btn'); + + if (contactForm) { + contactForm.addEventListener('submit', function(e) { + e.preventDefault(); + + // Get form data + const formData = { + firstName: document.getElementById('firstName').value, + lastName: document.getElementById('lastName').value, + email: document.getElementById('email').value, + phone: document.getElementById('phone').value, + company: document.getElementById('company').value, + service: document.getElementById('service').value, + budget: document.getElementById('budget').value, + message: document.getElementById('message').value + }; + + // Disable submit button and show loading state + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = 'Sending...'; + } + + // Send form data + fetch('/contact', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Show success message + showNotification(data.message, 'success'); + // Reset form + contactForm.reset(); + } else { + // Show error message + showNotification(data.message, 'error'); + } + }) + .catch(error => { + console.error('Error:', error); + showNotification('An error occurred. Please try again.', 'error'); + }) + .finally(() => { + // Re-enable submit button + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = 'Send Message'; + } + }); + }); + } +}); + +// Notification system +function showNotification(message, type = 'info') { + // Remove existing notifications + const existingNotifications = document.querySelectorAll('.notification'); + existingNotifications.forEach(notification => notification.remove()); + + // Create notification element + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` +
+ + ${message} + +
+ `; + + // Add to page + document.body.appendChild(notification); + + // Show notification + setTimeout(() => { + notification.classList.add('show'); + }, 100); + + // Auto-hide after 5 seconds (except for errors) + if (type !== 'error') { + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => { + if (notification.parentElement) { + notification.remove(); + } + }, 300); + }, 5000); + } +}