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