Add form mailer

This commit is contained in:
2025-07-14 08:54:43 +02:00
parent 361beedb38
commit d5ae939f03
6 changed files with 395 additions and 2 deletions

22
.env.example Normal file
View File

@ -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

View File

@ -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

144
app.py
View File

@ -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"""
<html>
<body>
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> {form_data['firstName']} {form_data['lastName']}</p>
<p><strong>Email:</strong> {form_data['email']}</p>
<p><strong>Phone:</strong> {form_data.get('phone', 'Not provided')}</p>
<p><strong>Company:</strong> {form_data.get('company', 'Not provided')}</p>
<p><strong>Service Interested In:</strong> {form_data['service']}</p>
<p><strong>Budget:</strong> {form_data.get('budget', 'Not specified')}</p>
<p><strong>Message:</strong></p>
<p>{form_data['message'].replace(chr(10), '<br>')}</p>
</body>
</html>
"""
# 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')

View File

@ -6,3 +6,5 @@ jsmin==3.0.1
click==8.1.7
blinker==1.6.3
# Translation system - JSON-based, no compilation needed
# Email functionality
Flask-Mail==0.9.1

View File

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

View File

@ -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 = '<i class="fas fa-spinner fa-spin me-2"></i>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 = '<i class="fas fa-paper-plane me-2"></i>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 = `
<div class="notification-content">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} me-2"></i>
<span>${message}</span>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
`;
// 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);
}
}