commit b5e25b9fc4f3314e8623609b298b69907fb50a5c Author: DESKTOP-G4D78VI\Oli Date: Sat Sep 27 01:32:49 2025 +0100 initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..7277a45 --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +# SkyView Drones - Professional Drone Services Website + +A modern, responsive website for your drone business specializing in aerial photography, video production, industrial inspections, and agricultural monitoring. + +## Features + +### 🚁 Complete Business Showcase +- **Hero Section** - Eye-catching introduction with call-to-action buttons +- **Services Overview** - Detailed sections for all your service offerings: + - Aerial Photography (Real estate, events, marketing) + - Video Production (4K recording, cinematic movements, editing) + - Industrial Inspections (Infrastructure, thermal imaging, safety assessments) + - Agricultural Monitoring (Crop analysis, livestock tracking, land surveying) + +### 📱 Professional Design +- **Responsive Design** - Looks great on desktop, tablet, and mobile devices +- **Modern UI** - Clean, professional design with smooth animations +- **Interactive Portfolio** - Filterable gallery to showcase your work +- **Professional Branding** - Consistent color scheme and typography + +### 📋 Lead Generation +- **Quote Request Form** - Comprehensive form to capture project details and client information +- **Contact Information** - Multiple ways for clients to reach you +- **Service-Specific Details** - Clear pricing expectations and service descriptions + +### ⚡ Technical Features +- **Fast Loading** - Optimized for speed and performance +- **SEO Ready** - Proper meta tags and semantic HTML structure +- **Form Validation** - Real-time validation for better user experience +- **Smooth Animations** - Professional scroll animations and transitions + +## Getting Started + +### 1. Customize Your Information + +Replace the placeholder information with your actual business details: + +**In `index.html`:** +- Update company name "SkyView Drones" to your business name +- Replace phone number `(555) 123-4567` with your actual number +- Update email `info@skyviewdrones.com` with your business email +- Modify service area from "Nationwide Coverage" to your actual coverage area +- Update the hero title and descriptions to match your brand voice + +**Key sections to customize:** +```html + + + + +

(Your Phone Number)

+

your-email@yourdomain.com

+

Your Service Area

+``` + +### 2. Add Your Content + +**Replace placeholder images:** +- Add your actual drone footage and photos +- Update the portfolio section with your real work +- Replace the about section image with your photo or team photo + +**Portfolio Images:** +- Replace the placeholder URLs in the portfolio section +- Add your actual project images +- Update project descriptions + +### 3. Customize Branding + +**Colors (in `styles.css`):** +- Primary blue: `#0066cc` - used for buttons and accents +- Secondary red: `#ff6b6b` - used for call-to-action buttons +- Background gradient: `linear-gradient(135deg, #667eea 0%, #764ba2 100%)` + +**Fonts:** +- Current: Inter (clean, professional) +- Can be changed by updating the Google Fonts import + +### 4. Set Up Form Handling + +The forms currently show success messages but need backend integration: + +**For Quote Form (`quoteForm`):** +- Collects: Name, email, phone, company, service type, location, date, budget, description +- Currently shows alert - replace with actual form submission + +**For Contact Form (`contactForm`):** +- Collects: Name, email, subject, message +- Currently shows alert - replace with actual form submission + +**Integration options:** +- **Netlify Forms** - Easy setup for static sites +- **Formspree** - Simple form handling service +- **EmailJS** - Send emails directly from JavaScript +- **Custom Backend** - PHP, Node.js, or other server-side solution + +Example EmailJS integration: +```javascript +// Replace the setTimeout in script.js with: +emailjs.send('YOUR_SERVICE_ID', 'YOUR_TEMPLATE_ID', data) + .then(() => { + alert('Thank you for your quote request!'); + this.reset(); + }); +``` + +## File Structure + +``` +drone-website/ +├── index.html # Main HTML file +├── styles.css # All CSS styles +├── script.js # JavaScript functionality +└── README.md # This file +``` + +## Customization Guide + +### Adding New Services +1. Add a new service card in the services section +2. Create corresponding portfolio filter button +3. Add portfolio items with the new category + +### Modifying Colors +Update the CSS custom properties or search and replace color values: +- Primary: `#0066cc` +- Secondary: `#ff6b6b` +- Background: `#f8f9fa` +- Text: `#333` + +### Adding More Pages +Create new HTML files and link them in the navigation: +```html +
  • Services
  • +``` + +### SEO Optimization +1. Update meta descriptions for each page +2. Add proper alt tags to all images +3. Use semantic HTML structure +4. Add schema markup for local business +5. Create sitemap.xml +6. Set up Google Analytics + +## Deployment Options + +### Static Hosting (Recommended) +- **Netlify** - Free tier with form handling +- **Vercel** - Free with great performance +- **GitHub Pages** - Free for public repositories +- **Firebase Hosting** - Google's hosting solution + +### Traditional Hosting +- Upload files to any web hosting provider +- Ensure HTTPS is enabled +- Set up form handling backend if needed + +## Performance Tips + +1. **Optimize Images** + - Compress photos before uploading + - Use WebP format when possible + - Add lazy loading for better performance + +2. **Speed Optimization** + - Minify CSS and JavaScript for production + - Use a CDN for faster global loading + - Enable gzip compression + +3. **SEO Best Practices** + - Add relevant keywords naturally + - Create descriptive page titles + - Use proper heading hierarchy (H1, H2, H3) + - Add local business schema markup + +## Browser Support + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) +- Mobile browsers (iOS Safari, Chrome Mobile) + +## License + +This template is free to use for your drone business. Customize it as needed for your brand and services. + +## Support + +For customization help or questions about implementing specific features, feel free to ask for assistance with: +- Form integration +- Additional functionality +- Design modifications +- SEO optimization +- Performance improvements + +--- + +**Ready to take your drone business online?** 🚁 + +This website template provides everything you need to establish a professional online presence and start capturing leads for your drone services business. diff --git a/a2.png b/a2.png new file mode 100644 index 0000000..b63fd18 Binary files /dev/null and b/a2.png differ diff --git a/aws-ses-setup.md b/aws-ses-setup.md new file mode 100644 index 0000000..212280f --- /dev/null +++ b/aws-ses-setup.md @@ -0,0 +1,160 @@ +# AWS SES Setup Instructions + +To enable email functionality for your PTSAerial contact forms using AWS SES, follow these steps: + +## 1. AWS SES Setup + +### Verify Email Addresses +1. Log into your AWS Console +2. Go to Amazon SES service +3. Navigate to "Verified identities" +4. Add and verify these email addresses: + - `oli@ptslondon.co.uk` (recipient) + - `noreply@ptslondon.co.uk` (sender - must be from your domain) + +### Domain Verification (Recommended) +1. In SES, go to "Verified identities" +2. Click "Create identity" > "Domain" +3. Enter `ptslondon.co.uk` +4. Follow DNS verification steps +5. This allows sending from any address @ptslondon.co.uk + +## 2. Create IAM User for Website + +### Create IAM Policy +1. Go to IAM service in AWS Console +2. Click "Policies" > "Create policy" +3. Use JSON editor and paste: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ses:SendEmail", + "ses:SendRawEmail" + ], + "Resource": "*" + } + ] +} +``` +4. Name it `PTSAerial-SES-Send-Policy` + +### Create IAM User +1. Go to "Users" > "Create user" +2. Username: `ptsaerial-website` +3. Attach the policy you just created +4. Create access keys for "Application running outside AWS" +5. **Save the Access Key ID and Secret Access Key securely** + +## 3. Update JavaScript Configuration + +In your `script.js` file, update these values: + +```javascript +const awsConfig = { + region: 'eu-west-1', // Your SES region (e.g., us-east-1, eu-west-1, us-west-2) + accessKeyId: 'YOUR_ACCESS_KEY_ID', // From IAM user + secretAccessKey: 'YOUR_SECRET_ACCESS_KEY', // From IAM user +}; +``` + +And update the sender email: +```javascript +Source: 'noreply@ptslondon.co.uk', // Must be verified in SES +``` + +## 4. SES Sandbox vs Production + +### If in Sandbox Mode (default): +- Can only send to verified email addresses +- Limited to 200 emails per day +- Maximum 1 email per second + +### To Move to Production: +1. In SES console, click "Request production access" +2. Fill out the form explaining your use case +3. AWS will review (usually approved within 24 hours) +4. Production allows sending to any email address + +## 5. Security Considerations + +### For Production Use (More Secure): +Instead of putting credentials in the frontend, consider: + +1. **API Gateway + Lambda** (Recommended): + - Create a Lambda function to send emails + - Use API Gateway to expose an endpoint + - Call the API from your website + - Credentials stay server-side + +2. **Cognito Identity Pools**: + - Use temporary credentials + - More secure than permanent access keys + +### Basic Frontend Implementation: +For testing/simple use, the current implementation works but: +- Access keys are visible in the browser +- Only use this for low-security scenarios +- Consider IP restrictions in IAM policy + +## 6. Test Configuration + +1. Update the configuration values in `script.js` +2. Ensure SES is out of sandbox mode OR oli@ptslondon.co.uk is verified +3. Test both contact forms +4. Check oli@ptslondon.co.uk for emails +5. Monitor AWS SES console for send statistics + +## 7. Monitoring & Troubleshooting + +### SES Console Monitoring: +- Check "Sending statistics" for delivery rates +- Review "Suppression list" for bounced emails +- Monitor "Reputation metrics" + +### Common Issues: +- **Invalid sender**: Ensure sender email is verified +- **Access denied**: Check IAM permissions +- **Sandbox restrictions**: Verify recipient or request production access +- **Region mismatch**: Ensure correct region in config + +### Browser Console Errors: +Check for: +- AWS SDK loading errors +- CORS issues (not applicable for SES) +- Network connectivity +- Invalid credentials + +## 8. Email Content Details + +### Quote Request Emails Include: +- Customer name, email, phone, company +- Service type and location +- Preferred date and budget range +- Detailed project description +- HTML formatted table for easy reading + +### Contact Form Emails Include: +- Customer name, email, subject +- Message content +- Professional HTML formatting + +## 9. Cost Information + +AWS SES Pricing (as of 2025): +- First 62,000 emails per month: $0.10 per 1,000 emails +- Additional emails: $0.10 per 1,000 emails +- No monthly fees +- Very cost-effective for business use + +## 10. Regional Recommendations + +Choose your region based on: +- **eu-west-1** (Ireland): Good for UK business +- **us-east-1** (N. Virginia): Lowest cost, highest feature availability +- **eu-west-2** (London): UK data residency if required + +Your current configuration uses `eu-west-1` which is ideal for UK operations. diff --git a/drone-op.jpg b/drone-op.jpg new file mode 100644 index 0000000..4d8cc3f Binary files /dev/null and b/drone-op.jpg differ diff --git a/drone.png b/drone.png new file mode 100644 index 0000000..79423ad Binary files /dev/null and b/drone.png differ diff --git a/emailjs-setup-guide.md b/emailjs-setup-guide.md new file mode 100644 index 0000000..6a52601 --- /dev/null +++ b/emailjs-setup-guide.md @@ -0,0 +1,199 @@ +# EmailJS Setup Guide for PTSAerial + +I've switched your website back to EmailJS, which is much easier and more reliable than AWS SES for frontend forms. Here's how to set it up: + +## 1. Create EmailJS Account + +1. Go to [EmailJS.com](https://www.emailjs.com/) +2. Sign up for a free account (200 emails/month) +3. Verify your email address + +## 2. Add Email Service + +1. In your EmailJS dashboard, click "Email Services" +2. Click "Add New Service" +3. Choose "Gmail" (recommended) or your email provider +4. For Gmail: + - Enter your Gmail email (could be oli@ptslondon.co.uk if it's Gmail) + - Or create a dedicated Gmail account for website emails +5. Follow the OAuth setup process +6. **Copy your Service ID** (you'll need this) + +## 3. Create Email Templates + +### Template 1: Quote Request Template +1. Go to "Email Templates" → "Create New Template" +2. **Template ID**: `quote_template` +3. **Subject**: `New Quote Request - PTSAerial - {{service_type}}` +4. **Content**: +``` +New Quote Request from PTSAerial Website + +Customer Details: +Name: {{customer_name}} +Email: {{customer_email}} +Phone: {{customer_phone}} +Company: {{customer_company}} + +Project Details: +Service Type: {{service_type}} +Location: {{project_location}} +Preferred Date: {{preferred_date}} +Budget Range: {{budget_range}} + +Project Description: +{{project_description}} + +--- +Reply to this email to respond directly to the customer. +Website: PTSAerial.co.uk +``` + +### Template 2: Contact Form Template +1. Create another template +2. **Template ID**: `contact_template` +3. **Subject**: `New Contact Message - PTSAerial - {{message_subject}}` +4. **Content**: +``` +New Contact Message from PTSAerial Website + +From: {{customer_name}} +Email: {{customer_email}} +Subject: {{message_subject}} + +Message: +{{message_content}} + +--- +Reply to this email to respond directly to the customer. +Website: PTSAerial.co.uk +``` + +## 4. Get Your Configuration + +1. In EmailJS dashboard, go to "Account" +2. Copy your **Public Key** (also called User ID) +3. You'll also need your **Service ID** from step 2 + +## 5. Update Your Website + +In your `script.js` file, replace these values: + +```javascript +// Line ~69: Replace YOUR_PUBLIC_KEY +emailjs.init("YOUR_PUBLIC_KEY"); // Replace with your actual public key + +// Line ~83 and ~127: Replace YOUR_SERVICE_ID +sendEmail('quote_template', templateParams) +// Should become: +emailjs.send('YOUR_SERVICE_ID', 'quote_template', templateParams) +``` + +**Example:** +```javascript +emailjs.init("user_abc123def456"); // Your public key +// and +return emailjs.send('service_gmail_xyz789', templateId, templateParams) +``` + +## 6. Test Your Setup + +1. Save your script.js file with the correct keys +2. Open your website +3. Fill out both forms (quote and contact) +4. Check oli@ptslondon.co.uk for emails +5. Check browser console (F12) for any errors + +## 7. EmailJS Dashboard Monitoring + +After testing, check your EmailJS dashboard: +- "History" tab shows sent emails +- Monitor your monthly usage +- Check for any failed sends + +## Configuration Summary + +**What you need to replace in script.js:** + +```javascript +// Replace this line: +emailjs.init("YOUR_PUBLIC_KEY"); +// With: +emailjs.init("your_actual_public_key"); + +// And replace this line: +return emailjs.send('YOUR_SERVICE_ID', templateId, templateParams) +// With: +return emailjs.send('your_actual_service_id', templateId, templateParams) +``` + +## Email Template Variables + +The website sends these variables to your templates: + +### Quote Form (`quote_template`): +- `{{customer_name}}` - Customer's name +- `{{customer_email}}` - Customer's email +- `{{customer_phone}}` - Phone number +- `{{customer_company}}` - Company name +- `{{service_type}}` - Selected service +- `{{project_location}}` - Project location +- `{{preferred_date}}` - Preferred date +- `{{budget_range}}` - Budget range +- `{{project_description}}` - Project description + +### Contact Form (`contact_template`): +- `{{customer_name}}` - Customer's name +- `{{customer_email}}` - Customer's email +- `{{message_subject}}` - Message subject +- `{{message_content}}` - Message content + +## Troubleshooting + +### Common Issues: + +1. **"EmailJS not loaded"** - Check the script tag in index.html +2. **"Template not found"** - Ensure template IDs match exactly +3. **"Service not found"** - Check your Service ID is correct +4. **Emails not arriving** - Check spam folder first + +### Testing in Console: + +Open browser console (F12) and run: +```javascript +emailjs.send('your_service_id', 'quote_template', { + customer_name: 'Test User', + customer_email: 'test@example.com', + service_type: 'Aerial Photography' +}); +``` + +## Benefits of EmailJS vs AWS SES + +✅ **Easier Setup**: No AWS account or IAM configuration needed +✅ **Frontend Friendly**: Designed for browser use +✅ **No CORS Issues**: Works directly from any website +✅ **Free Tier**: 200 emails/month is usually sufficient +✅ **Template Management**: Easy visual template editor +✅ **Reliable Delivery**: Built-in email service integration + +## Free Tier Limits + +- **200 emails per month** +- **Basic templates** +- **Email history tracking** + +For higher volume, upgrade to paid plans starting at $15/month. + +--- + +**Quick Setup Checklist:** +- [ ] Create EmailJS account +- [ ] Add Gmail service and get Service ID +- [ ] Create quote_template and contact_template +- [ ] Get Public Key from Account settings +- [ ] Update script.js with your keys +- [ ] Test both forms +- [ ] Check oli@ptslondon.co.uk for emails + +The setup should take about 15 minutes and is much more reliable than AWS SES for website forms! diff --git a/emailjs-setup.md b/emailjs-setup.md new file mode 100644 index 0000000..e69de29 diff --git a/hero-video.mp4 b/hero-video.mp4 new file mode 100644 index 0000000..37b454c Binary files /dev/null and b/hero-video.mp4 differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..46c9391 --- /dev/null +++ b/index.html @@ -0,0 +1,417 @@ + + + + + + PTSAerial - Professional Aerial Photography & Industrial Solutions | Dartford, Kent + + + + + + + +
    + +
    + + +
    +
    +

    Professional Drone Services

    +

    Capturing stunning aerial photography, providing industrial inspections, and agricultural monitoring with cutting-edge drone technology

    + +
    +
    + +
    +
    + + +
    +
    +

    Our Services

    +

    Professional drone solutions for every need

    + +
    +
    +
    + +
    +

    Aerial Photography

    +

    Stunning high-resolution aerial photographs for real estate, events, marketing, and personal projects. Capture unique perspectives that showcase your property or event from above.

    +
      +
    • 4K High-Resolution Images
    • +
    • Real Estate Photography
    • +
    • Event Coverage
    • +
    • Marketing Content
    • +
    +
    + +
    +
    + +
    +

    Industrial

    +

    Safe and efficient inspections of infrastructure, buildings, towers, and industrial facilities. Reduce costs and safety risks with our advanced drone technology.

    +
      +
    • Infrastructure Inspections
    • +
    • Thermal Imaging
    • +
    • Power Line Inspections
    • +
    • Detailed Reports
    • +
    +
    + +
    +
    + +
    +

    Agricultural

    +

    Precision agriculture solutions including crop monitoring, livestock tracking, and land surveying. Optimize your farming operations with actionable aerial data.

    +
      +
    • Crop Health Analysis
    • +
    • Livestock Monitoring
    • +
    • Land Surveying
    • +
    • Irrigation Planning
    • +
    +
    + +
    +
    + +
    +

    Construction

    +

    Advanced solutions for construction site monitoring, including progress tracking, safety inspections, and site surveys. Ensure your project stays on schedule and within budget.

    +
      +
    • Site Progress Tracking
    • +
    • Safety Inspections
    • +
    • 3D Mapping
    • +
    • Site Surveys
    • +
    +
    + +
    +
    + +
    +

    Architectural

    +

    High-quality aerial imagery and 3D modeling for architectural projects. Showcase your designs with stunning visuals and accurate site assessments.

    +
      +
    • 3D Modeling
    • +
    • Aerial Imagery
    • +
    • Site Assessments
    • +
    • Design Visualization
    • +
    +
    + +
    +
    + +
    +

    Security Monitoring

    +

    Professional security surveillance and monitoring services for commercial properties, events, and industrial sites. Real-time monitoring with advanced thermal imaging capabilities.

    +
      +
    • Perimeter Surveillance
    • +
    • Thermal Imaging
    • +
    • Real-time Monitoring
    • +
    • Incident Documentation
    • +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +

    About PTSAerial

    +

    PTS London trading as PTSAerial provides professional, reliable, and innovative drone services across Kent and the UK. Based in Dartford, our CAA-licensed team is fully insured and equipped with the latest drone technology to deliver exceptional results for every project.

    + +
    + +
    +

    100%

    +

    Client Satisfaction

    +
    +
    +

    24/7

    +

    Support Available

    +
    +
    + +
    +

    Certifications & Insurance

    +
      +
    • CAA Licensed (UK)
    • +
    • Fully Insured Operations
    • +
    • Professional Equipment
    • +
    • Safety First Approach
    • +
    +
    +
    +
    + Professional Drone Operator +
    +
    +
    +
    + + +
    +
    +

    Get Your Free Quote

    +

    Tell us about your project and we'll provide a detailed quote within 24 hours

    + +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + +
    +
    + + +
    +
    + +
    + + +
    + + +
    +
    +
    + + +
    +
    +

    Contact Us

    +
    +
    +
    + +
    +

    Phone

    +

    +44 (0)20 3883 7578

    +
    +
    +
    + +
    +

    Email

    +

    oli@ptslondon.co.uk

    +
    +
    +
    + +
    +

    Location

    +

    Dartford, Kent, UK

    +
    +
    +
    + +
    +

    Send us a message

    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..3846e0b --- /dev/null +++ b/script.js @@ -0,0 +1,451 @@ +// Mobile Navigation Toggle +document.addEventListener('DOMContentLoaded', function() { + const hamburger = document.querySelector('.hamburger'); + const navMenu = document.querySelector('.nav-menu'); + + hamburger.addEventListener('click', function() { + hamburger.classList.toggle('active'); + navMenu.classList.toggle('active'); + }); + + // Close mobile menu when clicking on a link + document.querySelectorAll('.nav-link').forEach(n => n.addEventListener('click', () => { + hamburger.classList.remove('active'); + navMenu.classList.remove('active'); + })); +}); + +// Smooth Scrolling for Navigation Links +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); +}); + +// Portfolio Filter Functionality +document.addEventListener('DOMContentLoaded', function() { + const filterButtons = document.querySelectorAll('.filter-btn'); + const portfolioItems = document.querySelectorAll('.portfolio-item'); + + filterButtons.forEach(button => { + button.addEventListener('click', function() { + // Remove active class from all buttons + filterButtons.forEach(btn => btn.classList.remove('active')); + // Add active class to clicked button + this.classList.add('active'); + + const filterValue = this.getAttribute('data-filter'); + + portfolioItems.forEach(item => { + if (filterValue === 'all' || item.getAttribute('data-category') === filterValue) { + item.style.display = 'block'; + setTimeout(() => { + item.style.opacity = '1'; + item.style.transform = 'scale(1)'; + }, 100); + } else { + item.style.opacity = '0'; + item.style.transform = 'scale(0.8)'; + setTimeout(() => { + item.style.display = 'none'; + }, 300); + } + }); + }); + }); +}); + +// Form Handling with EmailJS +document.addEventListener('DOMContentLoaded', function() { + // Check if EmailJS is loaded + if (typeof emailjs === 'undefined') { + console.error('EmailJS not loaded. Please check the script tag.'); + return; + } + + // Initialize EmailJS + emailjs.init("QznwPi5vZti7xnjRj"); // Replace with your EmailJS public key + + console.log('EmailJS initialized successfully'); + + const quoteForm = document.getElementById('quoteForm'); + const contactForm = document.getElementById('contactForm'); + + // Function to send email via EmailJS + function sendEmail(templateId, templateParams) { + console.log('Attempting to send email with EmailJS:', templateId); + console.log('Template parameters:', templateParams); + + return emailjs.send('service_c51669d', templateId, templateParams) + .then(function(response) { + console.log('EmailJS Success:', response.status, response.text); + return response; + }) + .catch(function(error) { + console.error('EmailJS Error:', error); + throw error; + }); + } + + // Quote Form Submission + if (quoteForm) { + quoteForm.addEventListener('submit', function(e) { + e.preventDefault(); + + // Get form data + const formData = new FormData(this); + const data = {}; + for (let [key, value] of formData.entries()) { + data[key] = value; + } + + // Show loading state + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + submitBtn.textContent = 'Submitting...'; + submitBtn.disabled = true; + + // Prepare EmailJS template parameters + const templateParams = { + to_email: 'oli@ptslondon.co.uk', + from_name: data.name, + reply_to: data.email, + customer_name: data.name, + customer_email: data.email, + customer_phone: data.phone || 'Not provided', + customer_company: data.company || 'Not provided', + service_type: data.service, + project_location: data.location, + preferred_date: data.date || 'Not specified', + budget_range: data.budget || 'Not specified', + project_description: data.description, + form_type: 'Quote Request' + }; + + // Send email using EmailJS + sendEmail('quote_template', templateParams) + .then((result) => { + console.log('Quote email sent successfully:', result); + showNotification('Thank you for your quote request! We will contact you within 24 hours.', 'success'); + this.reset(); + }) + .catch((error) => { + console.error('EmailJS error:', error); + let errorMessage = 'There was an error sending your quote request. '; + + if (error.text && error.text.includes('template')) { + errorMessage += 'Email template not found. '; + } else if (error.text && error.text.includes('service')) { + errorMessage += 'Email service configuration error. '; + } + + errorMessage += 'Please contact us directly at oli@ptslondon.co.uk.'; + showNotification(errorMessage, 'error'); + }) + .finally(() => { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + }); + }); + } + + // Contact Form Submission + if (contactForm) { + contactForm.addEventListener('submit', function(e) { + e.preventDefault(); + + // Get form data + const formData = new FormData(this); + const data = {}; + for (let [key, value] of formData.entries()) { + data[key] = value; + } + + // Show loading state + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + submitBtn.textContent = 'Sending...'; + submitBtn.disabled = true; + + // Prepare EmailJS template parameters + const templateParams = { + to_email: 'oli@ptslondon.co.uk', + from_name: data.contact_name, + reply_to: data.contact_email, + customer_name: data.contact_name, + customer_email: data.contact_email, + message_subject: data.subject, + message_content: data.message, + form_type: 'Contact Message' + }; + + // Send email using EmailJS + sendEmail('contact_template', templateParams) + .then((result) => { + console.log('Contact email sent successfully:', result); + showNotification('Thank you for your message! We will get back to you soon.', 'success'); + this.reset(); + }) + .catch((error) => { + console.error('Contact EmailJS error:', error); + let errorMessage = 'There was an error sending your message. '; + + if (error.text && error.text.includes('template')) { + errorMessage += 'Email template not found. '; + } else if (error.text && error.text.includes('service')) { + errorMessage += 'Email service configuration error. '; + } + + errorMessage += 'Please contact us directly at oli@ptslondon.co.uk.'; + showNotification(errorMessage, 'error'); + }) + .finally(() => { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + }); + }); + } +}); + +// Navbar Background on Scroll +window.addEventListener('scroll', function() { + const header = document.querySelector('.header'); + if (window.scrollY > 100) { + header.style.background = 'rgba(255, 255, 255, 0.98)'; + header.style.boxShadow = '0 2px 20px rgba(0, 0, 0, 0.1)'; + } else { + header.style.background = 'rgba(255, 255, 255, 0.95)'; + header.style.boxShadow = 'none'; + } +}); + +// Intersection Observer for Animations +const observerOptions = { + threshold: 0.1, + rootMargin: '0px 0px -50px 0px' +}; + +const observer = new IntersectionObserver(function(entries) { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + } + }); +}, observerOptions); + +// Observe elements for animation +document.addEventListener('DOMContentLoaded', function() { + const animateElements = document.querySelectorAll('.service-card, .portfolio-item, .about-content, .contact-item'); + animateElements.forEach(el => { + el.classList.add('fade-in'); + observer.observe(el); + }); +}); + +// Form Validation +function validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); +} + +function validatePhone(phone) { + const re = /^[\+]?[1-9][\d]{0,15}$/; + return re.test(phone.replace(/\s/g, '')); +} + +// Real-time form validation +document.addEventListener('DOMContentLoaded', function() { + const emailInputs = document.querySelectorAll('input[type="email"]'); + const phoneInputs = document.querySelectorAll('input[type="tel"]'); + + emailInputs.forEach(input => { + input.addEventListener('blur', function() { + if (this.value && !validateEmail(this.value)) { + this.style.borderColor = '#ff6b6b'; + this.style.boxShadow = '0 0 0 3px rgba(255, 107, 107, 0.1)'; + } else { + this.style.borderColor = '#ddd'; + this.style.boxShadow = 'none'; + } + }); + }); + + phoneInputs.forEach(input => { + input.addEventListener('blur', function() { + if (this.value && !validatePhone(this.value)) { + this.style.borderColor = '#ff6b6b'; + this.style.boxShadow = '0 0 0 3px rgba(255, 107, 107, 0.1)'; + } else { + this.style.borderColor = '#ddd'; + this.style.boxShadow = 'none'; + } + }); + }); +}); + +// Set minimum date for date inputs to today +document.addEventListener('DOMContentLoaded', function() { + const dateInput = document.getElementById('date'); + if (dateInput) { + const today = new Date().toISOString().split('T')[0]; + dateInput.setAttribute('min', today); + } +}); + +// Lazy loading for images +document.addEventListener('DOMContentLoaded', function() { + const images = document.querySelectorAll('img[data-src]'); + + const imageObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const img = entry.target; + img.src = img.dataset.src; + img.removeAttribute('data-src'); + imageObserver.unobserve(img); + } + }); + }); + + images.forEach(img => imageObserver.observe(img)); +}); + +// Add loading states for buttons +function addLoadingState(button, loadingText = 'Loading...') { + const originalText = button.textContent; + button.textContent = loadingText; + button.disabled = true; + button.classList.add('loading'); + + return function removeLoadingState() { + button.textContent = originalText; + button.disabled = false; + button.classList.remove('loading'); + }; +} + +// Enhanced form submission with better error handling +function submitForm(form, endpoint) { + const submitButton = form.querySelector('button[type="submit"]'); + const removeLoading = addLoadingState(submitButton); + + const formData = new FormData(form); + + // Convert FormData to regular object + const data = {}; + for (let [key, value] of formData.entries()) { + data[key] = value; + } + + // Simulate API call (replace with actual endpoint) + return new Promise((resolve, reject) => { + setTimeout(() => { + // Simulate success/failure + if (Math.random() > 0.1) { // 90% success rate + resolve(data); + } else { + reject(new Error('Network error')); + } + }, 2000); + }) + .then(result => { + removeLoading(); + showNotification('Form submitted successfully!', 'success'); + form.reset(); + return result; + }) + .catch(error => { + removeLoading(); + showNotification('There was an error submitting the form. Please try again.', 'error'); + throw error; + }); +} + +// Notification system +function showNotification(message, type = 'info') { + // Remove existing notifications + const existingNotifications = document.querySelectorAll('.notification'); + existingNotifications.forEach(notification => notification.remove()); + + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.innerHTML = ` + ${message} + + `; + + // Add styles + Object.assign(notification.style, { + position: 'fixed', + top: '20px', + right: '20px', + padding: '1rem 1.5rem', + borderRadius: '5px', + color: 'white', + fontWeight: '500', + zIndex: '10000', + animation: 'slideInRight 0.3s ease', + display: 'flex', + alignItems: 'center', + gap: '1rem', + maxWidth: '300px' + }); + + // Set background color based on type + const colors = { + success: '#28a745', + error: '#dc3545', + warning: '#ffc107', + info: '#17a2b8' + }; + notification.style.backgroundColor = colors[type] || colors.info; + + // Add close functionality + const closeBtn = notification.querySelector('.notification-close'); + closeBtn.style.background = 'none'; + closeBtn.style.border = 'none'; + closeBtn.style.color = 'white'; + closeBtn.style.fontSize = '1.5rem'; + closeBtn.style.cursor = 'pointer'; + closeBtn.style.padding = '0'; + closeBtn.style.marginLeft = 'auto'; + + closeBtn.addEventListener('click', () => notification.remove()); + + document.body.appendChild(notification); + + // Auto remove after 5 seconds + setTimeout(() => { + if (document.body.contains(notification)) { + notification.remove(); + } + }, 5000); +} + +// Add CSS for notification animation +const style = document.createElement('style'); +style.textContent = ` + @keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + .loading { + opacity: 0.7; + cursor: not-allowed; + } +`; +document.head.appendChild(style); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..11c6d71 --- /dev/null +++ b/styles.css @@ -0,0 +1,798 @@ +/* Reset and Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + line-height: 1.6; + color: #333; + scroll-behavior: smooth; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* Header and Navigation */ +.header { + position: fixed; + top: 0; + width: 100%; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + z-index: 1000; + transition: all 0.3s ease; +} + +.nav-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.nav-logo { + display: flex; + align-items: center; + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); + text-decoration: none; +} + +.nav-logo .logo-icon { + width: 32px; + height: 32px; + margin-right: 0.5rem; + object-fit: contain; +} + +.nav-logo span { + color: var(--text-color); +} + +.nav-menu { + display: flex; + list-style: none; + gap: 2rem; + align-items: center; +} + +.nav-link { + text-decoration: none; + color: #333; + font-weight: 500; + transition: color 0.3s ease; + position: relative; +} + +.nav-link:hover { + color: #0066cc; +} + +.nav-link:after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + width: 0; + height: 2px; + background: #0066cc; + transition: width 0.3s ease; +} + +.nav-link:hover:after { + width: 100%; +} + +.nav-cta { + background: #0066cc; + color: white !important; + padding: 0.5rem 1.5rem; + border-radius: 25px; + transition: all 0.3s ease; +} + +.nav-cta:hover { + background: #0052a3; + transform: translateY(-2px); +} + +.nav-cta:after { + display: none; +} + +.hamburger { + display: none; + flex-direction: column; + cursor: pointer; +} + +.hamburger span { + width: 25px; + height: 3px; + background: #333; + margin: 3px 0; + transition: 0.3s; +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + position: relative; + background: linear-gradient(135deg, #1b6799 0%, #15065a 70%); + color: white; + text-align: center; + overflow: hidden; +} + +.hero-content { + z-index: 2; + max-width: 800px; + padding: 2rem; +} + +.hero-title { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + animation: fadeInUp 1s ease; +} + +.hero-subtitle { + font-size: 1.25rem; + margin-bottom: 2.5rem; + opacity: 0.9; + animation: fadeInUp 1s ease 0.2s both; +} + +.hero-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + animation: fadeInUp 1s ease 0.4s both; +} + +.btn { + display: inline-block; + padding: 1rem 2rem; + border-radius: 5px; + text-decoration: none; + font-weight: 600; + transition: all 0.3s ease; + border: none; + cursor: pointer; + font-size: 1rem; +} + +.btn-primary { + background: #ff6b6b; + color: white; +} + +.btn-primary:hover { + background: #ff5252; + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(255, 107, 107, 0.3); +} + +.btn-secondary { + background: transparent; + color: white; + border: 2px solid white; +} + +.btn-secondary:hover { + background: white; + color: #667eea; + transform: translateY(-2px); +} + +.btn-large { + padding: 1.25rem 2.5rem; + font-size: 1.1rem; +} + +.hero-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + opacity: 0.4; +} + +.hero-video video { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Sections */ +.section-title { + font-size: 2.5rem; + font-weight: 700; + text-align: center; + margin-bottom: 1rem; + color: #333; +} + +.section-subtitle { + font-size: 1.2rem; + text-align: center; + margin-bottom: 3rem; + color: #666; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +/* Services Section */ +.services { + padding: 5rem 0; + background: #f8f9fa; +} + +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 3rem; +} + +.service-card { + background: white; + padding: 2.5rem; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + text-align: center; +} + +.service-card:hover { + transform: translateY(-10px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +.service-icon { + width: 80px; + height: 80px; + background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.5rem; + color: white; + font-size: 2rem; +} + +.service-card h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: #333; +} + +.service-card p { + color: #666; + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.service-features { + list-style: none; + text-align: left; +} + +.service-features li { + padding: 0.5rem 0; + color: #555; + position: relative; + padding-left: 1.5rem; +} + +.service-features li:before { + content: '✓'; + position: absolute; + left: 0; + color: #0066cc; + font-weight: bold; +} + +/* Portfolio Section */ +.portfolio { + padding: 5rem 0; + background: white; +} + +.portfolio-filter { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 3rem; + flex-wrap: wrap; +} + +.filter-btn { + padding: 0.75rem 1.5rem; + border: 2px solid #0066cc; + background: transparent; + color: #0066cc; + border-radius: 25px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 500; +} + +.filter-btn.active, +.filter-btn:hover { + background: #0066cc; + color: white; +} + +.portfolio-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.portfolio-item { + position: relative; + border-radius: 10px; + overflow: hidden; + cursor: pointer; + transition: transform 0.3s ease; +} + +.portfolio-item:hover { + transform: scale(1.05); +} + +.portfolio-item img { + width: 100%; + height: 250px; + object-fit: cover; + transition: all 0.3s ease; +} + +.portfolio-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + color: white; + padding: 2rem; + transform: translateY(100%); + transition: all 0.3s ease; +} + +.portfolio-item:hover .portfolio-overlay { + transform: translateY(0); +} + +.portfolio-overlay h4 { + font-size: 1.25rem; + margin-bottom: 0.5rem; +} + +/* About Section */ +.about { + padding: 5rem 0; + background: #f8f9fa; +} + +.about-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; +} + +.about-text h2 { + font-size: 2.5rem; + margin-bottom: 1.5rem; + color: #333; +} + +.about-text p { + font-size: 1.1rem; + color: #666; + margin-bottom: 2rem; + line-height: 1.7; +} + +.about-stats { + display: flex; + gap: 2rem; + margin-bottom: 2rem; +} + +.stat { + text-align: center; +} + +.stat h3 { + font-size: 2rem; + color: #0066cc; + font-weight: 700; +} + +.stat p { + color: #666; + margin: 0; +} + +.certifications h4 { + margin-bottom: 1rem; + color: #333; +} + +.certifications ul { + list-style: none; +} + +.certifications li { + padding: 0.5rem 0; + color: #666; +} + +.certifications i { + color: #28a745; + margin-right: 0.5rem; +} + +.about-image img { + width: 100%; + border-radius: 10px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +/* Quote Section */ +.quote-section { + padding: 5rem 0; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.quote-section .section-title, +.quote-section .section-subtitle { + color: white; +} + +.quote-form { + max-width: 800px; + margin: 0 auto; + background: rgba(255, 255, 255, 0.1); + padding: 3rem; + border-radius: 15px; + backdrop-filter: blur(10px); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 1rem; + border: none; + border-radius: 5px; + background: rgba(255, 255, 255, 0.9); + font-size: 1rem; + transition: all 0.3s ease; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + background: white; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3); +} + +.form-group textarea { + resize: vertical; + min-height: 120px; +} + +/* Contact Section */ +.contact { + padding: 5rem 0; + background: white; +} + +.contact-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; +} + +.contact-info { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.contact-item { + display: flex; + align-items: flex-start; + gap: 1rem; +} + +.contact-item i { + font-size: 1.5rem; + color: #0066cc; + margin-top: 0.25rem; +} + +.contact-item h4 { + margin-bottom: 0.5rem; + color: #333; +} + +.contact-item p { + color: #666; + margin: 0; +} + +.contact-form { + background: #f8f9fa; + padding: 2rem; + border-radius: 10px; +} + +.contact-form h3 { + margin-bottom: 1.5rem; + color: #333; +} + +.contact-form .form-group input, +.contact-form .form-group textarea { + background: white; + border: 1px solid #ddd; + color: #333; +} + +/* Footer */ +.footer { + background: #2c3e50; + color: white; + padding: 3rem 0 1rem; +} + +.footer-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; +} + +.footer-logo { + display: flex; + align-items: center; + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1rem; +} + +.footer-logo .logo-icon { + width: 32px; + height: 32px; + margin-right: 0.5rem; + object-fit: contain; + filter: brightness(0) invert(1); /* Makes the icon white for footer */ +} + +.footer-logo span { + color: white; +} + +.footer-section h4 { + margin-bottom: 1rem; + color: #0066cc; +} + +.footer-section ul { + list-style: none; +} + +.footer-section ul li { + margin-bottom: 0.5rem; +} + +.footer-section ul li a { + color: #bbb; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-section ul li a:hover { + color: white; +} + +.footer-section p { + color: #bbb; + margin-bottom: 0.5rem; +} + +.social-links { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.social-links a { + width: 40px; + height: 40px; + background: #0066cc; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + text-decoration: none; + transition: all 0.3s ease; +} + +.social-links a:hover { + background: #0052a3; + transform: translateY(-2px); +} + +.footer-bottom { + border-top: 1px solid #444; + padding-top: 1rem; + text-align: center; + color: #bbb; +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hamburger { + display: flex; + } + + .nav-menu { + position: fixed; + left: -100%; + top: 70px; + flex-direction: column; + background-color: white; + width: 100%; + text-align: center; + transition: 0.3s; + box-shadow: 0 10px 27px rgba(0, 0, 0, 0.05); + padding: 2rem 0; + } + + .nav-menu.active { + left: 0; + } + + .hero-title { + font-size: 2.5rem; + } + + .hero-subtitle { + font-size: 1.1rem; + } + + .hero-buttons { + flex-direction: column; + align-items: center; + } + + .services-grid { + grid-template-columns: 1fr; + } + + .portfolio-grid { + grid-template-columns: 1fr; + } + + .about-content { + grid-template-columns: 1fr; + text-align: center; + } + + .about-stats { + justify-content: center; + } + + .contact-content { + grid-template-columns: 1fr; + } + + .form-row { + grid-template-columns: 1fr; + } + + .section-title { + font-size: 2rem; + } + + .nav-container { + padding: 1rem; + } +} + +@media (max-width: 480px) { + .hero-title { + font-size: 2rem; + } + + .service-card, + .quote-form { + padding: 1.5rem; + } + + .about-stats { + flex-direction: column; + text-align: center; + } + + .portfolio-filter { + gap: 0.5rem; + } + + .filter-btn { + padding: 0.5rem 1rem; + font-size: 0.9rem; + } +} + +/* Utility Classes */ +.hidden { + display: none; +} + +.fade-in { + opacity: 0; + transform: translateY(20px); + transition: all 0.6s ease; +} + +.fade-in.visible { + opacity: 1; + transform: translateY(0); +} diff --git a/troubleshooting.md b/troubleshooting.md new file mode 100644 index 0000000..e440616 --- /dev/null +++ b/troubleshooting.md @@ -0,0 +1,164 @@ +# AWS SES Troubleshooting Guide + +Your website now has enhanced error handling and logging. Here's how to troubleshoot email sending issues: + +## 1. Check Browser Console + +Open your website and: +1. Press F12 to open Developer Tools +2. Go to the "Console" tab +3. Submit a test form +4. Look for error messages + +### Common Console Messages: + +**Success:** +``` +AWS SES configured successfully +Attempting to send email with subject: New Quote Request - PTSAerial - [service] +Email sent successfully: {MessageId: "..."} +``` + +**Common Errors:** +``` +AWS SDK not loaded. Please check the script tag. +Error configuring AWS: [error details] +SES Error: MessageRejected +SES Error: AccessDenied +SES Error: InvalidParameterValue +``` + +## 2. Common Issues & Solutions + +### Issue: "MessageRejected" Error +**Cause:** Email addresses not verified in SES +**Solution:** +1. Go to AWS SES Console +2. Navigate to "Verified identities" +3. Verify both `oli@ptslondon.co.uk` and `noreply@ptslondon.co.uk` +4. Check your email for verification links + +### Issue: "AccessDenied" Error +**Cause:** IAM user lacks SES permissions +**Solution:** +1. Go to AWS IAM Console +2. Check your user has the SES policy attached +3. Ensure policy includes `ses:SendEmail` and `ses:SendRawEmail` + +### Issue: "Sandbox" Restrictions +**Cause:** SES is in sandbox mode +**Solution:** +1. In SES Console, request production access +2. Or verify all recipient email addresses +3. Sandbox only allows verified recipients + +### Issue: "Region" Mismatch +**Cause:** Wrong AWS region in config +**Solution:** +1. Check which region your SES is set up in +2. Update `region: 'eu-west-1'` in script.js to match + +### Issue: "CORS" Errors +**Cause:** Browser security restrictions +**Solution:** +- This shouldn't occur with SES, but if it does, consider using a backend API instead + +## 3. Verify SES Setup + +### Check in AWS Console: + +1. **SES Dashboard** → "Sending statistics" + - Should show attempted sends + - Check for bounce/complaint rates + +2. **Verified identities** + - `oli@ptslondon.co.uk` should show "Verified" + - `noreply@ptslondon.co.uk` should show "Verified" + +3. **Configuration sets** (optional) + - Can help with tracking delivery + +## 4. Test with Minimal Example + +Add this to your browser console to test SES directly: + +```javascript +// Test AWS SES configuration +AWS.config.update({ + region: 'eu-west-1', + accessKeyId: 'AKIA3C5Y7TIMPQ7PHLEM', + secretAccessKey: 'BPCPlS2X77KwgvFweOO3IebeT9U5gBNnZ5/7ZRX2WEtO' +}); + +const ses = new AWS.SES(); +const params = { + Destination: { ToAddresses: ['oli@ptslondon.co.uk'] }, + Message: { + Body: { Text: { Data: 'Test email from website' } }, + Subject: { Data: 'Test Email' } + }, + Source: 'noreply@ptslondon.co.uk' +}; + +ses.sendEmail(params, function(err, data) { + if (err) console.error('Error:', err); + else console.log('Success:', data); +}); +``` + +## 5. Security Note + +⚠️ **Important:** Your AWS credentials are currently visible in the browser source code. This is acceptable for testing but consider these alternatives for production: + +### Option A: Backend API (Recommended) +Create a simple backend endpoint that handles email sending: +```javascript +// Instead of direct SES, call your API +fetch('/api/send-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) +}); +``` + +### Option B: Cognito Identity Pool +Use temporary credentials instead of permanent access keys. + +## 6. Alternative: Quick Backend Solution + +If SES continues to have issues, here's a simple Node.js backend you could deploy: + +```javascript +// server.js +const express = require('express'); +const AWS = require('aws-sdk'); +const app = express(); + +AWS.config.update({ + region: 'eu-west-1', + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY +}); + +app.post('/send-email', (req, res) => { + const ses = new AWS.SES(); + // Email sending logic here +}); +``` + +## 7. Monitoring + +Check these regularly: +- AWS SES bounce rate (keep under 5%) +- AWS SES complaint rate (keep under 0.1%) +- Your website's email delivery success rate + +## Next Steps + +1. Check browser console for specific error messages +2. Verify email addresses in AWS SES +3. Ensure SES is out of sandbox mode or recipients are verified +4. Test with the minimal example above +5. Consider implementing a backend API for production use + +Let me know what specific error messages you see in the console!