initial
This commit is contained in:
203
README.md
Normal file
203
README.md
Normal file
@@ -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
|
||||||
|
<!-- Update these in the header -->
|
||||||
|
<div class="nav-logo">
|
||||||
|
<span>Your Business Name</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Update contact information -->
|
||||||
|
<p>(Your Phone Number)</p>
|
||||||
|
<p>your-email@yourdomain.com</p>
|
||||||
|
<p>Your Service Area</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<li><a href="services.html" class="nav-link">Services</a></li>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
160
aws-ses-setup.md
Normal file
160
aws-ses-setup.md
Normal file
@@ -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.
|
||||||
BIN
drone-op.jpg
Normal file
BIN
drone-op.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
199
emailjs-setup-guide.md
Normal file
199
emailjs-setup-guide.md
Normal file
@@ -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!
|
||||||
0
emailjs-setup.md
Normal file
0
emailjs-setup.md
Normal file
BIN
hero-video.mp4
Normal file
BIN
hero-video.mp4
Normal file
Binary file not shown.
417
index.html
Normal file
417
index.html
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PTSAerial - Professional Aerial Photography & Industrial Solutions | Dartford, Kent</title>
|
||||||
|
<meta name="description" content="PTS London trading as PTSAerial - Professional drone services for aerial photography, video capture, industrial inspections, and agricultural monitoring across Kent and the UK. CAA licensed and fully insured.">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<div class="nav-logo">
|
||||||
|
<img src="drone.png" alt="PTSAerial Drone" class="logo-icon">
|
||||||
|
<span>PTSAerial</span>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li><a href="#home" class="nav-link">Home</a></li>
|
||||||
|
<li><a href="#services" class="nav-link">Services</a></li>
|
||||||
|
<!-- <li><a href="#portfolio" class="nav-link">Portfolio</a></li> -->
|
||||||
|
<li><a href="#about" class="nav-link">About</a></li>
|
||||||
|
<li><a href="#contact" class="nav-link">Contact</a></li>
|
||||||
|
<li><a href="#quote" class="nav-link nav-cta">Get Quote</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="hamburger">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section id="home" class="hero">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title">Professional Drone Services</h1>
|
||||||
|
<p class="hero-subtitle">Capturing stunning aerial photography, providing industrial inspections, and agricultural monitoring with cutting-edge drone technology</p>
|
||||||
|
<div class="hero-buttons">
|
||||||
|
<a href="#quote" class="btn btn-primary">Get Free Quote</a>
|
||||||
|
<!-- <a href="#portfolio" class="btn btn-secondary">View Our Work</a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-video">
|
||||||
|
<video autoplay muted loop>
|
||||||
|
<source src="hero-video.mp4" type="video/mp4">
|
||||||
|
<!-- Placeholder for hero video -->
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Services Section -->
|
||||||
|
<section id="services" class="services">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Our Services</h2>
|
||||||
|
<p class="section-subtitle">Professional drone solutions for every need</p>
|
||||||
|
|
||||||
|
<div class="services-grid">
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Aerial Photography</h3>
|
||||||
|
<p>Stunning high-resolution aerial photographs for real estate, events, marketing, and personal projects. Capture unique perspectives that showcase your property or event from above.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>4K High-Resolution Images</li>
|
||||||
|
<li>Real Estate Photography</li>
|
||||||
|
<li>Event Coverage</li>
|
||||||
|
<li>Marketing Content</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-industry"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Industrial</h3>
|
||||||
|
<p>Safe and efficient inspections of infrastructure, buildings, towers, and industrial facilities. Reduce costs and safety risks with our advanced drone technology.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>Infrastructure Inspections</li>
|
||||||
|
<li>Thermal Imaging</li>
|
||||||
|
<li>Power Line Inspections</li>
|
||||||
|
<li>Detailed Reports</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-seedling"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Agricultural</h3>
|
||||||
|
<p>Precision agriculture solutions including crop monitoring, livestock tracking, and land surveying. Optimize your farming operations with actionable aerial data.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>Crop Health Analysis</li>
|
||||||
|
<li>Livestock Monitoring</li>
|
||||||
|
<li>Land Surveying</li>
|
||||||
|
<li>Irrigation Planning</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-hard-hat"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Construction</h3>
|
||||||
|
<p>Advanced solutions for construction site monitoring, including progress tracking, safety inspections, and site surveys. Ensure your project stays on schedule and within budget.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>Site Progress Tracking</li>
|
||||||
|
<li>Safety Inspections</li>
|
||||||
|
<li>3D Mapping</li>
|
||||||
|
<li>Site Surveys</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-drafting-compass"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Architectural</h3>
|
||||||
|
<p>High-quality aerial imagery and 3D modeling for architectural projects. Showcase your designs with stunning visuals and accurate site assessments.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>3D Modeling</li>
|
||||||
|
<li>Aerial Imagery</li>
|
||||||
|
<li>Site Assessments</li>
|
||||||
|
<li>Design Visualization</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="service-card">
|
||||||
|
<div class="service-icon">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
</div>
|
||||||
|
<h3>Security Monitoring</h3>
|
||||||
|
<p>Professional security surveillance and monitoring services for commercial properties, events, and industrial sites. Real-time monitoring with advanced thermal imaging capabilities.</p>
|
||||||
|
<ul class="service-features">
|
||||||
|
<li>Perimeter Surveillance</li>
|
||||||
|
<li>Thermal Imaging</li>
|
||||||
|
<li>Real-time Monitoring</li>
|
||||||
|
<li>Incident Documentation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<section id="portfolio" class="portfolio">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Our Work</h2>
|
||||||
|
<p class="section-subtitle">See the quality and creativity in our drone projects</p>
|
||||||
|
|
||||||
|
<div class="portfolio-filter">
|
||||||
|
<button class="filter-btn active" data-filter="all">All</button>
|
||||||
|
<button class="filter-btn" data-filter="photography">Photography</button>
|
||||||
|
<button class="filter-btn" data-filter="industrial">Industrial</button>
|
||||||
|
<button class="filter-btn" data-filter="agriculture">Agriculture</button>
|
||||||
|
<button class="filter-btn" data-filter="security">Security</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="portfolio-grid">
|
||||||
|
<div class="portfolio-item" data-category="photography">
|
||||||
|
<img src="https://via.placeholder.com/400x300/0066cc/ffffff?text=Real+Estate+Photo" alt="Real Estate Photography">
|
||||||
|
<div class="portfolio-overlay">
|
||||||
|
<h4>Real Estate Photography</h4>
|
||||||
|
<p>Luxury home showcase</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-item" data-category="industrial">
|
||||||
|
<img src="https://via.placeholder.com/400x300/cc0066/ffffff?text=Tower+Inspection" alt="Tower Inspection">
|
||||||
|
<div class="portfolio-overlay">
|
||||||
|
<h4>Tower Inspection</h4>
|
||||||
|
<p>Cell tower safety assessment</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-item" data-category="agriculture">
|
||||||
|
<img src="https://via.placeholder.com/400x300/66cc00/ffffff?text=Crop+Monitoring" alt="Crop Monitoring">
|
||||||
|
<div class="portfolio-overlay">
|
||||||
|
<h4>Crop Monitoring</h4>
|
||||||
|
<p>Precision agriculture analysis</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-item" data-category="security">
|
||||||
|
<img src="https://via.placeholder.com/400x300/990000/ffffff?text=Security+Monitoring" alt="Security Monitoring">
|
||||||
|
<div class="portfolio-overlay">
|
||||||
|
<h4>Security Monitoring</h4>
|
||||||
|
<p>Perimeter surveillance</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="portfolio-item" data-category="photography">
|
||||||
|
<img src="https://via.placeholder.com/400x300/0066cc/ffffff?text=Construction+Site" alt="Construction Photography">
|
||||||
|
<div class="portfolio-overlay">
|
||||||
|
<h4>Construction Progress</h4>
|
||||||
|
<p>Development documentation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section> -->
|
||||||
|
|
||||||
|
<!-- About Section -->
|
||||||
|
<section id="about" class="about">
|
||||||
|
<div class="container">
|
||||||
|
<div class="about-content">
|
||||||
|
<div class="about-text">
|
||||||
|
<h2>About PTSAerial</h2>
|
||||||
|
<p>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.</p>
|
||||||
|
|
||||||
|
<div class="about-stats">
|
||||||
|
<!-- <div class="stat">
|
||||||
|
<h3>500+</h3>
|
||||||
|
<p>Projects Completed</p>
|
||||||
|
</div> -->
|
||||||
|
<div class="stat">
|
||||||
|
<h3>100%</h3>
|
||||||
|
<p>Client Satisfaction</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<h3>24/7</h3>
|
||||||
|
<p>Support Available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="certifications">
|
||||||
|
<h4>Certifications & Insurance</h4>
|
||||||
|
<ul>
|
||||||
|
<li><i class="fas fa-check"></i> CAA Licensed (UK)</li>
|
||||||
|
<li><i class="fas fa-check"></i> Fully Insured Operations</li>
|
||||||
|
<li><i class="fas fa-check"></i> Professional Equipment</li>
|
||||||
|
<li><i class="fas fa-check"></i> Safety First Approach</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="about-image">
|
||||||
|
<img src="drone-op.jpg" alt="Professional Drone Operator">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Quote Form Section -->
|
||||||
|
<section id="quote" class="quote-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Get Your Free Quote</h2>
|
||||||
|
<p class="section-subtitle">Tell us about your project and we'll provide a detailed quote within 24 hours</p>
|
||||||
|
|
||||||
|
<form id="quoteForm" class="quote-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full Name *</label>
|
||||||
|
<input type="text" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address *</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone">Phone Number</label>
|
||||||
|
<input type="tel" id="phone" name="phone">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="company">Company/Organisation</label>
|
||||||
|
<input type="text" id="company" name="company">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="service">Service Type *</label>
|
||||||
|
<select id="service" name="service" required>
|
||||||
|
<option value="">Select a service</option>
|
||||||
|
<option value="aerial-photography">Aerial Photography</option>
|
||||||
|
<option value="industrial-inspection">Industrial Inspection</option>
|
||||||
|
<option value="agricultural-monitoring">Agricultural Monitoring</option>
|
||||||
|
<option value="security-monitoring">Security Monitoring</option>
|
||||||
|
<option value="construction">Construction</option>
|
||||||
|
<option value="architectural">Architectural</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location">Project Location *</label>
|
||||||
|
<input type="text" id="location" name="location" placeholder="City / County" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Preferred Date</label>
|
||||||
|
<input type="date" id="date" name="date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Project Description *</label>
|
||||||
|
<textarea id="description" name="description" rows="5" placeholder="Please describe your project, specific requirements, and any special considerations..." required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-large">Submit Quote Request</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Contact Section -->
|
||||||
|
<section id="contact" class="contact">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Contact Us</h2>
|
||||||
|
<div class="contact-content">
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="contact-item">
|
||||||
|
<i class="fas fa-phone"></i>
|
||||||
|
<div>
|
||||||
|
<h4>Phone</h4>
|
||||||
|
<p>+44 (0)20 3883 7578</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<div>
|
||||||
|
<h4>Email</h4>
|
||||||
|
<p>oli@ptslondon.co.uk</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
<div>
|
||||||
|
<h4>Location</h4>
|
||||||
|
<p>Dartford, Kent, UK</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-form">
|
||||||
|
<h3>Send us a message</h3>
|
||||||
|
<form id="contactForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" name="contact_name" placeholder="Your Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" name="contact_email" placeholder="Your Email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" name="subject" placeholder="Subject" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea name="message" rows="5" placeholder="Your Message" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Send Message</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-section">
|
||||||
|
<div class="footer-logo">
|
||||||
|
<img src="drone.png" alt="PTSAerial Drone" class="logo-icon">
|
||||||
|
<span>PTSAerial</span>
|
||||||
|
</div>
|
||||||
|
<p>Professional drone services for aerial photography, industrial inspections, and agricultural monitoring across Kent and the UK.</p>
|
||||||
|
<!-- <div class="social-links">
|
||||||
|
<a href="#"><i class="fab fa-facebook"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-youtube"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-linkedin"></i></a>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Services</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#services">Aerial Photography</a></li>
|
||||||
|
<li><a href="#services">Industrial Inspections</a></li>
|
||||||
|
<li><a href="#services">Agricultural Monitoring</a></li>
|
||||||
|
<li><a href="#services">Security Monitoring</a></li>
|
||||||
|
<li><a href="#services">Construction</a></li>
|
||||||
|
<li><a href="#services">Architectural</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Company</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#about">About Us</a></li>
|
||||||
|
<!-- <li><a href="#portfolio">Portfolio</a></li> -->
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
<li><a href="#quote">Get Quote</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Contact Info</h4>
|
||||||
|
<p><i class="fas fa-phone"></i> +44 (0)20 3883 7578</p>
|
||||||
|
<p><i class="fas fa-envelope"></i> oli@ptslondon.co.uk</p>
|
||||||
|
<p><i class="fas fa-map-marker-alt"></i> Dartford, Kent, UK</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2025 PTSAerial (PTS London Ltd). All rights reserved. | CAA Licensed | Fully Insured | Based in Dartford, Kent</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
451
script.js
Normal file
451
script.js
Normal file
@@ -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 = `
|
||||||
|
<span>${message}</span>
|
||||||
|
<button class="notification-close">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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);
|
||||||
798
styles.css
Normal file
798
styles.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
164
troubleshooting.md
Normal file
164
troubleshooting.md
Normal file
@@ -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!
|
||||||
Reference in New Issue
Block a user