Add form mailer
This commit is contained in:
22
.env.example
Normal file
22
.env.example
Normal 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
|
||||
55
README.md
55
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
|
||||
|
||||
144
app.py
144
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"""
|
||||
<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')
|
||||
|
||||
@ -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
|
||||
@ -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); }
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user