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);
+ }
+}