v1.0.0-alpha
This commit is contained in:
parent
355f787a81
commit
b349cd2cdd
122
DEPLOYMENT.md
122
DEPLOYMENT.md
@ -1,122 +0,0 @@
|
|||||||
# 🚀 Production Deployment Guide
|
|
||||||
|
|
||||||
## 📋 **Prerequisites**
|
|
||||||
|
|
||||||
- Salesforce CLI (sf) installed
|
|
||||||
- Python 3.8+ on your server
|
|
||||||
- Access to your Salesforce sandbox
|
|
||||||
|
|
||||||
## 🎯 **Step 1: Deploy LWC to Salesforce**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Make script executable
|
|
||||||
chmod +x deploy-lwc-production.sh
|
|
||||||
|
|
||||||
# Run deployment
|
|
||||||
./deploy-lwc-production.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Output:**
|
|
||||||
```
|
|
||||||
✅ LWC deployment successful!
|
|
||||||
✅ Custom objects deployed!
|
|
||||||
✅ Permission set created!
|
|
||||||
✅ Lightning App Page created!
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 **Step 2: Deploy PDF API to Your Server**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On your server
|
|
||||||
cd python-pdf-generator
|
|
||||||
|
|
||||||
# Create virtual environment
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Start API server
|
|
||||||
python3 api_server.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Server will start on:** `http://0.0.0.0:8000`
|
|
||||||
|
|
||||||
## 🔧 **Step 3: Configure LWC with Your API URL**
|
|
||||||
|
|
||||||
Edit `force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Change this line
|
|
||||||
pdfApiBaseUrl = 'https://YOUR-ACTUAL-IP:8000/api';
|
|
||||||
```
|
|
||||||
|
|
||||||
**Replace `YOUR-ACTUAL-IP` with your server's IP address.**
|
|
||||||
|
|
||||||
## 🔒 **Step 4: Security Configuration**
|
|
||||||
|
|
||||||
### **Firewall Setup:**
|
|
||||||
```bash
|
|
||||||
# Open port 8000
|
|
||||||
sudo ufw allow 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### **CORS Configuration (if needed):**
|
|
||||||
Edit `python-pdf-generator/api_server.py`:
|
|
||||||
```python
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=[
|
|
||||||
"https://tso3--r1.sandbox.lightning.force.com",
|
|
||||||
"https://your-salesforce-domain.com"
|
|
||||||
],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 **Step 5: Test Your Deployment**
|
|
||||||
|
|
||||||
1. **Open Salesforce Sandbox:** `https://tso3--r1.sandbox.lightning.force.com`
|
|
||||||
2. **Login:** `contact+tso3@propertycrm.ae.r1` / `Demo@123`
|
|
||||||
3. **Search for:** "Property Brochure Generator"
|
|
||||||
4. **Test the flow:** Template → Property → Preview → Download
|
|
||||||
|
|
||||||
## 📊 **Expected Results**
|
|
||||||
|
|
||||||
- ✅ LWC loads in Salesforce
|
|
||||||
- ✅ Properties load from your data
|
|
||||||
- ✅ PDF preview generates
|
|
||||||
- ✅ PDF downloads successfully
|
|
||||||
- ✅ All 23+ properties accessible
|
|
||||||
|
|
||||||
## 🚨 **Troubleshooting**
|
|
||||||
|
|
||||||
### **LWC Not Loading:**
|
|
||||||
- Check deployment logs
|
|
||||||
- Verify permission sets
|
|
||||||
- Check user access
|
|
||||||
|
|
||||||
### **API Connection Failed:**
|
|
||||||
- Verify IP address in LWC
|
|
||||||
- Check firewall settings
|
|
||||||
- Ensure API server is running
|
|
||||||
|
|
||||||
### **No Properties Found:**
|
|
||||||
- Verify `pcrm__Property__c` object exists
|
|
||||||
- Check field permissions
|
|
||||||
- Verify data in sandbox
|
|
||||||
|
|
||||||
## 📞 **Support**
|
|
||||||
|
|
||||||
For deployment issues:
|
|
||||||
1. Check Salesforce CLI logs
|
|
||||||
2. Verify API server status
|
|
||||||
3. Check browser console for errors
|
|
||||||
4. Ensure all prerequisites are met
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎯 Your system is now production-ready!**
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
# 🚀 Production Deployment Checklist - Property Brochure Generator LWC
|
|
||||||
|
|
||||||
## 📋 **Pre-Deployment Checklist**
|
|
||||||
|
|
||||||
### **✅ LWC Component Ready**
|
|
||||||
- [ ] All LLM-style emojis removed from UI
|
|
||||||
- [ ] Professional SVG icons implemented
|
|
||||||
- [ ] Error handling and validation implemented
|
|
||||||
- [ ] Toast notifications configured
|
|
||||||
- [ ] Loading states implemented
|
|
||||||
- [ ] Responsive design tested
|
|
||||||
|
|
||||||
### **✅ Configuration Updated**
|
|
||||||
- [ ] PDF API URL updated in `propertyTemplateSelector.js`
|
|
||||||
- [ ] PDF API URL updated in `PropertyTemplateController.cls`
|
|
||||||
- [ ] Production configuration file created
|
|
||||||
- [ ] Error messages customized for production
|
|
||||||
- [ ] Success messages customized for production
|
|
||||||
|
|
||||||
### **✅ Code Quality**
|
|
||||||
- [ ] No console.log statements in production code
|
|
||||||
- [ ] Error boundaries implemented
|
|
||||||
- [ ] Performance optimizations applied
|
|
||||||
- [ ] Accessibility features implemented
|
|
||||||
- [ ] Cross-browser compatibility tested
|
|
||||||
|
|
||||||
## 🌐 **PDF API Server Deployment**
|
|
||||||
|
|
||||||
### **✅ Server Configuration**
|
|
||||||
- [ ] Python API server deployed to your IP
|
|
||||||
- [ ] Port 8000 opened in firewall
|
|
||||||
- [ ] HTTPS configured (recommended for production)
|
|
||||||
- [ ] CORS configured for Salesforce domains
|
|
||||||
- [ ] Environment variables set
|
|
||||||
|
|
||||||
### **✅ API Endpoints Working**
|
|
||||||
- [ ] `/api/health` - Health check endpoint
|
|
||||||
- [ ] `/api/preview` - PDF preview generation
|
|
||||||
- [ ] `/api/generate-pdf` - PDF generation and download
|
|
||||||
- [ ] `/api/templates` - Available templates
|
|
||||||
|
|
||||||
## 🔧 **Salesforce Configuration**
|
|
||||||
|
|
||||||
### **✅ Custom Objects Deployed**
|
|
||||||
- [ ] `Property_Template__c` object deployed
|
|
||||||
- [ ] `Property__c` object fields updated
|
|
||||||
- [ ] Permission sets configured
|
|
||||||
- [ ] User access granted
|
|
||||||
|
|
||||||
### **✅ LWC Deployment**
|
|
||||||
- [ ] Component deployed to sandbox
|
|
||||||
- [ ] Lightning App Page created
|
|
||||||
- [ ] Component added to page layouts
|
|
||||||
- [ ] User permissions verified
|
|
||||||
|
|
||||||
## 📱 **Testing Checklist**
|
|
||||||
|
|
||||||
### **✅ Functionality Testing**
|
|
||||||
- [ ] Template selection working
|
|
||||||
- [ ] Property data loading from Salesforce
|
|
||||||
- [ ] Form validation working
|
|
||||||
- [ ] Image upload functionality
|
|
||||||
- [ ] PDF preview generation
|
|
||||||
- [ ] PDF download working
|
|
||||||
|
|
||||||
### **✅ User Experience Testing**
|
|
||||||
- [ ] 5-step wizard flow smooth
|
|
||||||
- [ ] Error messages clear and helpful
|
|
||||||
- [ ] Loading states informative
|
|
||||||
- [ ] Responsive on mobile devices
|
|
||||||
- [ ] Accessibility features working
|
|
||||||
|
|
||||||
### **✅ Integration Testing**
|
|
||||||
- [ ] Salesforce data integration working
|
|
||||||
- [ ] PDF API communication successful
|
|
||||||
- [ ] Error handling graceful
|
|
||||||
- [ ] Performance acceptable
|
|
||||||
|
|
||||||
## 🚨 **Production Security**
|
|
||||||
|
|
||||||
### **✅ Security Measures**
|
|
||||||
- [ ] API endpoints secured
|
|
||||||
- [ ] CORS properly configured
|
|
||||||
- [ ] Input validation implemented
|
|
||||||
- [ ] File upload restrictions set
|
|
||||||
- [ ] Error messages don't expose sensitive data
|
|
||||||
|
|
||||||
### **✅ Monitoring & Logging**
|
|
||||||
- [ ] Error logging configured
|
|
||||||
- [ ] Performance monitoring enabled
|
|
||||||
- [ ] User activity tracking
|
|
||||||
- [ ] API usage monitoring
|
|
||||||
|
|
||||||
## 📊 **Performance Optimization**
|
|
||||||
|
|
||||||
### **✅ Performance Settings**
|
|
||||||
- [ ] Image compression enabled
|
|
||||||
- [ ] PDF generation optimized
|
|
||||||
- [ ] Caching implemented
|
|
||||||
- [ ] Database queries optimized
|
|
||||||
- [ ] API response times acceptable
|
|
||||||
|
|
||||||
## 🔄 **Post-Deployment**
|
|
||||||
|
|
||||||
### **✅ Verification**
|
|
||||||
- [ ] All users can access component
|
|
||||||
- [ ] PDF generation working for all templates
|
|
||||||
- [ ] Error handling working correctly
|
|
||||||
- [ ] Performance meets requirements
|
|
||||||
|
|
||||||
### **✅ Documentation**
|
|
||||||
- [ ] User manual created
|
|
||||||
- [ ] Admin guide prepared
|
|
||||||
- [ ] Troubleshooting guide available
|
|
||||||
- [ ] Support contact information provided
|
|
||||||
|
|
||||||
## 📞 **Support & Maintenance**
|
|
||||||
|
|
||||||
### **✅ Support Plan**
|
|
||||||
- [ ] Support team trained
|
|
||||||
- [ ] Escalation procedures defined
|
|
||||||
- [ ] Maintenance schedule planned
|
|
||||||
- [ ] Backup and recovery procedures
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Final Steps Before Go-Live**
|
|
||||||
|
|
||||||
1. **Update PDF API URL** in both LWC and Apex controller
|
|
||||||
2. **Test complete workflow** end-to-end
|
|
||||||
3. **Verify user permissions** and access
|
|
||||||
4. **Monitor system performance** for first 24 hours
|
|
||||||
5. **Provide user training** and documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎉 Your Property Brochure Generator is now Production Ready!**
|
|
||||||
57
deploy-fix.sh
Executable file
57
deploy-fix.sh
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🚀 Deploying Fixed Apex Controllers to Salesforce..."
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
# Check if sf CLI is installed
|
||||||
|
if ! command -v sf &> /dev/null; then
|
||||||
|
echo "❌ Salesforce CLI (sf) is not installed. Please install it first."
|
||||||
|
echo " Visit: https://developer.salesforce.com/tools/sfdxcli"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're authenticated
|
||||||
|
echo "🔐 Checking authentication..."
|
||||||
|
if ! sf org display &> /dev/null; then
|
||||||
|
echo "❌ Not authenticated to Salesforce. Please login first:"
|
||||||
|
echo " sf org login web"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Authenticated to Salesforce"
|
||||||
|
|
||||||
|
# Deploy the fixed Apex controllers
|
||||||
|
echo "📦 Deploying PropertyDataController and other Apex classes..."
|
||||||
|
sf project deploy start --source-dir force-app/main/default/classes --target-org $(sf org display --json | jq -r '.result.username')
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ Apex classes deployed successfully!"
|
||||||
|
else
|
||||||
|
echo "❌ Deployment failed. Please check the error messages above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy the LWC component
|
||||||
|
echo "⚡ Deploying LWC component..."
|
||||||
|
sf project deploy start --source-dir force-app/main/default/lwc --target-org $(sf org display --json | jq -r '.result.username')
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ LWC component deployed successfully!"
|
||||||
|
else
|
||||||
|
echo "❌ LWC deployment failed. Please check the error messages above."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Deployment Complete!"
|
||||||
|
echo "======================"
|
||||||
|
echo "✅ Fixed PropertyDataController deployed"
|
||||||
|
echo "✅ Updated LWC component deployed"
|
||||||
|
echo "✅ No more 'pcrm__View__c' field errors"
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Now refresh your Salesforce org and test the dropdown!"
|
||||||
|
echo " The dropdown should now show all 24 properties without errors."
|
||||||
|
echo ""
|
||||||
|
echo "📱 If you still have issues, use the debug tools in the component:"
|
||||||
|
echo " 1. Click '🚀 Bypass Template' to create a working dropdown"
|
||||||
|
echo " 2. Click '🔍 Check Template' to debug any remaining issues"
|
||||||
51
force-app/main/default/classes/PDFGenerationProxy.cls
Normal file
51
force-app/main/default/classes/PDFGenerationProxy.cls
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
public with sharing class PDFGenerationProxy {
|
||||||
|
|
||||||
|
@AuraEnabled
|
||||||
|
public static String generatePDFFromHTML(String htmlContent) {
|
||||||
|
try {
|
||||||
|
// Prepare the request
|
||||||
|
Http http = new Http();
|
||||||
|
HttpRequest request = new HttpRequest();
|
||||||
|
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
|
||||||
|
request.setMethod('POST');
|
||||||
|
request.setHeader('Content-Type', 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');
|
||||||
|
|
||||||
|
// Create multipart form data
|
||||||
|
String boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
|
||||||
|
String body = '';
|
||||||
|
body += '--' + boundary + '\r\n';
|
||||||
|
body += 'Content-Disposition: form-data; name="input"; filename="template.html"\r\n';
|
||||||
|
body += 'Content-Type: text/html\r\n\r\n';
|
||||||
|
body += htmlContent + '\r\n';
|
||||||
|
body += '--' + boundary + '--\r\n';
|
||||||
|
|
||||||
|
request.setBody(body);
|
||||||
|
request.setTimeout(120000); // 2 minutes timeout
|
||||||
|
|
||||||
|
// Make the callout
|
||||||
|
HttpResponse response = http.send(request);
|
||||||
|
|
||||||
|
if (response.getStatusCode() == 200) {
|
||||||
|
// Convert the PDF response to base64
|
||||||
|
Blob pdfBlob = response.getBodyAsBlob();
|
||||||
|
return EncodingUtil.base64Encode(pdfBlob);
|
||||||
|
} else {
|
||||||
|
throw new CalloutException('API call failed with status: ' + response.getStatusCode() + ' - ' + response.getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuraHandledException('PDF generation failed: ' + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AuraEnabled
|
||||||
|
public static String testAPIConnection() {
|
||||||
|
try {
|
||||||
|
// Test with simple HTML
|
||||||
|
String testHtml = '<!DOCTYPE html><html><head><title>Test</title></head><body><h1>Test PDF Generation</h1></body></html>';
|
||||||
|
return generatePDFFromHTML(testHtml);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuraHandledException('API test failed: ' + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<apiVersion>64.0</apiVersion>
|
||||||
|
<status>Active</status>
|
||||||
|
</ApexClass>
|
||||||
@ -19,6 +19,10 @@ public with sharing class PropertyDataController {
|
|||||||
'pcrm__Sub_Locality_Bayut_Dubizzle__c, pcrm__Tower_Bayut_Dubizzle__c, ' +
|
'pcrm__Sub_Locality_Bayut_Dubizzle__c, pcrm__Tower_Bayut_Dubizzle__c, ' +
|
||||||
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
|
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
|
||||||
'pcrm__City_Propertyfinder__c, ' +
|
'pcrm__City_Propertyfinder__c, ' +
|
||||||
|
'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' +
|
||||||
|
'Contact__c, Contact__r.FirstName, Contact__r.LastName, ' +
|
||||||
|
'Email__c, Phone__c, ' +
|
||||||
|
'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' +
|
||||||
'CreatedDate, LastModifiedDate ' +
|
'CreatedDate, LastModifiedDate ' +
|
||||||
'FROM pcrm__Property__c ' +
|
'FROM pcrm__Property__c ' +
|
||||||
'ORDER BY Name ASC';
|
'ORDER BY Name ASC';
|
||||||
@ -82,6 +86,10 @@ public with sharing class PropertyDataController {
|
|||||||
'pcrm__Sub_Locality_Bayut_Dubizzle__c, pcrm__Tower_Bayut_Dubizzle__c, ' +
|
'pcrm__Sub_Locality_Bayut_Dubizzle__c, pcrm__Tower_Bayut_Dubizzle__c, ' +
|
||||||
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
|
'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' +
|
||||||
'pcrm__City_Propertyfinder__c, ' +
|
'pcrm__City_Propertyfinder__c, ' +
|
||||||
|
'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' +
|
||||||
|
'Contact__c, Contact__r.FirstName, Contact__r.LastName, ' +
|
||||||
|
'Email__c, Phone__c, ' +
|
||||||
|
'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' +
|
||||||
'CreatedDate, LastModifiedDate ' +
|
'CreatedDate, LastModifiedDate ' +
|
||||||
'FROM pcrm__Property__c ' +
|
'FROM pcrm__Property__c ' +
|
||||||
'WHERE Id = :propertyId';
|
'WHERE Id = :propertyId';
|
||||||
@ -129,4 +137,42 @@ public with sharing class PropertyDataController {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@AuraEnabled(cacheable=true)
|
||||||
|
public static List<Map<String, Object>> getPropertyImages(String propertyId) {
|
||||||
|
try {
|
||||||
|
System.debug('=== FETCHING PROPERTY IMAGES ===');
|
||||||
|
System.debug('Property ID: ' + propertyId);
|
||||||
|
|
||||||
|
List<Map<String, Object>> images = new List<Map<String, Object>>();
|
||||||
|
|
||||||
|
// Query Image Genie records for this property
|
||||||
|
List<pcrm__Image_Genie__c> imageRecords = [
|
||||||
|
SELECT Id, Name, pcrm__Category__c, pcrm__Title__c, Public_URL__c, pcrm__Property__c
|
||||||
|
FROM pcrm__Image_Genie__c
|
||||||
|
WHERE pcrm__Property__c = :propertyId
|
||||||
|
ORDER BY pcrm__Category__c, Name
|
||||||
|
];
|
||||||
|
|
||||||
|
System.debug('Found ' + imageRecords.size() + ' image records');
|
||||||
|
|
||||||
|
for (pcrm__Image_Genie__c img : imageRecords) {
|
||||||
|
Map<String, Object> imageData = new Map<String, Object>();
|
||||||
|
imageData.put('id', img.Id);
|
||||||
|
imageData.put('name', img.pcrm__Title__c);
|
||||||
|
imageData.put('category', img.pcrm__Category__c);
|
||||||
|
imageData.put('url', img.Public_URL__c);
|
||||||
|
images.add(imageData);
|
||||||
|
|
||||||
|
System.debug('Image: ' + img.pcrm__Title__c + ' - Category: ' + img.pcrm__Category__c + ' - URL: ' + img.Public_URL__c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.debug('Error fetching property images: ' + e.getMessage());
|
||||||
|
System.debug('Stack trace: ' + e.getStackTraceString());
|
||||||
|
throw new AuraHandledException('Failed to fetch property images: ' + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -176,7 +176,7 @@ public with sharing class PropertyPdfGeneratorController {
|
|||||||
// Make HTTP callout to Python API
|
// Make HTTP callout to Python API
|
||||||
Http http = new Http();
|
Http http = new Http();
|
||||||
HttpRequest request = new HttpRequest();
|
HttpRequest request = new HttpRequest();
|
||||||
request.setEndpoint('https://salesforce.tech4biz.io/api/generate-pdf');
|
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
|
||||||
request.setMethod('POST');
|
request.setMethod('POST');
|
||||||
request.setHeader('Content-Type', 'application/json');
|
request.setHeader('Content-Type', 'application/json');
|
||||||
request.setBody(jsonPayload);
|
request.setBody(jsonPayload);
|
||||||
|
|||||||
@ -86,6 +86,66 @@ public with sharing class PropertyTemplateController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to create template HTML content
|
||||||
|
private static String createTemplateHTML(Map<String, Object> propertyMap, String templateName) {
|
||||||
|
try {
|
||||||
|
String propertyName = (String) propertyMap.get('propertyName') != null ? (String) propertyMap.get('propertyName') : 'Property Name';
|
||||||
|
String propertyType = (String) propertyMap.get('propertyType') != null ? (String) propertyMap.get('propertyType') : 'Property Type';
|
||||||
|
String location = (String) propertyMap.get('location') != null ? (String) propertyMap.get('location') : 'Location';
|
||||||
|
String price = (String) propertyMap.get('price') != null ? (String) propertyMap.get('price') : 'Price';
|
||||||
|
String bedrooms = (String) propertyMap.get('bedrooms') != null ? (String) propertyMap.get('bedrooms') : 'N/A';
|
||||||
|
String bathrooms = (String) propertyMap.get('bathrooms') != null ? (String) propertyMap.get('bathrooms') : 'N/A';
|
||||||
|
String area = (String) propertyMap.get('area') != null ? (String) propertyMap.get('area') : 'N/A';
|
||||||
|
String description = (String) propertyMap.get('description') != null ? (String) propertyMap.get('description') : 'Property Description';
|
||||||
|
|
||||||
|
// Create a professional property brochure HTML
|
||||||
|
String htmlContent = '<!DOCTYPE html><html><head><meta charset="UTF-8">';
|
||||||
|
htmlContent += '<title>' + propertyName + ' - Property Brochure</title>';
|
||||||
|
htmlContent += '<style>';
|
||||||
|
htmlContent += 'body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }';
|
||||||
|
htmlContent += '.brochure { max-width: 800px; margin: 0 auto; background: white; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); overflow: hidden; }';
|
||||||
|
htmlContent += '.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; text-align: center; }';
|
||||||
|
htmlContent += '.header h1 { margin: 0; font-size: 2.5em; font-weight: 300; }';
|
||||||
|
htmlContent += '.header .subtitle { margin: 10px 0 0 0; font-size: 1.2em; opacity: 0.9; }';
|
||||||
|
htmlContent += '.content { padding: 40px; }';
|
||||||
|
htmlContent += '.property-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 30px 0; }';
|
||||||
|
htmlContent += '.stat-box { background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; border: 1px solid #dee2e6; }';
|
||||||
|
htmlContent += '.stat-label { font-weight: bold; color: #667eea; font-size: 14px; text-transform: uppercase; margin-bottom: 8px; }';
|
||||||
|
htmlContent += '.stat-value { font-size: 18px; color: #333; font-weight: 600; }';
|
||||||
|
htmlContent += '.description { background: #f8f9fa; padding: 25px; border-radius: 10px; margin: 25px 0; }';
|
||||||
|
htmlContent += '.description h2 { color: #667eea; margin-bottom: 15px; font-size: 20px; border-bottom: 2px solid #667eea; padding-bottom: 8px; }';
|
||||||
|
htmlContent += '.footer { text-align: center; color: #666; font-size: 12px; border-top: 1px solid #dee2e6; padding-top: 20px; margin-top: 30px; }';
|
||||||
|
htmlContent += '</style></head><body>';
|
||||||
|
htmlContent += '<div class="brochure">';
|
||||||
|
htmlContent += '<div class="header">';
|
||||||
|
htmlContent += '<h1>' + propertyName + '</h1>';
|
||||||
|
htmlContent += '<div class="subtitle">' + propertyType + ' • ' + location + '</div>';
|
||||||
|
htmlContent += '</div>';
|
||||||
|
htmlContent += '<div class="content">';
|
||||||
|
htmlContent += '<div class="property-stats">';
|
||||||
|
htmlContent += '<div class="stat-box"><div class="stat-label">Price</div><div class="stat-value">' + price + '</div></div>';
|
||||||
|
htmlContent += '<div class="stat-box"><div class="stat-label">Bedrooms</div><div class="stat-value">' + bedrooms + '</div></div>';
|
||||||
|
htmlContent += '<div class="stat-box"><div class="stat-label">Bathrooms</div><div class="stat-value">' + bathrooms + '</div></div>';
|
||||||
|
htmlContent += '<div class="stat-box"><div class="stat-label">Area</div><div class="stat-value">' + area + '</div></div>';
|
||||||
|
htmlContent += '</div>';
|
||||||
|
htmlContent += '<div class="description">';
|
||||||
|
htmlContent += '<h2>Property Description</h2>';
|
||||||
|
htmlContent += '<p>' + description + '</p>';
|
||||||
|
htmlContent += '</div>';
|
||||||
|
htmlContent += '</div>';
|
||||||
|
htmlContent += '<div class="footer">';
|
||||||
|
htmlContent += '<p><strong>Generated on:</strong> ' + Datetime.now().format('MMMM dd, yyyy \'at\' h:mm a') + '</p>';
|
||||||
|
htmlContent += '<p><em>Property CRM System - Professional Brochure</em></p>';
|
||||||
|
htmlContent += '</div>';
|
||||||
|
htmlContent += '</div></body></html>';
|
||||||
|
|
||||||
|
return htmlContent;
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.debug('Error creating template HTML: ' + e.getMessage());
|
||||||
|
return '<html><body><h1>Error generating template</h1><p>' + e.getMessage() + '</p></body></html>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AuraEnabled
|
@AuraEnabled
|
||||||
public static Map<String, Object> generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF) {
|
public static Map<String, Object> generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF) {
|
||||||
try {
|
try {
|
||||||
@ -93,12 +153,35 @@ public with sharing class PropertyTemplateController {
|
|||||||
Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData);
|
Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData);
|
||||||
|
|
||||||
// Call external Python API for PDF generation
|
// Call external Python API for PDF generation
|
||||||
String apiEndpoint = 'http://160.187.166.67:8000/api/generate-pdf'; // Production PDF Generator API
|
String apiEndpoint = 'https://salesforce.tech4biz.io/generate-pdf'; // Production PDF Generator API
|
||||||
|
|
||||||
// Prepare request body
|
// Prepare request body - using the format that was working in first prompt
|
||||||
Map<String, Object> requestBody = new Map<String, Object>();
|
Map<String, Object> requestBody = new Map<String, Object>();
|
||||||
|
|
||||||
|
// Debug: Log the property data and template name
|
||||||
|
System.debug('=== DEBUG INFO ===');
|
||||||
|
System.debug('propertyMap: ' + propertyMap);
|
||||||
|
System.debug('templateName: ' + templateName);
|
||||||
|
System.debug('propertyMap keys: ' + propertyMap.keySet());
|
||||||
|
|
||||||
|
// Generate HTML content
|
||||||
|
String generatedHTML = createTemplateHTML(propertyMap, templateName);
|
||||||
|
System.debug('Generated HTML length: ' + generatedHTML.length());
|
||||||
|
System.debug('Generated HTML preview: ' + generatedHTML.substring(0, Math.min(200, generatedHTML.length())));
|
||||||
|
|
||||||
|
// Validate that HTML content was generated
|
||||||
|
if (String.isBlank(generatedHTML)) {
|
||||||
|
Map<String, Object> result = new Map<String, Object>();
|
||||||
|
result.put('success', false);
|
||||||
|
result.put('message', 'Error: HTML content generation failed - content is empty');
|
||||||
|
System.debug('ERROR: Generated HTML is empty!');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody.put('html_content', generatedHTML);
|
||||||
requestBody.put('property_data', propertyMap);
|
requestBody.put('property_data', propertyMap);
|
||||||
requestBody.put('template_name', templateName);
|
requestBody.put('template_name', templateName);
|
||||||
|
requestBody.put('filename', 'property_brochure.pdf');
|
||||||
|
|
||||||
// Make HTTP callout to Python API
|
// Make HTTP callout to Python API
|
||||||
Http http = new Http();
|
Http http = new Http();
|
||||||
@ -106,10 +189,18 @@ public with sharing class PropertyTemplateController {
|
|||||||
request.setEndpoint(apiEndpoint);
|
request.setEndpoint(apiEndpoint);
|
||||||
request.setMethod('POST');
|
request.setMethod('POST');
|
||||||
request.setHeader('Content-Type', 'application/json');
|
request.setHeader('Content-Type', 'application/json');
|
||||||
|
request.setHeader('Accept', 'application/json');
|
||||||
request.setBody(JSON.serialize(requestBody));
|
request.setBody(JSON.serialize(requestBody));
|
||||||
|
request.setTimeout(120000); // 2 minutes timeout
|
||||||
|
|
||||||
|
System.debug('Calling Python API at: ' + apiEndpoint);
|
||||||
|
System.debug('Request body: ' + JSON.serialize(requestBody));
|
||||||
|
|
||||||
HttpResponse response = http.send(request);
|
HttpResponse response = http.send(request);
|
||||||
|
|
||||||
|
System.debug('Response status: ' + response.getStatusCode());
|
||||||
|
System.debug('Response body: ' + response.getBody());
|
||||||
|
|
||||||
if (response.getStatusCode() == 200) {
|
if (response.getStatusCode() == 200) {
|
||||||
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
|
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
|
||||||
|
|
||||||
@ -118,11 +209,14 @@ public with sharing class PropertyTemplateController {
|
|||||||
result.put('pdfUrl', responseMap.get('pdf_url'));
|
result.put('pdfUrl', responseMap.get('pdf_url'));
|
||||||
result.put('message', 'PDF generated successfully');
|
result.put('message', 'PDF generated successfully');
|
||||||
|
|
||||||
|
System.debug('PDF generation successful: ' + result);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
Map<String, Object> result = new Map<String, Object>();
|
Map<String, Object> result = new Map<String, Object>();
|
||||||
result.put('success', false);
|
result.put('success', false);
|
||||||
result.put('message', 'Failed to generate PDF: ' + response.getStatus());
|
result.put('message', 'Failed to generate PDF: ' + response.getStatus() + ' - ' + response.getBody());
|
||||||
|
|
||||||
|
System.debug('PDF generation failed: ' + result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1262
package-lock.json
generated
1262
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,9 @@
|
|||||||
</types>
|
</types>
|
||||||
<types>
|
<types>
|
||||||
<members>PropertyTemplateController</members>
|
<members>PropertyTemplateController</members>
|
||||||
|
<members>PropertyDataController</members>
|
||||||
|
<members>PdfApiController</members>
|
||||||
|
<members>PropertyPdfGeneratorController</members>
|
||||||
<name>ApexClass</name>
|
<name>ApexClass</name>
|
||||||
</types>
|
</types>
|
||||||
<types>
|
<types>
|
||||||
|
|||||||
@ -1,608 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Advanced Property PDF Templates
|
|
||||||
Highly sophisticated templates with real estate images and professional layouts
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
from reportlab.lib.pagesizes import A4
|
|
||||||
from reportlab.lib.styles import ParagraphStyle
|
|
||||||
from reportlab.lib.units import inch, cm
|
|
||||||
from reportlab.lib import colors
|
|
||||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
|
|
||||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak
|
|
||||||
from reportlab.platypus.flowables import KeepTogether
|
|
||||||
from reportlab.pdfgen import canvas
|
|
||||||
from reportlab.lib.colors import HexColor
|
|
||||||
from PIL import Image as PILImage
|
|
||||||
import io
|
|
||||||
import base64
|
|
||||||
|
|
||||||
class AdvancedPropertyTemplates:
|
|
||||||
"""Advanced property PDF templates with sophisticated designs"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.setup_advanced_styles()
|
|
||||||
|
|
||||||
def setup_advanced_styles(self):
|
|
||||||
"""Setup advanced paragraph styles"""
|
|
||||||
self.styles = {}
|
|
||||||
|
|
||||||
# Ultra Premium Title
|
|
||||||
self.styles['UltraTitle'] = ParagraphStyle(
|
|
||||||
name='UltraTitle',
|
|
||||||
fontSize=48,
|
|
||||||
textColor=HexColor('#1a1a1a'),
|
|
||||||
alignment=TA_CENTER,
|
|
||||||
spaceAfter=35,
|
|
||||||
fontName='Helvetica-Bold',
|
|
||||||
leading=56
|
|
||||||
)
|
|
||||||
|
|
||||||
# Premium Subtitle
|
|
||||||
self.styles['PremiumSubtitle'] = ParagraphStyle(
|
|
||||||
name='PremiumSubtitle',
|
|
||||||
fontSize=22,
|
|
||||||
textColor=HexColor('#666666'),
|
|
||||||
alignment=TA_CENTER,
|
|
||||||
spaceAfter=30,
|
|
||||||
fontName='Helvetica',
|
|
||||||
leading=26
|
|
||||||
)
|
|
||||||
|
|
||||||
# Section Headers
|
|
||||||
self.styles['SectionHeader'] = ParagraphStyle(
|
|
||||||
name='SectionHeader',
|
|
||||||
fontSize=28,
|
|
||||||
textColor=HexColor('#1f2937'),
|
|
||||||
alignment=TA_LEFT,
|
|
||||||
spaceAfter=20,
|
|
||||||
fontName='Helvetica-Bold',
|
|
||||||
leading=32
|
|
||||||
)
|
|
||||||
|
|
||||||
# Content Text
|
|
||||||
self.styles['ContentText'] = ParagraphStyle(
|
|
||||||
name='ContentText',
|
|
||||||
fontSize=13,
|
|
||||||
textColor=HexColor('#374151'),
|
|
||||||
alignment=TA_JUSTIFY,
|
|
||||||
spaceAfter=15,
|
|
||||||
fontName='Helvetica',
|
|
||||||
leading=18
|
|
||||||
)
|
|
||||||
|
|
||||||
# Feature Text
|
|
||||||
self.styles['FeatureText'] = ParagraphStyle(
|
|
||||||
name='FeatureText',
|
|
||||||
fontSize=14,
|
|
||||||
textColor=HexColor('#1f2937'),
|
|
||||||
alignment=TA_LEFT,
|
|
||||||
spaceAfter=12,
|
|
||||||
fontName='Helvetica-Bold',
|
|
||||||
leading=18
|
|
||||||
)
|
|
||||||
|
|
||||||
# Price Display
|
|
||||||
self.styles['PriceDisplay'] = ParagraphStyle(
|
|
||||||
name='PriceDisplay',
|
|
||||||
fontSize=36,
|
|
||||||
textColor=HexColor('#dc2626'),
|
|
||||||
alignment=TA_CENTER,
|
|
||||||
spaceAfter=30,
|
|
||||||
fontName='Helvetica-Bold',
|
|
||||||
leading=42
|
|
||||||
)
|
|
||||||
|
|
||||||
# Amenity Item
|
|
||||||
self.styles['AmenityItem'] = ParagraphStyle(
|
|
||||||
name='AmenityItem',
|
|
||||||
fontSize=13,
|
|
||||||
textColor=HexColor('#4b5563'),
|
|
||||||
alignment=TA_LEFT,
|
|
||||||
spaceAfter=10,
|
|
||||||
fontName='Helvetica',
|
|
||||||
leading=17
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_luxury_villa_template(self, data: Dict[str, Any], output_path: str) -> str:
|
|
||||||
"""Create ultra-luxury villa template with sophisticated design"""
|
|
||||||
doc = SimpleDocTemplate(
|
|
||||||
output_path,
|
|
||||||
pagesize=A4,
|
|
||||||
rightMargin=0.3*cm,
|
|
||||||
leftMargin=0.3*cm,
|
|
||||||
topMargin=0.3*cm,
|
|
||||||
bottomMargin=0.3*cm
|
|
||||||
)
|
|
||||||
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Page 1: Cover Page
|
|
||||||
story.extend(self._create_cover_page(data))
|
|
||||||
story.append(PageBreak())
|
|
||||||
|
|
||||||
# Page 2: Property Overview
|
|
||||||
story.extend(self._create_property_overview(data))
|
|
||||||
story.append(PageBreak())
|
|
||||||
|
|
||||||
# Page 3: Features & Amenities
|
|
||||||
story.extend(self._create_features_page(data))
|
|
||||||
story.append(PageBreak())
|
|
||||||
|
|
||||||
# Page 4: Location & Investment
|
|
||||||
story.extend(self._create_investment_page(data))
|
|
||||||
|
|
||||||
# Build PDF with custom header/footer
|
|
||||||
doc.build(story, onFirstPage=lambda c, d: self._create_luxury_header_footer(c, 1, "LUXURY VILLA"),
|
|
||||||
onLaterPages=lambda c, d: self._create_luxury_header_footer(c, d.page, "LUXURY VILLA"))
|
|
||||||
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
def _create_cover_page(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create sophisticated cover page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Main Title
|
|
||||||
story.append(Paragraph("LUXURY VILLA COLLECTION", self.styles['UltraTitle']))
|
|
||||||
story.append(Spacer(1, 40))
|
|
||||||
|
|
||||||
# Property Name
|
|
||||||
story.append(Paragraph(f"{data.get('propertyName', 'Exclusive Villa')}", self.styles['UltraTitle']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Location
|
|
||||||
story.append(Paragraph(f"Located in {data.get('location', 'Dubai')}", self.styles['PremiumSubtitle']))
|
|
||||||
story.append(Spacer(1, 50))
|
|
||||||
|
|
||||||
# Price Highlight
|
|
||||||
story.append(Paragraph("INVESTMENT VALUE", self.styles['SectionHeader']))
|
|
||||||
story.append(Paragraph(f"AED {data.get('price', 'N/A')}", self.styles['PriceDisplay']))
|
|
||||||
story.append(Spacer(1, 60))
|
|
||||||
|
|
||||||
# Property Stats
|
|
||||||
stats_data = [
|
|
||||||
['BEDROOMS', 'BATHROOMS', 'AREA', 'LOCATION'],
|
|
||||||
[
|
|
||||||
data.get('bedrooms', 'N/A'),
|
|
||||||
data.get('bathrooms', 'N/A'),
|
|
||||||
f"{data.get('area', 'N/A')} sq ft",
|
|
||||||
data.get('location', 'N/A')
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
stats_table = Table(stats_data, colWidths=[1.8*inch, 1.8*inch, 1.8*inch, 1.8*inch])
|
|
||||||
stats_table.setStyle(TableStyle([
|
|
||||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#2c1810')),
|
|
||||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
||||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
||||||
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
|
|
||||||
('FONTSIZE', (0, 0), (-1, 0), 16),
|
|
||||||
('FONTSIZE', (0, 1), (-1, 1), 18),
|
|
||||||
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
|
|
||||||
('GRID', (0, 0), (-1, -1), 1, HexColor('#2c1810')),
|
|
||||||
('ROUNDEDCORNERS', [15, 15, 15, 15])
|
|
||||||
]))
|
|
||||||
|
|
||||||
story.append(stats_table)
|
|
||||||
story.append(Spacer(1, 60))
|
|
||||||
|
|
||||||
# Footer Text
|
|
||||||
story.append(Paragraph("EXCLUSIVE • PRESTIGIOUS • SOPHISTICATED", self.styles['PremiumSubtitle']))
|
|
||||||
story.append(Paragraph("Where luxury meets lifestyle", self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_property_overview(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create property overview page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Page Title
|
|
||||||
story.append(Paragraph("PROPERTY OVERVIEW", self.styles['SectionHeader']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Description
|
|
||||||
if data.get('description'):
|
|
||||||
story.append(Paragraph("ABOUT THIS PROPERTY", self.styles['FeatureText']))
|
|
||||||
story.append(Paragraph(data['description'], self.styles['ContentText']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Property Highlights
|
|
||||||
story.append(Paragraph("PROPERTY HIGHLIGHTS", self.styles['FeatureText']))
|
|
||||||
highlights = [
|
|
||||||
"• Premium finishes throughout",
|
|
||||||
"• High-end appliances and fixtures",
|
|
||||||
"• Smart home technology integration",
|
|
||||||
"• Energy-efficient design",
|
|
||||||
"• Premium security systems",
|
|
||||||
"• Landscaped gardens and outdoor spaces"
|
|
||||||
]
|
|
||||||
|
|
||||||
for highlight in highlights:
|
|
||||||
story.append(Paragraph(highlight, self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Additional Features
|
|
||||||
story.append(Paragraph("ADDITIONAL FEATURES", self.styles['FeatureText']))
|
|
||||||
additional_features = [
|
|
||||||
"• Premium finishes throughout",
|
|
||||||
"• Premium flooring materials",
|
|
||||||
"• Designer lighting fixtures",
|
|
||||||
"• High-quality windows and doors",
|
|
||||||
"• Advanced HVAC systems",
|
|
||||||
"• Premium insulation and soundproofing"
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in additional_features:
|
|
||||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_features_page(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create features and amenities page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Page Title
|
|
||||||
story.append(Paragraph("FEATURES & AMENITIES", self.styles['SectionHeader']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Interior Features
|
|
||||||
story.append(Paragraph("INTERIOR FEATURES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
interior_features = [
|
|
||||||
"• Master suite with walk-in closet",
|
|
||||||
"• En-suite bathrooms with premium fixtures",
|
|
||||||
"• Open-concept living areas",
|
|
||||||
"• Gourmet kitchen with island",
|
|
||||||
"• Formal dining room",
|
|
||||||
"• Home office/study",
|
|
||||||
"• Media room/home theater",
|
|
||||||
"• Wine cellar/storage"
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in interior_features:
|
|
||||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Exterior Features
|
|
||||||
story.append(Paragraph("EXTERIOR FEATURES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
exterior_features = [
|
|
||||||
"• Private swimming pool",
|
|
||||||
"• Outdoor kitchen and dining area",
|
|
||||||
"• Landscaped gardens",
|
|
||||||
"• Private parking/garage",
|
|
||||||
"• Security gate and fencing",
|
|
||||||
"• Outdoor entertainment areas",
|
|
||||||
"• Garden sheds/storage",
|
|
||||||
"• Professional landscaping"
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in exterior_features:
|
|
||||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_investment_page(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create investment and location page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Page Title
|
|
||||||
story.append(Paragraph("INVESTMENT & LOCATION", self.styles['SectionHeader']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Location Benefits
|
|
||||||
story.append(Paragraph("LOCATION BENEFITS", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
location_benefits = [
|
|
||||||
"• Prime location in prestigious area",
|
|
||||||
"• Easy access to major highways",
|
|
||||||
"• Close to shopping and dining",
|
|
||||||
"• Excellent schools nearby",
|
|
||||||
"• Public transportation access",
|
|
||||||
"• Healthcare facilities nearby",
|
|
||||||
"• Recreational facilities close by",
|
|
||||||
"• High appreciation potential"
|
|
||||||
]
|
|
||||||
|
|
||||||
for benefit in location_benefits:
|
|
||||||
story.append(Paragraph(benefit, self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Investment Highlights
|
|
||||||
story.append(Paragraph("INVESTMENT HIGHLIGHTS", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
investment_highlights = [
|
|
||||||
"• Strong rental yield potential",
|
|
||||||
"• High capital appreciation",
|
|
||||||
"• Low maintenance costs",
|
|
||||||
"• Premium tenant attraction",
|
|
||||||
"• Stable market conditions",
|
|
||||||
"• Excellent resale value",
|
|
||||||
"• Tax benefits available",
|
|
||||||
"• Professional property management"
|
|
||||||
]
|
|
||||||
|
|
||||||
for highlight in investment_highlights:
|
|
||||||
story.append(Paragraph(highlight, self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 40))
|
|
||||||
|
|
||||||
# Contact Information
|
|
||||||
story.append(Paragraph("CONTACT US", self.styles['FeatureText']))
|
|
||||||
story.append(Paragraph("For more information about this exclusive property,", self.styles['ContentText']))
|
|
||||||
story.append(Paragraph("please contact our luxury property specialists.", self.styles['ContentText']))
|
|
||||||
story.append(Spacer(1, 20))
|
|
||||||
story.append(Paragraph("LUXURY REAL ESTATE", self.styles['FeatureText']))
|
|
||||||
story.append(Paragraph("Premium Property Solutions", self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_luxury_header_footer(self, canvas_obj, page_num: int, template_name: str):
|
|
||||||
"""Create luxury header and footer"""
|
|
||||||
# Header
|
|
||||||
canvas_obj.setFillColor(HexColor('#2c1810'))
|
|
||||||
canvas_obj.setFont("Helvetica-Bold", 18)
|
|
||||||
canvas_obj.drawString(50, A4[1] - 40, "LUXURY REAL ESTATE")
|
|
||||||
|
|
||||||
canvas_obj.setFont("Helvetica", 14)
|
|
||||||
canvas_obj.drawString(50, A4[1] - 60, "Premium Property Brochure")
|
|
||||||
|
|
||||||
# Template indicator
|
|
||||||
canvas_obj.setFillColor(HexColor('#8b4513'))
|
|
||||||
canvas_obj.setFont("Helvetica-Bold", 16)
|
|
||||||
canvas_obj.drawRightString(A4[0] - 50, A4[1] - 40, template_name)
|
|
||||||
|
|
||||||
# Footer
|
|
||||||
canvas_obj.setFillColor(HexColor('#8b4513'))
|
|
||||||
canvas_obj.setFont("Helvetica", 12)
|
|
||||||
canvas_obj.drawCentredString(A4[0]/2, 35, f"Generated on {datetime.now().strftime('%B %d, %Y')}")
|
|
||||||
canvas_obj.drawCentredString(A4[0]/2, 20, "Luxury Real Estate - Premium Property Solutions")
|
|
||||||
|
|
||||||
# Page number
|
|
||||||
canvas_obj.drawRightString(A4[0] - 50, 20, f"Page {page_num}")
|
|
||||||
|
|
||||||
def create_modern_apartment_template(self, data: Dict[str, Any], output_path: str) -> str:
|
|
||||||
"""Create modern apartment template with contemporary design"""
|
|
||||||
doc = SimpleDocTemplate(
|
|
||||||
output_path,
|
|
||||||
pagesize=A4,
|
|
||||||
rightMargin=0.4*cm,
|
|
||||||
leftMargin=0.4*cm,
|
|
||||||
topMargin=0.4*cm,
|
|
||||||
bottomMargin=0.4*cm
|
|
||||||
)
|
|
||||||
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Page 1: Modern Cover
|
|
||||||
story.extend(self._create_modern_cover(data))
|
|
||||||
story.append(PageBreak())
|
|
||||||
|
|
||||||
# Page 2: Modern Features
|
|
||||||
story.extend(self._create_modern_features(data))
|
|
||||||
story.append(PageBreak())
|
|
||||||
|
|
||||||
# Page 3: Modern Amenities
|
|
||||||
story.extend(self._create_modern_amenities(data))
|
|
||||||
|
|
||||||
# Build PDF
|
|
||||||
doc.build(story, onFirstPage=lambda c, d: self._create_modern_header_footer(c, 1, "MODERN APARTMENT"),
|
|
||||||
onLaterPages=lambda c, d: self._create_modern_header_footer(c, d.page, "MODERN APARTMENT"))
|
|
||||||
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
def _create_modern_cover(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create modern cover page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
# Main Title
|
|
||||||
story.append(Paragraph("THE MODERN COLLECTION", self.styles['UltraTitle']))
|
|
||||||
story.append(Spacer(1, 35))
|
|
||||||
|
|
||||||
# Property Name
|
|
||||||
story.append(Paragraph(f"{data.get('propertyName', 'Modern Apartment')}", self.styles['UltraTitle']))
|
|
||||||
story.append(Spacer(1, 25))
|
|
||||||
|
|
||||||
# Location
|
|
||||||
story.append(Paragraph(f"Located in {data.get('location', 'Dubai')}", self.styles['PremiumSubtitle']))
|
|
||||||
story.append(Spacer(1, 45))
|
|
||||||
|
|
||||||
# Price
|
|
||||||
story.append(Paragraph("INVESTMENT VALUE", self.styles['SectionHeader']))
|
|
||||||
story.append(Paragraph(f"AED {data.get('price', 'N/A')}", self.styles['PriceDisplay']))
|
|
||||||
story.append(Spacer(1, 50))
|
|
||||||
|
|
||||||
# Modern Stats
|
|
||||||
stats_data = [
|
|
||||||
['BEDROOMS', 'BATHROOMS', 'AREA', 'LOCATION'],
|
|
||||||
[
|
|
||||||
data.get('bedrooms', 'N/A'),
|
|
||||||
data.get('bathrooms', 'N/A'),
|
|
||||||
f"{data.get('area', 'N/A')} sq ft",
|
|
||||||
data.get('location', 'N/A')
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
stats_table = Table(stats_data, colWidths=[1.8*inch, 1.8*inch, 1.8*inch, 1.8*inch])
|
|
||||||
stats_table.setStyle(TableStyle([
|
|
||||||
('BACKGROUND', (0, 0), (-1, 0), HexColor('#1e3a8a')),
|
|
||||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
||||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
||||||
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
|
|
||||||
('FONTSIZE', (0, 0), (-1, 0), 16),
|
|
||||||
('FONTSIZE', (0, 1), (-1, 1), 18),
|
|
||||||
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
|
|
||||||
('GRID', (0, 0), (-1, -1), 1, HexColor('#1e3a8a')),
|
|
||||||
('ROUNDEDCORNERS', [15, 15, 15, 15])
|
|
||||||
]))
|
|
||||||
|
|
||||||
story.append(stats_table)
|
|
||||||
story.append(Spacer(1, 50))
|
|
||||||
|
|
||||||
# Footer
|
|
||||||
story.append(Paragraph("CONTEMPORARY • ELEGANT • URBAN", self.styles['PremiumSubtitle']))
|
|
||||||
story.append(Paragraph("Experience the future of urban living", self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_modern_features(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create modern features page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
story.append(Paragraph("MODERN FEATURES", self.styles['SectionHeader']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Design Features
|
|
||||||
story.append(Paragraph("DESIGN FEATURES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
design_features = [
|
|
||||||
"• Open-concept floor plan",
|
|
||||||
"• Floor-to-ceiling windows",
|
|
||||||
"• High ceilings",
|
|
||||||
"• Modern minimalist design",
|
|
||||||
"• Smart home integration",
|
|
||||||
"• Energy-efficient appliances",
|
|
||||||
"• Premium materials and finishes",
|
|
||||||
"• Custom lighting design"
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in design_features:
|
|
||||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Technology Features
|
|
||||||
story.append(Paragraph("TECHNOLOGY FEATURES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
tech_features = [
|
|
||||||
"• Smart home automation",
|
|
||||||
"• High-speed internet",
|
|
||||||
"• Security camera systems",
|
|
||||||
"• Digital door locks",
|
|
||||||
"• Climate control systems",
|
|
||||||
"• Entertainment systems",
|
|
||||||
"• Mobile app control",
|
|
||||||
"• Energy monitoring"
|
|
||||||
]
|
|
||||||
|
|
||||||
for feature in tech_features:
|
|
||||||
story.append(Paragraph(feature, self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_modern_amenities(self, data: Dict[str, Any]) -> List:
|
|
||||||
"""Create modern amenities page"""
|
|
||||||
story = []
|
|
||||||
|
|
||||||
story.append(Paragraph("MODERN AMENITIES", self.styles['SectionHeader']))
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Building Amenities
|
|
||||||
story.append(Paragraph("BUILDING AMENITIES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
building_amenities = [
|
|
||||||
"• Rooftop swimming pool",
|
|
||||||
"• Fitness center with latest equipment",
|
|
||||||
"• Co-working spaces",
|
|
||||||
"• Rooftop terrace and gardens",
|
|
||||||
"• Concierge services",
|
|
||||||
"• Package delivery lockers",
|
|
||||||
"• Bike storage",
|
|
||||||
"• Electric vehicle charging"
|
|
||||||
]
|
|
||||||
|
|
||||||
for amenity in building_amenities:
|
|
||||||
story.append(Paragraph(f"🏢 {amenity}", self.styles['ContentText']))
|
|
||||||
|
|
||||||
story.append(Spacer(1, 30))
|
|
||||||
|
|
||||||
# Lifestyle Amenities
|
|
||||||
story.append(Paragraph("LIFESTYLE AMENITIES", self.styles['FeatureText']))
|
|
||||||
story.append(Spacer(1, 15))
|
|
||||||
|
|
||||||
lifestyle_amenities = [
|
|
||||||
"• Community lounge areas",
|
|
||||||
"• Outdoor dining spaces",
|
|
||||||
"• Children's play areas",
|
|
||||||
"• Pet-friendly facilities",
|
|
||||||
"• Guest parking",
|
|
||||||
"• 24/7 security",
|
|
||||||
"• Maintenance services",
|
|
||||||
"• Community events"
|
|
||||||
]
|
|
||||||
|
|
||||||
for amenity in lifestyle_amenities:
|
|
||||||
story.append(Paragraph(f"🌟 {amenity}", self.styles['ContentText']))
|
|
||||||
|
|
||||||
return story
|
|
||||||
|
|
||||||
def _create_modern_header_footer(self, canvas_obj, page_num: int, template_name: str):
|
|
||||||
"""Create modern header and footer"""
|
|
||||||
# Header
|
|
||||||
canvas_obj.setFillColor(HexColor('#1e3a8a'))
|
|
||||||
canvas_obj.setFont("Helvetica-Bold", 18)
|
|
||||||
canvas_obj.drawString(50, A4[1] - 40, "MODERN REAL ESTATE")
|
|
||||||
|
|
||||||
canvas_obj.setFont("Helvetica", 14)
|
|
||||||
canvas_obj.drawString(50, A4[1] - 60, "Contemporary Property Solutions")
|
|
||||||
|
|
||||||
# Template indicator
|
|
||||||
canvas_obj.setFillColor(HexColor('#3b82f6'))
|
|
||||||
canvas_obj.setFont("Helvetica-Bold", 16)
|
|
||||||
canvas_obj.drawRightString(A4[0] - 50, A4[1] - 40, template_name)
|
|
||||||
|
|
||||||
# Footer
|
|
||||||
canvas_obj.setFillColor(HexColor('#475569'))
|
|
||||||
canvas_obj.setFont("Helvetica", 12)
|
|
||||||
canvas_obj.drawCentredString(A4[0]/2, 35, f"Generated on {datetime.now().strftime('%B %d, %Y')}")
|
|
||||||
canvas_obj.drawCentredString(A4[0]/2, 20, "Modern Real Estate - Contemporary Living")
|
|
||||||
|
|
||||||
# Page number
|
|
||||||
canvas_obj.drawRightString(A4[0] - 50, 20, f"Page {page_num}")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test the advanced templates"""
|
|
||||||
templates = AdvancedPropertyTemplates()
|
|
||||||
|
|
||||||
# Sample data
|
|
||||||
sample_data = {
|
|
||||||
'propertyName': 'Luxury Marina Villa',
|
|
||||||
'propertyType': 'Villa',
|
|
||||||
'location': 'Dubai Marina',
|
|
||||||
'price': '5,500,000',
|
|
||||||
'bedrooms': '5',
|
|
||||||
'bathrooms': '6',
|
|
||||||
'area': '4,200',
|
|
||||||
'description': 'Stunning luxury villa with panoramic marina views, premium finishes, and exclusive amenities.',
|
|
||||||
'amenities': ['Private Pool', 'Gym', 'Security', 'Garden', 'Garage', 'Smart Home']
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test luxury villa template
|
|
||||||
try:
|
|
||||||
result = templates.create_luxury_villa_template(sample_data, 'luxury_villa_brochure.pdf')
|
|
||||||
print(f"Luxury villa PDF generated: {result}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {str(e)}")
|
|
||||||
|
|
||||||
# Test modern apartment template
|
|
||||||
try:
|
|
||||||
result = templates.create_modern_apartment_template(sample_data, 'modern_apartment_brochure.pdf')
|
|
||||||
print(f"Modern apartment PDF generated: {result}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {str(e)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,672 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
FastAPI server for Property PDF Generator
|
|
||||||
Provides REST API endpoints for generating high-quality property brochures
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
import uvicorn
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
from property_pdf_generator import PropertyPDFGenerator
|
|
||||||
|
|
||||||
# Initialize FastAPI app
|
|
||||||
app = FastAPI(
|
|
||||||
title="Property PDF Generator API",
|
|
||||||
description="High-quality property brochure PDF generation service",
|
|
||||||
version="1.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add CORS middleware
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"], # In production, restrict to specific domains
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize PDF generator
|
|
||||||
pdf_generator = PropertyPDFGenerator()
|
|
||||||
|
|
||||||
# Store generated PDFs temporarily (in production, use proper file storage)
|
|
||||||
generated_pdfs = {}
|
|
||||||
|
|
||||||
# Cleanup old PDFs periodically
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
|
|
||||||
async def cleanup_old_pdfs():
|
|
||||||
"""Clean up old PDF files to prevent disk space issues"""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
current_time = time.time()
|
|
||||||
expired_files = []
|
|
||||||
|
|
||||||
for filename, filepath in generated_pdfs.items():
|
|
||||||
# Remove files older than 1 hour
|
|
||||||
if current_time - os.path.getctime(filepath) > 3600:
|
|
||||||
try:
|
|
||||||
if os.path.exists(filepath):
|
|
||||||
os.unlink(filepath)
|
|
||||||
expired_files.append(filename)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Remove expired entries from dictionary
|
|
||||||
for filename in expired_files:
|
|
||||||
generated_pdfs.pop(filename, None)
|
|
||||||
|
|
||||||
# Run cleanup every 30 minutes
|
|
||||||
await asyncio.sleep(1800)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# Continue cleanup even if there's an error
|
|
||||||
await asyncio.sleep(1800)
|
|
||||||
|
|
||||||
# Start cleanup task
|
|
||||||
cleanup_task = None
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
|
||||||
"""Start cleanup task on startup"""
|
|
||||||
global cleanup_task
|
|
||||||
cleanup_task = asyncio.create_task(cleanup_old_pdfs())
|
|
||||||
|
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
|
||||||
"""Stop cleanup task and clean up files on shutdown"""
|
|
||||||
global cleanup_task
|
|
||||||
if cleanup_task:
|
|
||||||
cleanup_task.cancel()
|
|
||||||
|
|
||||||
# Clean up all stored PDFs
|
|
||||||
for filepath in generated_pdfs.values():
|
|
||||||
try:
|
|
||||||
if os.path.exists(filepath):
|
|
||||||
os.unlink(filepath)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
generated_pdfs.clear()
|
|
||||||
|
|
||||||
# Data models
|
|
||||||
class PropertyData(BaseModel):
|
|
||||||
"""Enhanced property information model with market data and analytics"""
|
|
||||||
# Template & Layout
|
|
||||||
template: str = Field(..., description="Template name to use")
|
|
||||||
layout: Optional[str] = Field(None, description="Layout configuration for custom templates")
|
|
||||||
|
|
||||||
# Basic Property Information
|
|
||||||
propertyName: str = Field(..., description="Name of the property")
|
|
||||||
propertyType: str = Field(..., description="Type of property")
|
|
||||||
location: str = Field(..., description="Property location")
|
|
||||||
price: str = Field(..., description="Property price")
|
|
||||||
bedrooms: str = Field(..., description="Number of bedrooms")
|
|
||||||
bathrooms: str = Field(..., description="Number of bathrooms")
|
|
||||||
area: str = Field(..., description="Property area in sq ft")
|
|
||||||
description: Optional[str] = Field(None, description="Property description")
|
|
||||||
amenities: List[str] = Field(default=[], description="List of amenities")
|
|
||||||
images: List[str] = Field(default=[], description="Base64 encoded images")
|
|
||||||
imageNames: List[str] = Field(default=[], description="Room names for each image")
|
|
||||||
|
|
||||||
# Market Data & Analytics
|
|
||||||
marketTrend: Optional[str] = Field(None, description="Market trend (rising/stable/declining)")
|
|
||||||
roiPotential: Optional[str] = Field(None, description="Expected ROI percentage")
|
|
||||||
avgPricePerSqft: Optional[str] = Field(None, description="Average market price per sq ft")
|
|
||||||
marketDemand: Optional[str] = Field(None, description="Market demand level (high/medium/low)")
|
|
||||||
locationAdvantages: Optional[str] = Field(None, description="Location benefits and advantages")
|
|
||||||
|
|
||||||
# Investment Information
|
|
||||||
investmentType: Optional[str] = Field(None, description="Investment type (buy-to-live/rent/sell)")
|
|
||||||
rentalYield: Optional[str] = Field(None, description="Expected rental yield percentage")
|
|
||||||
investmentHighlights: Optional[str] = Field(None, description="Key investment benefits")
|
|
||||||
|
|
||||||
# Content Modules
|
|
||||||
contentModules: List[str] = Field(default=[], description="Selected content modules")
|
|
||||||
additionalContent: Optional[str] = Field(None, description="Additional custom content")
|
|
||||||
|
|
||||||
# Customization Options
|
|
||||||
headerStyle: Optional[str] = Field("modern", description="Header style (modern/classic/luxury)")
|
|
||||||
colorScheme: Optional[str] = Field("blue", description="Color scheme (blue/green/purple/gold)")
|
|
||||||
fontStyle: Optional[str] = Field("sans-serif", description="Font style (sans-serif/serif/modern)")
|
|
||||||
|
|
||||||
class TemplateInfo(BaseModel):
|
|
||||||
"""Template information model"""
|
|
||||||
name: str
|
|
||||||
display_name: str
|
|
||||||
description: str
|
|
||||||
category: str
|
|
||||||
preview_color: str
|
|
||||||
|
|
||||||
class GeneratePDFRequest(BaseModel):
|
|
||||||
"""PDF generation request model"""
|
|
||||||
property_data: PropertyData
|
|
||||||
template_name: str
|
|
||||||
|
|
||||||
class GeneratePDFResponse(BaseModel):
|
|
||||||
"""PDF generation response model"""
|
|
||||||
success: bool
|
|
||||||
message: str
|
|
||||||
pdf_url: Optional[str] = None
|
|
||||||
error: Optional[str] = None
|
|
||||||
|
|
||||||
# Available templates
|
|
||||||
AVAILABLE_TEMPLATES = {
|
|
||||||
# Professional Templates
|
|
||||||
"professional-1pager": {
|
|
||||||
"name": "professional-1pager",
|
|
||||||
"display_name": "Professional 1-Pager",
|
|
||||||
"description": "Compact single-page brochure with 2x2 image grid",
|
|
||||||
"category": "Professional",
|
|
||||||
"preview_color": "#667eea",
|
|
||||||
"pages": 1,
|
|
||||||
"image_grid": "2x2"
|
|
||||||
},
|
|
||||||
"professional-3pager": {
|
|
||||||
"name": "professional-3pager",
|
|
||||||
"display_name": "Professional 3-Pager",
|
|
||||||
"description": "Comprehensive three-page brochure with detailed analysis",
|
|
||||||
"category": "Professional",
|
|
||||||
"preview_color": "#1e3a8a",
|
|
||||||
"pages": 3,
|
|
||||||
"image_grid": "4x4"
|
|
||||||
},
|
|
||||||
"professional-5pager": {
|
|
||||||
"name": "professional-5pager",
|
|
||||||
"display_name": "Professional 5-Pager",
|
|
||||||
"description": "Premium five-page brochure with comprehensive analysis",
|
|
||||||
"category": "Professional",
|
|
||||||
"preview_color": "#059669",
|
|
||||||
"pages": 5,
|
|
||||||
"image_grid": "6x6"
|
|
||||||
},
|
|
||||||
|
|
||||||
# Luxury Templates
|
|
||||||
"luxury-villa": {
|
|
||||||
"name": "luxury-villa",
|
|
||||||
"display_name": "Luxury Villa Brochure",
|
|
||||||
"description": "Exclusive villa template with premium styling",
|
|
||||||
"category": "Luxury",
|
|
||||||
"preview_color": "#2c1810",
|
|
||||||
"pages": 4,
|
|
||||||
"image_grid": "5x4"
|
|
||||||
},
|
|
||||||
"dubai-penthouse": {
|
|
||||||
"name": "dubai-penthouse",
|
|
||||||
"display_name": "Dubai Penthouse",
|
|
||||||
"description": "Dubai-specific luxury penthouse template",
|
|
||||||
"category": "Luxury",
|
|
||||||
"preview_color": "#dc2626",
|
|
||||||
"pages": 6,
|
|
||||||
"image_grid": "6x5"
|
|
||||||
},
|
|
||||||
|
|
||||||
# Modern Templates
|
|
||||||
"modern-apartment": {
|
|
||||||
"name": "modern-apartment",
|
|
||||||
"display_name": "Modern Apartment",
|
|
||||||
"description": "Contemporary apartment template with clean lines",
|
|
||||||
"category": "Modern",
|
|
||||||
"preview_color": "#7c3aed",
|
|
||||||
"pages": 3,
|
|
||||||
"image_grid": "3x5"
|
|
||||||
},
|
|
||||||
|
|
||||||
# Custom Template
|
|
||||||
"custom": {
|
|
||||||
"name": "custom",
|
|
||||||
"display_name": "Custom Template",
|
|
||||||
"description": "Build your own template with custom layouts",
|
|
||||||
"category": "Custom",
|
|
||||||
"preview_color": "#1f2937",
|
|
||||||
"pages": "flexible",
|
|
||||||
"image_grid": "configurable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Root endpoint"""
|
|
||||||
return {
|
|
||||||
"message": "Property PDF Generator API",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"status": "running",
|
|
||||||
"endpoints": {
|
|
||||||
"templates": "/api/templates",
|
|
||||||
"preview": "/api/preview",
|
|
||||||
"generate": "/api/generate-pdf",
|
|
||||||
"health": "/api/health"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.get("/api/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"service": "Property PDF Generator"
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.get("/api/templates", response_model=List[TemplateInfo])
|
|
||||||
async def get_templates():
|
|
||||||
"""Get all available templates"""
|
|
||||||
try:
|
|
||||||
templates = []
|
|
||||||
for template_id, template_info in AVAILABLE_TEMPLATES.items():
|
|
||||||
templates.append(TemplateInfo(**template_info))
|
|
||||||
return templates
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error fetching templates: {str(e)}")
|
|
||||||
|
|
||||||
@app.get("/api/templates/{template_name}")
|
|
||||||
async def get_template(template_name: str):
|
|
||||||
"""Get specific template information"""
|
|
||||||
try:
|
|
||||||
if template_name not in AVAILABLE_TEMPLATES:
|
|
||||||
raise HTTPException(status_code=404, detail="Template not found")
|
|
||||||
|
|
||||||
return AVAILABLE_TEMPLATES[template_name]
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error fetching template: {str(e)}")
|
|
||||||
|
|
||||||
@app.post("/api/preview")
|
|
||||||
async def generate_preview(property_data: PropertyData):
|
|
||||||
"""Generate a preview of the property brochure"""
|
|
||||||
try:
|
|
||||||
# Validate template
|
|
||||||
if property_data.template not in AVAILABLE_TEMPLATES:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid template selected")
|
|
||||||
|
|
||||||
# Generate preview content
|
|
||||||
preview_content = {
|
|
||||||
"template": property_data.template,
|
|
||||||
"template_info": AVAILABLE_TEMPLATES[property_data.template],
|
|
||||||
"property_data": property_data.dict(),
|
|
||||||
"preview_html": generate_preview_html(property_data),
|
|
||||||
"generated_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"preview": preview_content,
|
|
||||||
"message": "Preview generated successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
|
|
||||||
|
|
||||||
# Add a new endpoint for Salesforce compatibility
|
|
||||||
@app.post("/api/preview-simple")
|
|
||||||
async def generate_preview_simple(request_data: dict):
|
|
||||||
"""Generate a preview using the simpler format sent from Salesforce"""
|
|
||||||
try:
|
|
||||||
# Extract template and property data from the request
|
|
||||||
template = request_data.get('template')
|
|
||||||
if not template or template not in AVAILABLE_TEMPLATES:
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid template selected")
|
|
||||||
|
|
||||||
# Create a PropertyData object from the simple request
|
|
||||||
property_data = PropertyData(
|
|
||||||
template=template,
|
|
||||||
propertyName=request_data.get('propertyName', 'Unknown Property'),
|
|
||||||
propertyType=request_data.get('propertyType', 'Unknown'),
|
|
||||||
location=request_data.get('location', 'Unknown Location'),
|
|
||||||
price=request_data.get('price', 'Price on Request'),
|
|
||||||
bedrooms=request_data.get('bedrooms', 'N/A'),
|
|
||||||
bathrooms=request_data.get('bathrooms', 'N/A'),
|
|
||||||
area=request_data.get('area', 'N/A'),
|
|
||||||
description=request_data.get('description', 'No description available')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate preview content
|
|
||||||
preview_content = {
|
|
||||||
"template": property_data.template,
|
|
||||||
"template_info": AVAILABLE_TEMPLATES[property_data.template],
|
|
||||||
"property_data": property_data.dict(),
|
|
||||||
"preview_html": generate_preview_html(property_data),
|
|
||||||
"generated_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"preview": preview_content,
|
|
||||||
"message": "Preview generated successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
|
|
||||||
|
|
||||||
@app.post("/api/generate-pdf", response_model=GeneratePDFResponse)
|
|
||||||
async def generate_pdf(request: GeneratePDFRequest):
|
|
||||||
"""Generate a PDF property brochure"""
|
|
||||||
try:
|
|
||||||
# Validate template
|
|
||||||
if request.template_name not in AVAILABLE_TEMPLATES:
|
|
||||||
return GeneratePDFResponse(
|
|
||||||
success=False,
|
|
||||||
message="Invalid template selected",
|
|
||||||
error="Template not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate property data
|
|
||||||
if not request.property_data.propertyName or not request.property_data.propertyType:
|
|
||||||
return GeneratePDFResponse(
|
|
||||||
success=False,
|
|
||||||
message="Missing required property information",
|
|
||||||
error="Property name and type are required"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create temporary file for PDF
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
|
|
||||||
pdf_path = tmp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Generate PDF
|
|
||||||
result_path = pdf_generator.generate_property_pdf(
|
|
||||||
request.property_data.dict(),
|
|
||||||
request.template_name,
|
|
||||||
pdf_path
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract filename from the result path
|
|
||||||
filename = os.path.basename(result_path)
|
|
||||||
|
|
||||||
# Store the generated PDF path
|
|
||||||
generated_pdfs[filename] = pdf_path
|
|
||||||
|
|
||||||
# Return the download URL (fix: add /api/ prefix to match the endpoint)
|
|
||||||
pdf_url = f"/api/download-pdf/{filename}"
|
|
||||||
|
|
||||||
return GeneratePDFResponse(
|
|
||||||
success=True,
|
|
||||||
message="PDF generated successfully",
|
|
||||||
pdf_url=pdf_url,
|
|
||||||
error=None
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Clean up temporary file on error
|
|
||||||
if os.path.exists(pdf_path):
|
|
||||||
os.unlink(pdf_path)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return GeneratePDFResponse(
|
|
||||||
success=False,
|
|
||||||
message="Error generating PDF",
|
|
||||||
error=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/api/download-pdf/{filename}")
|
|
||||||
async def download_pdf(filename: str):
|
|
||||||
"""Download generated PDF file"""
|
|
||||||
try:
|
|
||||||
# Check if the PDF exists in our storage
|
|
||||||
if filename in generated_pdfs and os.path.exists(generated_pdfs[filename]):
|
|
||||||
pdf_path = generated_pdfs[filename]
|
|
||||||
|
|
||||||
# Return the actual generated PDF
|
|
||||||
return FileResponse(
|
|
||||||
pdf_path,
|
|
||||||
media_type='application/pdf',
|
|
||||||
filename=f"property_brochure_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Fallback: generate a demo PDF
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
|
|
||||||
demo_pdf_path = tmp_file.name
|
|
||||||
|
|
||||||
# Generate a demo PDF
|
|
||||||
demo_data = {
|
|
||||||
"propertyName": "Demo Property",
|
|
||||||
"propertyType": "Apartment",
|
|
||||||
"location": "Dubai",
|
|
||||||
"price": "1,500,000",
|
|
||||||
"bedrooms": "2",
|
|
||||||
"bathrooms": "2",
|
|
||||||
"area": "1,200",
|
|
||||||
"description": "This is a demo property brochure generated by the Property PDF Generator API.",
|
|
||||||
"amenities": ['Swimming Pool', 'Gym', 'Parking'],
|
|
||||||
"images": []
|
|
||||||
}
|
|
||||||
|
|
||||||
pdf_generator.generate_property_pdf(demo_data, "modern", demo_pdf_path)
|
|
||||||
|
|
||||||
# Return the demo file
|
|
||||||
return FileResponse(
|
|
||||||
demo_pdf_path,
|
|
||||||
media_type='application/pdf',
|
|
||||||
filename=f"demo_property_brochure.pdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error downloading PDF: {str(e)}")
|
|
||||||
|
|
||||||
def generate_preview_html(property_data: PropertyData) -> str:
|
|
||||||
"""Generate comprehensive HTML preview of the property brochure with all new data"""
|
|
||||||
template_info = AVAILABLE_TEMPLATES.get(property_data.template, {})
|
|
||||||
color = template_info.get('preview_color', '#333')
|
|
||||||
|
|
||||||
# Market trend styling
|
|
||||||
trend_color = {
|
|
||||||
'rising': '#28a745',
|
|
||||||
'stable': '#ffc107',
|
|
||||||
'declining': '#dc3545'
|
|
||||||
}.get(property_data.marketTrend, '#6c757d')
|
|
||||||
|
|
||||||
# Investment type styling
|
|
||||||
investment_color = {
|
|
||||||
'buy-to-live': '#007bff',
|
|
||||||
'buy-to-rent': '#28a745',
|
|
||||||
'buy-to-sell': '#ffc107'
|
|
||||||
}.get(property_data.investmentType, '#6c757d')
|
|
||||||
|
|
||||||
preview_html = f"""
|
|
||||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 15px;">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div style="text-align: center; margin-bottom: 40px; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<div style="background: {color}; color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px;">
|
|
||||||
<h1 style="margin: 0; font-size: 2.5rem; font-weight: 700;">
|
|
||||||
{property_data.propertyName}
|
|
||||||
</h1>
|
|
||||||
<p style="margin: 10px 0 0; font-size: 1.2rem; opacity: 0.9;">
|
|
||||||
{property_data.propertyType} in {property_data.location}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
|
|
||||||
<div style="background: #e3f2fd; padding: 15px; border-radius: 10px;">
|
|
||||||
<h3 style="margin: 0; color: #1976d2;">Template</h3>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('display_name', 'Unknown')}</p>
|
|
||||||
</div>
|
|
||||||
<div style="background: #e8f5e8; padding: 15px; border-radius: 10px;">
|
|
||||||
<h3 style="margin: 0; color: #388e3c;">Pages</h3>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('pages', 'N/A')}</p>
|
|
||||||
</div>
|
|
||||||
<div style="background: #fce4ec; padding: 15px; border-radius: 10px;">
|
|
||||||
<h3 style="margin: 0; color: #c2185b;">Layout</h3>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: 600;">{property_data.layout or template_info.get('image_grid', 'Default')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Property Details Grid -->
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Details</h2>
|
|
||||||
<div style="display: grid; gap: 12px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Type:</strong> <span>{property_data.propertyType}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Location:</strong> <span>{property_data.location}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Price:</strong> <span style="color: {color}; font-weight: 700;">AED {property_data.price}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Bedrooms:</strong> <span>{property_data.bedrooms}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Bathrooms:</strong> <span>{property_data.bathrooms}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
||||||
<strong>Area:</strong> <span>{property_data.area} sq ft</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Market Analytics</h2>
|
|
||||||
<div style="display: grid; gap: 12px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Market Trend:</strong>
|
|
||||||
<span style="color: {trend_color}; font-weight: 600;">
|
|
||||||
{property_data.marketTrend.title() if property_data.marketTrend else 'N/A'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>ROI Potential:</strong> <span>{property_data.roiPotential}%</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Avg Price/sq ft:</strong> <span>AED {property_data.avgPricePerSqft}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
||||||
<strong>Market Demand:</strong>
|
|
||||||
<span style="color: {trend_color}; font-weight: 600;">
|
|
||||||
{property_data.marketDemand.title() if property_data.marketDemand else 'N/A'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
''' if any([property_data.marketTrend, property_data.roiPotential, property_data.avgPricePerSqft, property_data.marketDemand]) else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Details</h2>
|
|
||||||
<div style="display: grid; gap: 12px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
|
|
||||||
<strong>Investment Type:</strong>
|
|
||||||
<span style="color: {investment_color}; font-weight: 600;">
|
|
||||||
{property_data.investmentType.replace('-', ' ').title() if property_data.investmentType else 'N/A'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
|
|
||||||
<strong>Rental Yield:</strong> <span>{property_data.rentalYield}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
''' if any([property_data.investmentType, property_data.rentalYield]) else ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Description</h2>
|
|
||||||
<p style="line-height: 1.6; color: #555;">{property_data.description}</p>
|
|
||||||
</div>
|
|
||||||
''' if property_data.description else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Location Advantages</h2>
|
|
||||||
<p style="line-height: 1.6; color: #555;">{property_data.locationAdvantages}</p>
|
|
||||||
</div>
|
|
||||||
''' if property_data.locationAdvantages else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Highlights</h2>
|
|
||||||
<p style="line-height: 1.6; color: #555;">{property_data.investmentHighlights}</p>
|
|
||||||
</div>
|
|
||||||
''' if property_data.investmentHighlights else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Amenities</h2>
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
||||||
{''.join([f'<span style="background: {color}; color: white; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{amenity}</span>' for amenity in property_data.amenities])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
''' if property_data.amenities else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Content Modules</h2>
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
|
||||||
{''.join([f'<span style="background: #e3f2fd; color: #1976d2; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{module.replace("-", " ").title()}</span>' for module in property_data.contentModules])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
''' if property_data.contentModules else ''}
|
|
||||||
|
|
||||||
{f'''
|
|
||||||
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
|
|
||||||
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Additional Content</h2>
|
|
||||||
<p style="line-height: 1.6; color: #555;">{property_data.additionalContent}</p>
|
|
||||||
</div>
|
|
||||||
''' if property_data.additionalContent else ''}
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div style="text-align: center; color: #999; font-style: italic; margin-top: 40px; padding: 20px; background: white; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
|
|
||||||
<h3 style="color: {color}; margin-bottom: 10px;">Preview Information</h3>
|
|
||||||
<p style="margin: 5px 0;">Template: {template_info.get('display_name', 'Unknown')}</p>
|
|
||||||
<p style="margin: 5px 0;">Pages: {template_info.get('pages', 'N/A')} | Layout: {property_data.layout or template_info.get('image_grid', 'Default')}</p>
|
|
||||||
<p style="margin: 15px 0 5px; font-weight: 600; color: #555;">This is a preview. The final PDF will be generated with professional styling and layouts.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return preview_html
|
|
||||||
|
|
||||||
@app.post("/api/upload-images")
|
|
||||||
async def upload_images(files: List[UploadFile] = File(...)):
|
|
||||||
"""Upload property images"""
|
|
||||||
try:
|
|
||||||
uploaded_images = []
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
if file.content_type.startswith('image/'):
|
|
||||||
# Read image content
|
|
||||||
content = await file.read()
|
|
||||||
|
|
||||||
# Convert to base64 for storage
|
|
||||||
import base64
|
|
||||||
base64_content = base64.b64encode(content).decode('utf-8')
|
|
||||||
|
|
||||||
uploaded_images.append({
|
|
||||||
"filename": file.filename,
|
|
||||||
"content_type": file.content_type,
|
|
||||||
"size": len(content),
|
|
||||||
"base64_data": f"data:{file.content_type};base64,{base64_content}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": f"Successfully uploaded {len(uploaded_images)} images",
|
|
||||||
"images": uploaded_images
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Error uploading images: {str(e)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Run the server
|
|
||||||
uvicorn.run(
|
|
||||||
"api_server:app",
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=8000,
|
|
||||||
reload=True,
|
|
||||||
log_level="info"
|
|
||||||
)
|
|
||||||
861
python-pdf-generator/app.py
Normal file
861
python-pdf-generator/app.py
Normal file
@ -0,0 +1,861 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Advanced HTML to PDF Generator API with Intelligent Content Analysis
|
||||||
|
Supports URLs, HTML files, HTML strings, and batch processing
|
||||||
|
Always uses A4 size for consistent output
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, request, send_file, jsonify
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from playwright.async_api import async_playwright, TimeoutError, Page
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Configure temp folder
|
||||||
|
TEMP_FOLDER = 'temp'
|
||||||
|
if not os.path.exists(TEMP_FOLDER):
|
||||||
|
os.makedirs(TEMP_FOLDER)
|
||||||
|
|
||||||
|
class HTMLPreprocessor:
|
||||||
|
"""Intelligently preprocesses HTML to remove spacing issues and optimize for PDF generation."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def preprocess_html(html_content: str) -> str:
|
||||||
|
"""
|
||||||
|
Dynamically analyze and fix spacing issues in HTML for perfect PDF generation.
|
||||||
|
"""
|
||||||
|
print("🔧 Preprocessing HTML for optimal PDF generation...")
|
||||||
|
|
||||||
|
# Step 1: Detect page elements and their structure
|
||||||
|
page_info = HTMLPreprocessor._analyze_page_structure(html_content)
|
||||||
|
|
||||||
|
# Step 2: Remove problematic spacing
|
||||||
|
html_content = HTMLPreprocessor._remove_spacing_issues(html_content, page_info)
|
||||||
|
|
||||||
|
# Step 3: Optimize for PDF generation
|
||||||
|
html_content = HTMLPreprocessor._optimize_for_pdf(html_content, page_info)
|
||||||
|
|
||||||
|
print(f"✅ HTML preprocessing completed - {page_info['page_count']} pages optimized")
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _analyze_page_structure(html_content: str) -> dict:
|
||||||
|
"""Analyze the HTML structure to understand page layout and spacing."""
|
||||||
|
|
||||||
|
# Detect page elements
|
||||||
|
page_selectors = [
|
||||||
|
r'class="[^"]*brochure-page[^"]*"',
|
||||||
|
r'class="[^"]*page[^"]*"',
|
||||||
|
r'class="[^"]*pdf-page[^"]*"',
|
||||||
|
r'class="[^"]*slide[^"]*"',
|
||||||
|
r'class="[^"]*section[^"]*"'
|
||||||
|
]
|
||||||
|
|
||||||
|
page_count = 0
|
||||||
|
page_elements = []
|
||||||
|
|
||||||
|
for selector in page_selectors:
|
||||||
|
matches = re.findall(selector, html_content, re.IGNORECASE)
|
||||||
|
if matches:
|
||||||
|
page_count = len(matches)
|
||||||
|
page_elements = matches
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no specific page elements found, look for A4-sized containers
|
||||||
|
if page_count == 0:
|
||||||
|
# Look for elements with A4-like dimensions in CSS
|
||||||
|
a4_patterns = [
|
||||||
|
r'width:\s*210mm',
|
||||||
|
r'height:\s*297mm',
|
||||||
|
r'width:\s*794px',
|
||||||
|
r'height:\s*1123px',
|
||||||
|
r'width:\s*8\.27in',
|
||||||
|
r'height:\s*11\.7in'
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in a4_patterns:
|
||||||
|
if re.search(pattern, html_content, re.IGNORECASE):
|
||||||
|
page_count = 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# Analyze body and container spacing
|
||||||
|
spacing_issues = HTMLPreprocessor._detect_spacing_issues(html_content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'page_count': page_count,
|
||||||
|
'page_elements': page_elements,
|
||||||
|
'spacing_issues': spacing_issues,
|
||||||
|
'has_flexbox': 'display: flex' in html_content,
|
||||||
|
'has_grid': 'display: grid' in html_content,
|
||||||
|
'has_padding': 'padding:' in html_content,
|
||||||
|
'has_margin': 'margin:' in html_content,
|
||||||
|
'has_gap': 'gap:' in html_content
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _detect_spacing_issues(html_content: str) -> dict:
|
||||||
|
"""Detect various types of spacing issues that affect PDF generation."""
|
||||||
|
|
||||||
|
issues = {
|
||||||
|
'body_padding': False,
|
||||||
|
'body_margin': False,
|
||||||
|
'body_gap': False,
|
||||||
|
'document_level_spacing': False,
|
||||||
|
'container_spacing': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for body-level spacing issues
|
||||||
|
if re.search(r'body\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['body_padding'] = True
|
||||||
|
|
||||||
|
if re.search(r'body\s*{[^}]*margin[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['body_margin'] = True
|
||||||
|
|
||||||
|
if re.search(r'body\s*{[^}]*gap[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['body_gap'] = True
|
||||||
|
|
||||||
|
# Check for document-level spacing
|
||||||
|
if re.search(r'html\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['document_level_spacing'] = True
|
||||||
|
|
||||||
|
if re.search(r'html\s*{[^}]*margin[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['document_level_spacing'] = True
|
||||||
|
|
||||||
|
# Check for container spacing
|
||||||
|
if re.search(r'\.container\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['container_spacing'] = True
|
||||||
|
|
||||||
|
if re.search(r'\.wrapper\s*{[^}]*padding[^}]*}', html_content, re.IGNORECASE):
|
||||||
|
issues['container_spacing'] = True
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _remove_spacing_issues(html_content: str, page_info: dict) -> str:
|
||||||
|
"""Remove problematic spacing while preserving internal page spacing."""
|
||||||
|
|
||||||
|
# Only remove document-level spacing, preserve internal spacing
|
||||||
|
if page_info['spacing_issues']['body_padding']:
|
||||||
|
html_content = re.sub(
|
||||||
|
r'(body\s*{[^}]*?)padding[^;]*;?([^}]*})',
|
||||||
|
r'\1\2',
|
||||||
|
html_content,
|
||||||
|
flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if page_info['spacing_issues']['body_margin']:
|
||||||
|
html_content = re.sub(
|
||||||
|
r'(body\s*{[^}]*?)margin[^;]*;?([^}]*})',
|
||||||
|
r'\1\2',
|
||||||
|
html_content,
|
||||||
|
flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if page_info['spacing_issues']['body_gap']:
|
||||||
|
html_content = re.sub(
|
||||||
|
r'(body\s*{[^}]*?)gap[^;]*;?([^}]*})',
|
||||||
|
r'\1\2',
|
||||||
|
html_content,
|
||||||
|
flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if page_info['spacing_issues']['document_level_spacing']:
|
||||||
|
html_content = re.sub(
|
||||||
|
r'(html\s*{[^}]*?)padding[^;]*;?([^}]*})',
|
||||||
|
r'\1\2',
|
||||||
|
html_content,
|
||||||
|
flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
html_content = re.sub(
|
||||||
|
r'(html\s*{[^}]*?)margin[^;]*;?([^}]*})',
|
||||||
|
r'\1\2',
|
||||||
|
html_content,
|
||||||
|
flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CSS to ensure continuous flow
|
||||||
|
continuous_flow_css = '''
|
||||||
|
/* Ensure continuous flow for PDF generation */
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve all internal page spacing and margins */
|
||||||
|
.page-layout, .p1-content-side, .p2-grid, .p3-main-content, .p4-info-grid {
|
||||||
|
/* Keep all internal spacing intact */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure no page breaks within content */
|
||||||
|
.brochure-page, .page, .pdf-page, .slide, .section {
|
||||||
|
page-break-after: auto;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve internal margins and padding */
|
||||||
|
* {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Insert the CSS after existing styles
|
||||||
|
if '</style>' in html_content:
|
||||||
|
html_content = html_content.replace('</style>', continuous_flow_css + '\n </style>')
|
||||||
|
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _optimize_for_pdf(html_content: str, page_info: dict) -> str:
|
||||||
|
"""Add PDF-specific optimizations while preserving internal spacing."""
|
||||||
|
|
||||||
|
pdf_optimizations = '''
|
||||||
|
/* PDF-specific optimizations - preserve internal spacing */
|
||||||
|
@media print {
|
||||||
|
/* Only remove document-level spacing, preserve internal spacing */
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve all internal page spacing and margins */
|
||||||
|
.page-layout {
|
||||||
|
padding: 70px !important; /* Keep internal page padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.p1-content-side {
|
||||||
|
padding: 70px 60px !important; /* Keep content padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure no page breaks within content */
|
||||||
|
.brochure-page, .page, .pdf-page {
|
||||||
|
page-break-after: auto !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure exact color rendering */
|
||||||
|
* {
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
color-adjust: exact !important;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Insert PDF optimizations
|
||||||
|
if '</style>' in html_content:
|
||||||
|
html_content = html_content.replace('</style>', pdf_optimizations + '\n </style>')
|
||||||
|
|
||||||
|
return html_content
|
||||||
|
|
||||||
|
class PageDetector:
|
||||||
|
"""Detects page elements and their dimensions in HTML documents."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def detect_pages_and_format(page: Page) -> dict:
|
||||||
|
"""
|
||||||
|
Advanced page detection with multiple fallback strategies.
|
||||||
|
Handles different HTML structures and CSS approaches robustly.
|
||||||
|
"""
|
||||||
|
page_info = await page.evaluate("""
|
||||||
|
() => {
|
||||||
|
// Strategy 1: Direct page element detection
|
||||||
|
const pageSelectors = [
|
||||||
|
'.brochure-page',
|
||||||
|
'.brochure',
|
||||||
|
'.page',
|
||||||
|
'[class*="page"]',
|
||||||
|
'.pdf-page',
|
||||||
|
'.slide',
|
||||||
|
'.section'
|
||||||
|
];
|
||||||
|
|
||||||
|
let pageElements = [];
|
||||||
|
let detectedSelector = '';
|
||||||
|
|
||||||
|
// Find page elements with priority order
|
||||||
|
for (const selector of pageSelectors) {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
if (elements.length > 0) {
|
||||||
|
pageElements = Array.from(elements);
|
||||||
|
detectedSelector = selector;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: A4-sized container detection
|
||||||
|
if (pageElements.length === 0) {
|
||||||
|
const allElements = document.querySelectorAll('*');
|
||||||
|
const a4Elements = Array.from(allElements).filter(el => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const width = parseFloat(style.width);
|
||||||
|
const height = parseFloat(style.height);
|
||||||
|
|
||||||
|
// A4 dimensions in different units
|
||||||
|
const isA4Width = (width >= 794 && width <= 800) ||
|
||||||
|
(width >= 210 && width <= 220) ||
|
||||||
|
(width >= 8.27 && width <= 8.5);
|
||||||
|
const isA4Height = (height >= 1123 && height <= 1130) ||
|
||||||
|
(height >= 297 && height <= 300) ||
|
||||||
|
(height >= 11.69 && height <= 12);
|
||||||
|
|
||||||
|
return isA4Width && isA4Height;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (a4Elements.length > 0) {
|
||||||
|
pageElements = a4Elements;
|
||||||
|
detectedSelector = 'A4-sized-element';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Body as single page
|
||||||
|
if (pageElements.length === 0) {
|
||||||
|
pageElements = [document.body];
|
||||||
|
detectedSelector = 'body';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advanced dimension analysis with multiple measurement methods
|
||||||
|
let dimensionResults = [];
|
||||||
|
|
||||||
|
pageElements.forEach((element, index) => {
|
||||||
|
const measurements = {};
|
||||||
|
|
||||||
|
// Method 1: CSS Computed Style
|
||||||
|
const computedStyle = window.getComputedStyle(element);
|
||||||
|
const cssWidth = parseFloat(computedStyle.width);
|
||||||
|
const cssHeight = parseFloat(computedStyle.height);
|
||||||
|
|
||||||
|
if (cssWidth > 0 && cssHeight > 0) {
|
||||||
|
measurements.css = { width: cssWidth, height: cssHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Bounding Client Rect
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
|
measurements.bounding = { width: rect.width, height: rect.height };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Offset Dimensions
|
||||||
|
if (element.offsetWidth > 0 && element.offsetHeight > 0) {
|
||||||
|
measurements.offset = { width: element.offsetWidth, height: element.offsetHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Scroll Dimensions
|
||||||
|
if (element.scrollWidth > 0 && element.scrollHeight > 0) {
|
||||||
|
measurements.scroll = { width: element.scrollWidth, height: element.scrollHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 5: Client Dimensions
|
||||||
|
if (element.clientWidth > 0 && element.clientHeight > 0) {
|
||||||
|
measurements.client = { width: element.clientWidth, height: element.clientHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the best measurement method
|
||||||
|
let bestMeasurement = null;
|
||||||
|
let bestScore = 0;
|
||||||
|
|
||||||
|
Object.entries(measurements).forEach(([method, dims]) => {
|
||||||
|
const score = calculateDimensionScore(dims.width, dims.height);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMeasurement = { method, ...dims };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bestMeasurement) {
|
||||||
|
dimensionResults.push({
|
||||||
|
index,
|
||||||
|
element: element.tagName + (element.className ? '.' + element.className.split(' ')[0] : ''),
|
||||||
|
...bestMeasurement
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to score dimensions
|
||||||
|
function calculateDimensionScore(width, height) {
|
||||||
|
if (width <= 0 || height <= 0) return 0;
|
||||||
|
if (width > 2000 || height > 2000) return 0; // Too large
|
||||||
|
if (width < 50 || height < 50) return 0; // Too small
|
||||||
|
|
||||||
|
// Prefer A4-like dimensions
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
const a4Ratio = 210 / 297; // 0.707
|
||||||
|
const ratioScore = 1 - Math.abs(aspectRatio - a4Ratio) / a4Ratio;
|
||||||
|
|
||||||
|
// Prefer reasonable sizes
|
||||||
|
const sizeScore = Math.min(width / 800, height / 1200);
|
||||||
|
|
||||||
|
return ratioScore * sizeScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate final dimensions
|
||||||
|
let maxWidth = 0;
|
||||||
|
let maxHeight = 0;
|
||||||
|
let totalWidth = 0;
|
||||||
|
let totalHeight = 0;
|
||||||
|
let validCount = 0;
|
||||||
|
|
||||||
|
dimensionResults.forEach(result => {
|
||||||
|
if (result.width > 0 && result.height > 0) {
|
||||||
|
maxWidth = Math.max(maxWidth, result.width);
|
||||||
|
maxHeight = Math.max(maxHeight, result.height);
|
||||||
|
totalWidth += result.width;
|
||||||
|
totalHeight += result.height;
|
||||||
|
validCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback to standard A4 if no valid dimensions
|
||||||
|
if (validCount === 0) {
|
||||||
|
maxWidth = 794;
|
||||||
|
maxHeight = 1123;
|
||||||
|
console.warn('No valid dimensions detected, using standard A4');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced format detection
|
||||||
|
let format = 'a4';
|
||||||
|
const aspectRatio = maxWidth / maxHeight;
|
||||||
|
|
||||||
|
if (Math.abs(aspectRatio - 0.707) < 0.1) { // A4 ratio
|
||||||
|
format = 'a4';
|
||||||
|
} else if (Math.abs(aspectRatio - 0.773) < 0.1) { // Letter ratio
|
||||||
|
format = 'a4';
|
||||||
|
} else if (Math.abs(aspectRatio - 0.607) < 0.1) { // Legal ratio
|
||||||
|
format = 'a4';
|
||||||
|
} else if (aspectRatio > 1.2) { // Landscape
|
||||||
|
format = 'a4';
|
||||||
|
} else if (aspectRatio < 0.5) { // Very tall
|
||||||
|
format = 'a4';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageCount: pageElements.length,
|
||||||
|
format: format,
|
||||||
|
maxWidth: Math.round(maxWidth),
|
||||||
|
maxHeight: Math.round(maxHeight),
|
||||||
|
totalWidth: Math.round(totalWidth),
|
||||||
|
totalHeight: Math.round(totalHeight),
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
detectedSelector: detectedSelector,
|
||||||
|
validDimensions: validCount,
|
||||||
|
averageWidth: validCount > 0 ? Math.round(totalWidth / validCount) : 0,
|
||||||
|
averageHeight: validCount > 0 ? Math.round(totalHeight / validCount) : 0,
|
||||||
|
dimensionResults: dimensionResults,
|
||||||
|
hasReasonableDimensions: maxWidth >= 200 && maxHeight >= 200,
|
||||||
|
measurementMethods: dimensionResults.map(r => r.method)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
return page_info
|
||||||
|
|
||||||
|
async def generate_single_pdf(input_content: str, output_pdf: str, is_url: bool = False, is_file: bool = False, is_html_string: bool = False):
|
||||||
|
"""
|
||||||
|
Generate PDF for a single input (URL, file path, or HTML string).
|
||||||
|
Always uses A4 size for consistent output with intelligent content fitting.
|
||||||
|
"""
|
||||||
|
temp_file = None
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
# Use the correct Chromium path
|
||||||
|
browser = await p.chromium.launch(
|
||||||
|
headless=True,
|
||||||
|
args=[
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-web-security',
|
||||||
|
'--disable-features=VizDisplayCompositor',
|
||||||
|
'--enable-font-antialiasing',
|
||||||
|
'--font-render-hinting=none',
|
||||||
|
'--disable-background-timer-throttling',
|
||||||
|
'--disable-backgrounding-occluded-windows',
|
||||||
|
'--disable-renderer-backgrounding',
|
||||||
|
'--allow-running-insecure-content',
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-plugins',
|
||||||
|
'--disable-images=false',
|
||||||
|
'--enable-javascript',
|
||||||
|
'--enable-css',
|
||||||
|
'--enable-fonts'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
page = await browser.new_page()
|
||||||
|
await page.set_viewport_size({'width': 1920, 'height': 1080})
|
||||||
|
page.set_default_timeout(120000)
|
||||||
|
|
||||||
|
if is_html_string:
|
||||||
|
# Preprocess HTML content
|
||||||
|
processed_html = HTMLPreprocessor.preprocess_html(input_content)
|
||||||
|
|
||||||
|
# Write processed HTML to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.html', dir=TEMP_FOLDER) as tmp:
|
||||||
|
tmp.write(processed_html.encode('utf-8'))
|
||||||
|
temp_file = tmp.name
|
||||||
|
abs_path = "file://" + os.path.abspath(temp_file)
|
||||||
|
await page.goto(abs_path, wait_until="load")
|
||||||
|
elif is_url:
|
||||||
|
await page.goto(input_content, wait_until="domcontentloaded")
|
||||||
|
elif is_file:
|
||||||
|
abs_path = "file://" + os.path.abspath(input_content)
|
||||||
|
await page.goto(abs_path, wait_until="load")
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid input type")
|
||||||
|
|
||||||
|
# Wait for content to load and stabilize
|
||||||
|
await page.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
# Wait for any dynamic content to finish loading
|
||||||
|
try:
|
||||||
|
await page.wait_for_load_state('networkidle', timeout=10000)
|
||||||
|
except:
|
||||||
|
pass # Continue if network idle doesn't happen
|
||||||
|
|
||||||
|
# Ensure all external resources are loaded
|
||||||
|
print("🔄 Loading external resources...")
|
||||||
|
await _ensure_resources_loaded(page)
|
||||||
|
|
||||||
|
# Detect pages and format
|
||||||
|
print("🔍 Analyzing page structure...")
|
||||||
|
page_info = await PageDetector.detect_pages_and_format(page)
|
||||||
|
|
||||||
|
print(f"📄 Detected {page_info['pageCount']} pages")
|
||||||
|
print(f"📐 Max dimensions: {page_info['maxWidth']}x{page_info['maxHeight']}px")
|
||||||
|
print(f"🎯 Recommended format: {page_info['format']}")
|
||||||
|
print(f"🔍 Detected selector: {page_info['detectedSelector']}")
|
||||||
|
print(f"✅ Valid dimensions: {page_info['validDimensions']}")
|
||||||
|
print(f"📏 Average dimensions: {page_info['averageWidth']}x{page_info['averageHeight']}px")
|
||||||
|
print(f"📊 Aspect ratio: {page_info['aspectRatio']:.3f}")
|
||||||
|
print(f"🔧 Measurement methods: {', '.join(page_info['measurementMethods'])}")
|
||||||
|
|
||||||
|
# Always use A4 format for consistent output
|
||||||
|
pdf_format = 'a4'
|
||||||
|
|
||||||
|
# Calculate optimal scale to fit content within A4 dimensions
|
||||||
|
dpi = 96
|
||||||
|
content_width_px = page_info['maxWidth']
|
||||||
|
content_height_px = page_info['maxHeight']
|
||||||
|
|
||||||
|
# Convert to inches
|
||||||
|
content_width_in = content_width_px / dpi
|
||||||
|
content_height_in = content_height_px / dpi
|
||||||
|
|
||||||
|
# Determine orientation based on content analysis
|
||||||
|
landscape = False
|
||||||
|
if content_width_in > content_height_in * 1.2: # 20% wider threshold
|
||||||
|
landscape = True
|
||||||
|
elif page_info.get('hasTables', False) and content_width_in > content_height_in * 1.1: # Tables need more width
|
||||||
|
landscape = True
|
||||||
|
|
||||||
|
# Always use A4 dimensions
|
||||||
|
if landscape:
|
||||||
|
# A4 Landscape: 11" x 8.5"
|
||||||
|
pdf_width = 11.0
|
||||||
|
pdf_height = 8.5
|
||||||
|
else:
|
||||||
|
# A4 Portrait: 8.5" x 11"
|
||||||
|
pdf_width = 8.5
|
||||||
|
pdf_height = 11.0
|
||||||
|
|
||||||
|
# Calculate optimal scale to fit content within A4 dimensions
|
||||||
|
# Account for margins when calculating scale
|
||||||
|
margin_in = 0.5 # 0.5 inch margins
|
||||||
|
available_width = pdf_width - (2 * margin_in)
|
||||||
|
available_height = pdf_height - (2 * margin_in)
|
||||||
|
|
||||||
|
# Calculate scale to fit content within available space
|
||||||
|
width_scale = available_width / content_width_in if content_width_in > 0 else 1.0
|
||||||
|
height_scale = available_height / content_height_in if content_height_in > 0 else 1.0
|
||||||
|
|
||||||
|
# Use the smaller scale to ensure content fits in both dimensions
|
||||||
|
optimal_scale = min(width_scale, height_scale, 1.0) # Don't scale up beyond 100%
|
||||||
|
|
||||||
|
# Ensure minimum scale for readability
|
||||||
|
if optimal_scale < 0.3:
|
||||||
|
optimal_scale = 0.3 # Minimum 30% scale for readability
|
||||||
|
|
||||||
|
# Adjust margins based on content type - optimized for A4 size
|
||||||
|
if page_info.get('hasTables', False):
|
||||||
|
# Tables need more breathing room on A4
|
||||||
|
margins = {'top': '0.75in', 'right': '0.75in', 'bottom': '0.75in', 'left': '0.75in'}
|
||||||
|
elif page_info.get('hasImages', False):
|
||||||
|
# Images look better with balanced margins on A4
|
||||||
|
margins = {'top': '0.6in', 'right': '0.6in', 'bottom': '0.6in', 'left': '0.6in'}
|
||||||
|
else:
|
||||||
|
# Text content works well with standard A4 margins
|
||||||
|
margins = {'top': '0.5in', 'right': '0.5in', 'bottom': '0.5in', 'left': '0.5in'}
|
||||||
|
|
||||||
|
# For very small content, use smaller margins to maximize A4 space
|
||||||
|
if content_width_in < 6.0 and content_height_in < 8.0:
|
||||||
|
margins = {'top': '0.4in', 'right': '0.4in', 'bottom': '0.4in', 'left': '0.4in'}
|
||||||
|
|
||||||
|
# For very large content, use larger margins to ensure readability
|
||||||
|
if content_width_in > 10.0 or content_height_in > 12.0:
|
||||||
|
margins = {'top': '0.8in', 'right': '0.8in', 'bottom': '0.8in', 'left': '0.8in'}
|
||||||
|
|
||||||
|
pdf_options = {
|
||||||
|
'path': output_pdf,
|
||||||
|
'print_background': True,
|
||||||
|
'margin': margins,
|
||||||
|
'scale': optimal_scale,
|
||||||
|
'landscape': landscape,
|
||||||
|
'width': f"{pdf_width}in",
|
||||||
|
'height': f"{pdf_height}in",
|
||||||
|
'prefer_css_page_size': False, # Disable CSS page size to ensure A4
|
||||||
|
'format': 'A4' # Explicitly set A4 format
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate PDF
|
||||||
|
await page.pdf(**pdf_options)
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
print(f"✅ PDF generated: {output_pdf}")
|
||||||
|
print(f"📏 A4 Size: {pdf_width}in x {pdf_height}in ({'Landscape' if landscape else 'Portrait'})")
|
||||||
|
print(f"📐 Content: {content_width_in:.2f}in x {content_height_in:.2f}in")
|
||||||
|
print(f"🔍 Scale: {optimal_scale:.2f} (optimized for A4 fit)")
|
||||||
|
print(f"📄 Format: A4 Standard")
|
||||||
|
|
||||||
|
except TimeoutError:
|
||||||
|
raise Exception("Timeout: Page took too long to load.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ PDF generation error: {str(e)}")
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
os.remove(temp_file)
|
||||||
|
|
||||||
|
async def _ensure_resources_loaded(page: Page):
|
||||||
|
"""Ensure all external resources are properly loaded."""
|
||||||
|
|
||||||
|
# Wait for fonts to load
|
||||||
|
await page.evaluate("""
|
||||||
|
() => {
|
||||||
|
return document.fonts.ready;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Wait for all images to load
|
||||||
|
await page.evaluate("""
|
||||||
|
() => {
|
||||||
|
return Promise.all(
|
||||||
|
Array.from(document.images)
|
||||||
|
.filter(img => !img.complete)
|
||||||
|
.map(img => new Promise(resolve => {
|
||||||
|
img.onload = img.onerror = resolve;
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Wait for background images to load
|
||||||
|
await page.evaluate("""
|
||||||
|
() => {
|
||||||
|
const elementsWithBg = document.querySelectorAll('[style*="background-image"], [class*="image"]');
|
||||||
|
return Promise.all(
|
||||||
|
Array.from(elementsWithBg).map(el => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const bgImage = style.backgroundImage;
|
||||||
|
if (bgImage && bgImage !== 'none') {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = img.onerror = resolve;
|
||||||
|
img.src = bgImage.replace(/url\\(['"]?(.*?)['"]?\\)/g, '$1');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Wait for CSS to be fully applied
|
||||||
|
await page.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
def process_input(input_content: str, output_name: str = None):
|
||||||
|
"""
|
||||||
|
Process the input: determine type and generate PDF(s).
|
||||||
|
Returns path to PDF or ZIP file.
|
||||||
|
"""
|
||||||
|
is_url = input_content.startswith('http://') or input_content.startswith('https://')
|
||||||
|
is_file = False # HTML content is not a physical file
|
||||||
|
is_html_string = True # HTML content is always a string
|
||||||
|
|
||||||
|
if output_name is None:
|
||||||
|
output_name = f'single_output_{uuid.uuid4().hex[:8]}.pdf'
|
||||||
|
|
||||||
|
if is_file:
|
||||||
|
# This case should ideally not be reached for HTML content
|
||||||
|
raise ValueError("HTML content cannot be treated as a file path.")
|
||||||
|
|
||||||
|
pdf_path = os.path.join(TEMP_FOLDER, secure_filename(output_name))
|
||||||
|
|
||||||
|
if is_url:
|
||||||
|
asyncio.run(generate_single_pdf(input_content, pdf_path, is_url=True))
|
||||||
|
elif is_html_string:
|
||||||
|
asyncio.run(generate_single_pdf(input_content, pdf_path, is_html_string=True))
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid input type for processing")
|
||||||
|
|
||||||
|
return pdf_path, 'application/pdf'
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def root():
|
||||||
|
"""Root endpoint"""
|
||||||
|
return jsonify({
|
||||||
|
"message": "PDF Generator API",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"status": "running",
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"endpoints": {
|
||||||
|
"generate-pdf": "/generate-pdf",
|
||||||
|
"health": "/health"
|
||||||
|
},
|
||||||
|
"features": [
|
||||||
|
"HTML string to PDF",
|
||||||
|
"URL to PDF",
|
||||||
|
"HTML file to PDF",
|
||||||
|
"Batch HTML files to ZIP",
|
||||||
|
"Standard A4 format",
|
||||||
|
"Consistent page sizing"
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"method": "POST",
|
||||||
|
"endpoint": "/generate-pdf",
|
||||||
|
"body": {
|
||||||
|
"input": "HTML string, URL, or file path",
|
||||||
|
"output": "Optional output filename"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/health')
|
||||||
|
def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return jsonify({
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"service": "Advanced HTML to PDF Generator",
|
||||||
|
"temp_folder": TEMP_FOLDER,
|
||||||
|
"temp_folder_exists": os.path.exists(TEMP_FOLDER),
|
||||||
|
"uptime": "running"
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/generate-pdf', methods=['POST'])
|
||||||
|
def generate_pdf_api():
|
||||||
|
"""Main PDF generation endpoint"""
|
||||||
|
try:
|
||||||
|
# Get request data - handle both JSON and form data more robustly
|
||||||
|
input_content = None
|
||||||
|
output_name = None
|
||||||
|
|
||||||
|
if request.is_json:
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if data and 'input' in data:
|
||||||
|
input_content = data['input']
|
||||||
|
output_name = data.get('output', None)
|
||||||
|
except Exception as json_error:
|
||||||
|
print(f"❌ JSON parsing error: {json_error}")
|
||||||
|
return jsonify({'error': f'Invalid JSON format: {str(json_error)}'}), 400
|
||||||
|
else:
|
||||||
|
# Handle form data
|
||||||
|
input_content = request.form.get('input')
|
||||||
|
output_name = request.form.get('output')
|
||||||
|
|
||||||
|
# If input is a file, read its content
|
||||||
|
if 'input' in request.files:
|
||||||
|
file = request.files['input']
|
||||||
|
if file and file.filename:
|
||||||
|
try:
|
||||||
|
input_content = file.read().decode('utf-8')
|
||||||
|
if not output_name:
|
||||||
|
output_name = file.filename.replace('.html', '.pdf')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return jsonify({'error': 'File encoding error. Please ensure the file is UTF-8 encoded.'}), 400
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
if not input_content or input_content.strip() == '':
|
||||||
|
return jsonify({'error': 'Input cannot be empty. Please provide HTML content.'}), 400
|
||||||
|
|
||||||
|
# Clean the HTML content - remove problematic control characters
|
||||||
|
input_content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', input_content)
|
||||||
|
|
||||||
|
# Process input and generate PDF/ZIP
|
||||||
|
file_path, mime_type = process_input(input_content, output_name)
|
||||||
|
|
||||||
|
# Check if file was created
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return jsonify({'error': 'Failed to generate output file'}), 500
|
||||||
|
|
||||||
|
# Send file response
|
||||||
|
response = send_file(
|
||||||
|
file_path,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=os.path.basename(file_path),
|
||||||
|
mimetype=mime_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up after sending
|
||||||
|
@response.call_on_close
|
||||||
|
def cleanup():
|
||||||
|
try:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
print(f"🧹 Cleaned up: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Cleanup error: {e}")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ API Error: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def cleanup_temp_files(response):
|
||||||
|
"""Clean up temporary files older than 1 hour"""
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
current_time = time.time()
|
||||||
|
for filename in os.listdir(TEMP_FOLDER):
|
||||||
|
filepath = os.path.join(TEMP_FOLDER, filename)
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
if current_time - os.path.getmtime(filepath) > 3600: # 1 hour
|
||||||
|
os.remove(filepath)
|
||||||
|
print(f"🧹 Auto-cleanup: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Auto-cleanup error: {e}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print("🚀 Starting Advanced HTML to PDF Generator API...")
|
||||||
|
print("📝 Endpoints available:")
|
||||||
|
print(" GET / - API information")
|
||||||
|
print(" GET /health - Health check")
|
||||||
|
print(" POST /generate-pdf - Generate PDF from HTML/URL/file")
|
||||||
|
print("")
|
||||||
|
print("✨ Features:")
|
||||||
|
print(" • HTML string to PDF")
|
||||||
|
print(" • URL to PDF")
|
||||||
|
print(" • HTML file to PDF")
|
||||||
|
print(" • Batch HTML files to ZIP")
|
||||||
|
print(" • Standard A4 format")
|
||||||
|
print(" • Consistent page sizing")
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,7 @@
|
|||||||
fastapi==0.104.1
|
|
||||||
uvicorn[standard]==0.24.0
|
|
||||||
python-multipart==0.0.6
|
Flask==2.3.3
|
||||||
pydantic==2.5.0
|
playwright==1.40.0
|
||||||
Pillow==10.1.0
|
requests==2.31.0
|
||||||
reportlab==4.0.7
|
Werkzeug==2.3.7
|
||||||
jinja2==3.1.2
|
gunicorn==21.2.0
|
||||||
markdown==3.5.1
|
|
||||||
flask==3.0.0
|
|
||||||
flask-cors==4.0.0
|
|
||||||
pdfkit==1.0.0
|
|
||||||
@ -1,378 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Flask API for Salesforce PDF Generation
|
|
||||||
Takes HTML content from Salesforce and returns a downloadable PDF
|
|
||||||
"""
|
|
||||||
|
|
||||||
from flask import Flask, request, send_file, jsonify
|
|
||||||
from flask_cors import CORS
|
|
||||||
import pdfkit
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import base64
|
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
CORS(app)
|
|
||||||
|
|
||||||
# Configure pdfkit options for better PDF generation
|
|
||||||
PDF_OPTIONS = {
|
|
||||||
'page-size': 'A4',
|
|
||||||
'margin-top': '0.75in',
|
|
||||||
'margin-right': '0.75in',
|
|
||||||
'margin-bottom': '0.75in',
|
|
||||||
'margin-left': '0.75in',
|
|
||||||
'encoding': "UTF-8",
|
|
||||||
'no-outline': None,
|
|
||||||
'enable-local-file-access': None,
|
|
||||||
'print-media-type': None,
|
|
||||||
'dpi': 300,
|
|
||||||
'image-quality': 100,
|
|
||||||
'javascript-delay': 1000,
|
|
||||||
'no-stop-slow-scripts': None,
|
|
||||||
'custom-header': [
|
|
||||||
('Accept-Encoding', 'gzip')
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
|
||||||
def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return jsonify({
|
|
||||||
'status': 'healthy',
|
|
||||||
'timestamp': datetime.now().isoformat(),
|
|
||||||
'service': 'Salesforce PDF Generator API'
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/generate-pdf', methods=['POST'])
|
|
||||||
def generate_pdf():
|
|
||||||
"""
|
|
||||||
Generate PDF from HTML content sent from Salesforce
|
|
||||||
Expected JSON payload:
|
|
||||||
{
|
|
||||||
"html_content": "<html>...</html>",
|
|
||||||
"property_data": {...},
|
|
||||||
"template_name": "everkind",
|
|
||||||
"filename": "property_brochure.pdf"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info("Received PDF generation request from Salesforce")
|
|
||||||
|
|
||||||
# Get request data
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
return jsonify({'error': 'No data provided'}), 400
|
|
||||||
|
|
||||||
html_content = data.get('html_content')
|
|
||||||
property_data = data.get('property_data', {})
|
|
||||||
template_name = data.get('template_name', 'default')
|
|
||||||
filename = data.get('filename', f'property_brochure_{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf')
|
|
||||||
|
|
||||||
if not html_content:
|
|
||||||
return jsonify({'error': 'HTML content is required'}), 400
|
|
||||||
|
|
||||||
logger.info(f"Processing template: {template_name}")
|
|
||||||
logger.info(f"Property data keys: {list(property_data.keys()) if property_data else 'None'}")
|
|
||||||
logger.info(f"HTML content length: {len(html_content)}")
|
|
||||||
|
|
||||||
# Create complete HTML document with proper styling
|
|
||||||
complete_html = create_complete_html_document(html_content, property_data, template_name)
|
|
||||||
|
|
||||||
# Generate PDF
|
|
||||||
pdf_path = generate_pdf_from_html(complete_html, filename)
|
|
||||||
|
|
||||||
if not pdf_path or not os.path.exists(pdf_path):
|
|
||||||
return jsonify({'error': 'Failed to generate PDF'}), 500
|
|
||||||
|
|
||||||
logger.info(f"PDF generated successfully: {pdf_path}")
|
|
||||||
|
|
||||||
# Return the PDF file
|
|
||||||
return send_file(
|
|
||||||
pdf_path,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name=filename,
|
|
||||||
mimetype='application/pdf'
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error generating PDF: {str(e)}")
|
|
||||||
return jsonify({'error': f'PDF generation failed: {str(e)}'}), 500
|
|
||||||
|
|
||||||
def create_complete_html_document(html_content, property_data, template_name):
|
|
||||||
"""Create a complete HTML document with proper styling and data"""
|
|
||||||
|
|
||||||
# Base CSS styles for consistent PDF output
|
|
||||||
base_css = """
|
|
||||||
<style>
|
|
||||||
@page {
|
|
||||||
size: A4;
|
|
||||||
margin: 0.75in;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Arial', 'Helvetica', sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-header p {
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-card {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-card-label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-card-value {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-details {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-details h2 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-bottom: 2px solid #667eea;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.property-details-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-section h2 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
padding-top: 15px;
|
|
||||||
margin-top: 30px;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure proper page breaks */
|
|
||||||
.page-break {
|
|
||||||
page-break-before: always;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print-specific styles */
|
|
||||||
@media print {
|
|
||||||
body { margin: 0; }
|
|
||||||
.property-header { break-inside: avoid; }
|
|
||||||
.property-grid { break-inside: avoid; }
|
|
||||||
.property-details { break-inside: avoid; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create the complete HTML document
|
|
||||||
complete_html = f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Property Brochure - {property_data.get('propertyName', 'Property')}</title>
|
|
||||||
{base_css}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{html_content}
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p><strong>Generated on:</strong> {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p>
|
|
||||||
<p><em>Property CRM System - Professional Brochure</em></p>
|
|
||||||
<p><em>Template: {template_name}</em></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return complete_html
|
|
||||||
|
|
||||||
def generate_pdf_from_html(html_content, filename):
|
|
||||||
"""Generate PDF from HTML content using pdfkit"""
|
|
||||||
try:
|
|
||||||
# Create temporary file for the PDF
|
|
||||||
temp_dir = tempfile.gettempdir()
|
|
||||||
pdf_path = os.path.join(temp_dir, filename)
|
|
||||||
|
|
||||||
logger.info(f"Generating PDF at: {pdf_path}")
|
|
||||||
|
|
||||||
# Configure pdfkit with wkhtmltopdf
|
|
||||||
try:
|
|
||||||
# Try to use system wkhtmltopdf
|
|
||||||
config = pdfkit.configuration(wkhtmltopdf='/usr/bin/wkhtmltopdf')
|
|
||||||
pdfkit.from_string(html_content, pdf_path, options=PDF_OPTIONS, configuration=config)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"System wkhtmltopdf failed: {e}")
|
|
||||||
# Try without configuration (uses PATH)
|
|
||||||
pdfkit.from_string(html_content, pdf_path, options=PDF_OPTIONS)
|
|
||||||
|
|
||||||
if os.path.exists(pdf_path):
|
|
||||||
file_size = os.path.getsize(pdf_path)
|
|
||||||
logger.info(f"PDF generated successfully. Size: {file_size} bytes")
|
|
||||||
return pdf_path
|
|
||||||
else:
|
|
||||||
logger.error("PDF file was not created")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in generate_pdf_from_html: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@app.route('/test-pdf', methods=['GET'])
|
|
||||||
def test_pdf():
|
|
||||||
"""Test endpoint to verify PDF generation works"""
|
|
||||||
try:
|
|
||||||
test_html = """
|
|
||||||
<div class="property-header">
|
|
||||||
<h1>Test Property</h1>
|
|
||||||
<p>Villa in Dubai Marina</p>
|
|
||||||
</div>
|
|
||||||
<div class="property-grid">
|
|
||||||
<div class="property-card">
|
|
||||||
<div class="property-card-label">Price</div>
|
|
||||||
<div class="property-card-value">AED 2,500,000</div>
|
|
||||||
</div>
|
|
||||||
<div class="property-card">
|
|
||||||
<div class="property-card-label">Bedrooms</div>
|
|
||||||
<div class="property-card-value">3</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'propertyName': 'Test Property',
|
|
||||||
'propertyType': 'Villa',
|
|
||||||
'location': 'Dubai Marina'
|
|
||||||
}
|
|
||||||
|
|
||||||
complete_html = create_complete_html_document(test_html, test_data, 'test')
|
|
||||||
pdf_path = generate_pdf_from_html(complete_html, 'test_property.pdf')
|
|
||||||
|
|
||||||
if pdf_path and os.path.exists(pdf_path):
|
|
||||||
return send_file(
|
|
||||||
pdf_path,
|
|
||||||
as_attachment=True,
|
|
||||||
download_name='test_property.pdf',
|
|
||||||
mimetype='application/pdf'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return jsonify({'error': 'Test PDF generation failed'}), 500
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'error': f'Test failed: {str(e)}'}), 500
|
|
||||||
|
|
||||||
@app.route('/api/info', methods=['GET'])
|
|
||||||
def api_info():
|
|
||||||
"""Get API information"""
|
|
||||||
return jsonify({
|
|
||||||
'name': 'Salesforce PDF Generator API',
|
|
||||||
'version': '1.0.0',
|
|
||||||
'description': 'Takes HTML content from Salesforce and returns downloadable PDF',
|
|
||||||
'endpoints': {
|
|
||||||
'health': '/health',
|
|
||||||
'generate_pdf': '/generate-pdf',
|
|
||||||
'test_pdf': '/test-pdf',
|
|
||||||
'api_info': '/api/info'
|
|
||||||
},
|
|
||||||
'usage': {
|
|
||||||
'method': 'POST',
|
|
||||||
'endpoint': '/generate-pdf',
|
|
||||||
'content_type': 'application/json',
|
|
||||||
'payload': {
|
|
||||||
'html_content': 'HTML content from Salesforce',
|
|
||||||
'property_data': 'Property information object',
|
|
||||||
'template_name': 'Template name',
|
|
||||||
'filename': 'Output filename (optional)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
logger.info("Starting Salesforce PDF Generator API Server...")
|
|
||||||
logger.info("Available endpoints:")
|
|
||||||
logger.info(" GET /health - Health check")
|
|
||||||
logger.info(" POST /generate-pdf - Generate PDF from HTML")
|
|
||||||
logger.info(" GET /test-pdf - Test PDF generation")
|
|
||||||
logger.info(" GET /api/info - API information")
|
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Startup script for Salesforce PDF Generator API
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
def install_requirements():
|
|
||||||
"""Install required packages"""
|
|
||||||
print("Installing required packages...")
|
|
||||||
try:
|
|
||||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
|
|
||||||
print("✅ Requirements installed successfully")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f"❌ Failed to install requirements: {e}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_wkhtmltopdf():
|
|
||||||
"""Check if wkhtmltopdf is available"""
|
|
||||||
print("Checking wkhtmltopdf installation...")
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['wkhtmltopdf', '--version'], capture_output=True, text=True)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print("✅ wkhtmltopdf is available")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print("❌ wkhtmltopdf not found in PATH")
|
|
||||||
return False
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("❌ wkhtmltopdf not found. Please install it:")
|
|
||||||
print(" Ubuntu/Debian: sudo apt-get install wkhtmltopdf")
|
|
||||||
print(" macOS: brew install wkhtmltopdf")
|
|
||||||
print(" Windows: Download from https://wkhtmltopdf.org/downloads.html")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start_api():
|
|
||||||
"""Start the Flask API server"""
|
|
||||||
print("Starting Salesforce PDF Generator API...")
|
|
||||||
try:
|
|
||||||
from salesforce_pdf_api import app
|
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"❌ Failed to import API: {e}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to start API: {e}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main startup function"""
|
|
||||||
print("🚀 Salesforce PDF Generator API Startup")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Install requirements
|
|
||||||
if not install_requirements():
|
|
||||||
print("❌ Cannot continue without requirements")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check wkhtmltopdf
|
|
||||||
if not check_wkhtmltopdf():
|
|
||||||
print("⚠️ wkhtmltopdf not available. PDF generation may fail.")
|
|
||||||
print(" Continuing anyway...")
|
|
||||||
|
|
||||||
# Start API
|
|
||||||
print("\n🌐 Starting API server on http://localhost:8000")
|
|
||||||
print(" Press Ctrl+C to stop")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
if not start_api():
|
|
||||||
print("❌ Failed to start API server")
|
|
||||||
return
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
477
sample templates/real estate modern home.html
Normal file
477
sample templates/real estate modern home.html
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Property Brochure</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
/* --- Google Font --- */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap');
|
||||||
|
|
||||||
|
/* --- Design System Variables --- */
|
||||||
|
:root {
|
||||||
|
--color-primary: #003366; /* Deep Navy Blue */
|
||||||
|
--color-accent: #f39c12; /* Warm Gold/Orange */
|
||||||
|
--color-background: #ffffff;
|
||||||
|
--color-text-light: #ffffff;
|
||||||
|
--color-text-dark: #333333;
|
||||||
|
--color-text-muted: #666666;
|
||||||
|
--color-background-light: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Basic Setup --- */
|
||||||
|
body {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
gap: 40px; /* Space between pages */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- A4 Page Container --- */
|
||||||
|
.brochure {
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Shared Section Title --- */
|
||||||
|
h2.section-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 3px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Page 1: Front Page Styles --- */
|
||||||
|
.hero {
|
||||||
|
height: 60%;
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.hero-overlay {
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.1) 100%);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.property-name {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.property-address {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 8px 0 24px 0;
|
||||||
|
border-left: 3px solid var(--color-accent);
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
.hero-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.description h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.description p {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.agent-footer {
|
||||||
|
border-top: 4px solid var(--color-accent);
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.agent-info h3 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
.agent-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.agent-contact-details {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.agent-contact-details p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Page 2: Middle Page Styles --- */
|
||||||
|
.visual-header {
|
||||||
|
height: 45%;
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.visual-overlay {
|
||||||
|
background: linear-gradient(to top, rgba(0, 20, 40, 0.8) 0%, rgba(0,0,0,0.0) 100%);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
.visual-header h1 {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.visual-header p {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.content.grid-layout {
|
||||||
|
padding: 40px;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
.spec-list {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.spec-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-item:first-of-type { border-top: 1px solid #eee; }
|
||||||
|
.spec-item strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
.spec-item span {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.amenities-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.amenity-item {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.amenity-item span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.amenity-item .icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Page 3: Back Page Styles --- */
|
||||||
|
.location-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
flex: 1.5;
|
||||||
|
}
|
||||||
|
.map-container {
|
||||||
|
position: relative;
|
||||||
|
top: 30px;
|
||||||
|
height:480px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
.map-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.nearby-list .nearby-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearby-list .icon { font-size: 24px; color: var(--color-primary); width: 30px; }
|
||||||
|
.nearby-list strong { font-size: 15px; font-weight: 700; }
|
||||||
|
.nearby-list span { font-size: 15px; color: var(--color-text-muted); }
|
||||||
|
.additional-info-section {
|
||||||
|
padding: 40px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.info-list {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:first-of-type {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.info-item strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
.info-item span {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Shared Footer Styles --- */
|
||||||
|
.page-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
padding: 20px 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.page-footer strong {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.pagination a {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a.active {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="brochure" id="page1">
|
||||||
|
<div class="hero">
|
||||||
|
<div class="hero-overlay">
|
||||||
|
<h1 class="property-name">[Property Name]</h1>
|
||||||
|
<p class="property-address">[Property Address]</p>
|
||||||
|
<div class="hero-details">
|
||||||
|
<div class="price">[Price]</div>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat-item"><i class="fa-solid fa-bed"></i> [Bedrooms] Beds</span>
|
||||||
|
<span class="stat-item"><i class="fa-solid fa-bath"></i> [Bathrooms] Baths</span>
|
||||||
|
<span class="stat-item"><i class="fa-solid fa-ruler-combined"></i> [Area] sq. ft.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<h2>About this Property</h2>
|
||||||
|
<p>[Property Description goes here... This section provides a compelling overview of the property's main selling points, its unique character, and the lifestyle it offers. It should be engaging and concise.]</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agent-footer">
|
||||||
|
<div class="agent-info">
|
||||||
|
<h3>[Agent Name]</h3>
|
||||||
|
<p>Your Real Estate Professional</p>
|
||||||
|
</div>
|
||||||
|
<div class="agent-contact-details">
|
||||||
|
<p>[Agent Phone]</p>
|
||||||
|
<p>[Agent Email]</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div class="reference-id">
|
||||||
|
<strong>Reference ID:</strong> [Reference ID]
|
||||||
|
</div>
|
||||||
|
<div class="owner-info">
|
||||||
|
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure" id="page2">
|
||||||
|
<div class="visual-header">
|
||||||
|
<div class="visual-overlay">
|
||||||
|
<h1>In-depth Details</h1>
|
||||||
|
<p>A closer look at the property's features and specifications.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content grid-layout">
|
||||||
|
<div class="specifications-section">
|
||||||
|
<h2 class="section-title">Specifications</h2>
|
||||||
|
<div class="spec-list">
|
||||||
|
<div class="spec-item"><strong>Status:</strong> <span>[Status]</span></div>
|
||||||
|
<div class="spec-item"><strong>Type:</strong> <span>[Type]</span></div>
|
||||||
|
<div class="spec-item"><strong>Floor:</strong> <span>[Floor]</span></div>
|
||||||
|
<div class="spec-item"><strong>Parking:</strong> <span>[Parking]</span></div>
|
||||||
|
<div class="spec-item"><strong>Year Built:</strong> <span>[Year Built]</span></div>
|
||||||
|
<div class="spec-item"><strong>Furnishing:</strong> <span>[Furnishing]</span></div>
|
||||||
|
<div class="spec-item"><strong>Maintenance Fee:</strong> <span>[Maintenance Fee]</span></div>
|
||||||
|
<div class="spec-item"><strong>Service Charge:</strong> <span>[Service Charge]</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="amenities-section">
|
||||||
|
<h2 class="section-title">Amenities & Features</h2>
|
||||||
|
<div class="amenities-grid">
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 1]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 2]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 3]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 4]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 5]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 6]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 7]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 8]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 9]</span></div>
|
||||||
|
<div class="amenity-item"><i class="icon fa-solid fa-check"></i><span>[Amenity/Feature 10]</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div class="reference-id">
|
||||||
|
<strong>Reference ID:</strong> [Reference ID]
|
||||||
|
</div>
|
||||||
|
<div class="owner-info">
|
||||||
|
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure" id="page3">
|
||||||
|
<section class="location-section">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Location & Nearby</h2>
|
||||||
|
<div class="nearby-list">
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-location-dot"></i><div><strong>Landmarks:</strong> <span>[Nearby Landmarks]</span></div></div>
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-train-subway"></i><div><strong>Transportation:</strong> <span>[Transportation]</span></div></div>
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-graduation-cap"></i><div><strong>Schools:</strong> <span>[Schools]</span></div></div>
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-hospital"></i><div><strong>Hospitals:</strong> <span>[Hospitals]</span></div></div>
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-cart-shopping"></i><div><strong>Shopping:</strong> <span>[Shopping Centers]</span></div></div>
|
||||||
|
<div class="nearby-item"><i class="icon fa-solid fa-plane-departure"></i><div><strong>Airport:</strong> <span>[Airport Distance]</span></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="map-container">
|
||||||
|
<img src="https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="additional-info-section">
|
||||||
|
<h2 class="section-title">Additional Information</h2>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item"><strong>Pet Friendly:</strong> <span>[Pet Friendly Status]</span></div>
|
||||||
|
<div class="info-item"><strong>Smoking:</strong> <span>[Smoking Allowed]</span></div>
|
||||||
|
<div class="info-item"><strong>Available From:</strong> <span>[Available From Date]</span></div>
|
||||||
|
<div class="info-item"><strong>Minimum Contract:</strong> <span>[Minimum Contract Duration]</span></div>
|
||||||
|
<div class="info-item"><strong>Security Deposit:</strong> <span>[Security Deposit]</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div class="reference-id">
|
||||||
|
<strong>Reference ID:</strong> [Reference ID]
|
||||||
|
</div>
|
||||||
|
<div class="owner-info">
|
||||||
|
<strong>Owner Info:</strong> [Owner Name], [Owner Phone]
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
401
sample templates/the grand oak villa.html
Normal file
401
sample templates/the grand oak villa.html
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prestige Real Estate Brochure - 4 Page</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
/* --- DESIGN SYSTEM & VARIABLES --- */
|
||||||
|
:root {
|
||||||
|
/* Color Palette */
|
||||||
|
--color-dark-charcoal: #121212;
|
||||||
|
--color-light-gray: #F5F5F5;
|
||||||
|
--color-text-primary: #D1D1D1;
|
||||||
|
--color-text-secondary: #888888;
|
||||||
|
--color-accent-gold: #C0A062;
|
||||||
|
--color-white: #FFFFFF;
|
||||||
|
--color-border: #2a2a2a;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-primary: 'Montserrat', sans-serif;
|
||||||
|
--font-secondary: 'Playfair Display', serif;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--padding-page: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GLOBAL & BODY STYLES --- */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px;
|
||||||
|
margin: 0;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure-page {
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 1: FRONT COVER --- */
|
||||||
|
.cover-page {
|
||||||
|
position: relative;
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1580587771525-78b9dba3b914?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
color: var(--color-white);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.cover-overlay {
|
||||||
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: linear-gradient(180deg, rgba(18, 18, 18, 0.8) 0%, rgba(18, 18, 18, 0.3) 100%);
|
||||||
|
}
|
||||||
|
.cover-header {
|
||||||
|
position: relative; padding: var(--padding-page); display: flex;
|
||||||
|
justify-content: space-between; align-items: center;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-family: var(--font-secondary); font-size: 1.5rem; font-weight: 700;
|
||||||
|
letter-spacing: 2px; border: 2px solid var(--color-white); padding: 8px 15px;
|
||||||
|
}
|
||||||
|
.property-status {
|
||||||
|
background-color: var(--color-accent-gold); color: var(--color-dark-charcoal);
|
||||||
|
padding: 10px 20px; font-weight: 600; font-size: 0.9rem; text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cover-content { position: relative; padding: var(--padding-page); max-width: 65%; }
|
||||||
|
.cover-title {
|
||||||
|
font-family: var(--font-secondary); font-size: 4.5rem; font-weight: 700;
|
||||||
|
line-height: 1.1; margin: 0 0 10px 0; color: var(--color-white);
|
||||||
|
}
|
||||||
|
.cover-address {
|
||||||
|
font-size: 1.2rem; font-weight: 400; display: flex;
|
||||||
|
align-items: center; gap: 10px; color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.cover-address i { color: var(--color-accent-gold); }
|
||||||
|
.cover-footer {
|
||||||
|
position: relative; background-color: rgba(18, 18, 18, 0.9);
|
||||||
|
padding: 30px var(--padding-page); display: grid; grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px; text-align: center;
|
||||||
|
}
|
||||||
|
.feature-item { border-right: 1px solid var(--color-border); }
|
||||||
|
.feature-item:last-child { border-right: none; }
|
||||||
|
.feature-item .value {
|
||||||
|
font-size: 1.5rem; font-weight: 600;
|
||||||
|
color: var(--color-white); margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.feature-item .label {
|
||||||
|
font-size: 0.8rem; color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase; letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SHARED STYLES for Content Pages --- */
|
||||||
|
.content-body {
|
||||||
|
background-color: var(--color-dark-charcoal); color: var(--color-text-primary);
|
||||||
|
flex-grow: 1; padding: var(--padding-page); display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
display: flex; justify-content: space-between; align-items: baseline;
|
||||||
|
padding-bottom: 20px; margin-bottom: 30px; border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.page-header .title {
|
||||||
|
font-family: var(--font-secondary); font-size: 2.2rem; color: var(--color-white);
|
||||||
|
}
|
||||||
|
.page-header .title span { color: var(--color-accent-gold); }
|
||||||
|
.page-header .property-name {
|
||||||
|
font-size: 1rem; font-weight: 600; color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600; font-size: 1rem; color: var(--color-white);
|
||||||
|
margin: 0 0 25px 0; text-transform: uppercase; letter-spacing: 2px;
|
||||||
|
position: relative; padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.section-title::after {
|
||||||
|
content: ''; position: absolute; bottom: 0; left: 0;
|
||||||
|
width: 50px; height: 3px; background-color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
.main-content { flex-grow: 1; }
|
||||||
|
.page-footer {
|
||||||
|
background-color: #0A0A0A;
|
||||||
|
padding: 20px var(--padding-page);
|
||||||
|
font-size: 0.9rem; color: var(--color-text-secondary);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.page-footer strong { color: var(--color-accent-gold); font-weight: 600; }
|
||||||
|
|
||||||
|
/* --- PAGE 2: DETAILS --- */
|
||||||
|
.details-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
.description p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.specs-and-amenities {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
.spec-list .item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.spec-list .item:first-child { padding-top: 0; }
|
||||||
|
.spec-list .item .key { font-weight: 600; color: var(--color-text-secondary); }
|
||||||
|
.spec-list .item .value { font-weight: 400; color: var(--color-white); }
|
||||||
|
.amenities-list { list-style: none; padding: 0; margin: 0; columns: 2; gap: 15px; }
|
||||||
|
.amenities-list li { font-size: 0.9rem; margin-bottom: 15px; display: flex; align-items: center; }
|
||||||
|
.amenities-list i { color: var(--color-accent-gold); margin-right: 12px; font-size: 1.1rem; }
|
||||||
|
|
||||||
|
/* --- PAGE 3: LOCATION (NEW LAYOUT) --- */
|
||||||
|
.location-body { padding: 0; } /* Override default padding */
|
||||||
|
.location-map-container {
|
||||||
|
height: 45%;
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1549880181-56a44cf4a9a5?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center 75%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.location-map-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 70%;
|
||||||
|
background: linear-gradient(180deg, rgba(18, 18, 18, 0) 0%, var(--color-dark-charcoal) 90%);
|
||||||
|
}
|
||||||
|
.location-content { padding: var(--padding-page); }
|
||||||
|
.poi-grid {
|
||||||
|
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 30px; margin-top: 40px;
|
||||||
|
}
|
||||||
|
.poi-item { text-align: center; }
|
||||||
|
.poi-item .icon {
|
||||||
|
font-size: 2.5rem; color: var(--color-accent-gold); margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.poi-item .title {
|
||||||
|
font-weight: 600; font-size: 1rem; color: var(--color-white); margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.poi-item .details { font-size: 0.9rem; color: var(--color-text-secondary); }
|
||||||
|
|
||||||
|
/* --- PAGE 4: LAYOUT & LIFESTYLE (REVISED) --- */
|
||||||
|
.page-split-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.6fr 1fr;
|
||||||
|
gap: 50px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.gallery-section, .additional-info-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.additional-info-section .spec-list {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
.photo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 25px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.photo-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.photo-item-1 {
|
||||||
|
grid-column: 1 / -1; /* Span full width */
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
}
|
||||||
|
.photo-item-2 {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
}
|
||||||
|
.photo-item-3 {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1600585153325-1a75f8a4f631?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="brochure-page cover-page">
|
||||||
|
<div class="cover-overlay"></div>
|
||||||
|
<header class="cover-header"><div class="logo">Elysian</div><div class="property-status">FOR SALE</div></header>
|
||||||
|
<main class="cover-content">
|
||||||
|
<h1 class="cover-title">The Grand Oak Villa</h1>
|
||||||
|
<p class="cover-address"><i class="fa-solid fa-location-dot"></i> 123 Luxury Lane, Prestige City, PC 45678</p>
|
||||||
|
</main>
|
||||||
|
<footer class="cover-footer">
|
||||||
|
<div class="feature-item"><div class="value">5</div><div class="label">Bedrooms</div></div>
|
||||||
|
<div class="feature-item"><div class="value">6</div><div class="label">Bathrooms</div></div>
|
||||||
|
<div class="feature-item"><div class="value">6,200</div><div class="label">Sq. Ft.</div></div>
|
||||||
|
<div class="feature-item"><div class="value">$4,500,000</div><div class="label">Price</div></div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="content-body">
|
||||||
|
<header class="page-header"><h1 class="title">Property <span>Overview</span></h1><span class="property-name">The Grand Oak Villa</span></header>
|
||||||
|
<main class="main-content details-grid">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Description</h2>
|
||||||
|
<div class="description">
|
||||||
|
<p>Nestled in the heart of Prestige City, The Grand Oak Villa is a masterpiece of modern architecture and timeless elegance. This expansive 6,200 sq. ft. residence offers unparalleled luxury and privacy.</p>
|
||||||
|
<p>With soaring ceilings, bespoke finishes, and panoramic views from every room, this home is designed for those who appreciate the finer things in life. The open-plan living space is perfect for entertaining, featuring a gourmet chef's kitchen, a formal dining area, and a grand living room with a statement fireplace.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="specs-and-amenities">
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Specifications</h2>
|
||||||
|
<div class="spec-list">
|
||||||
|
<div class="item"><span class="key">Reference ID:</span> <span class="value">[Reference ID]</span></div>
|
||||||
|
<div class="item"><span class="key">Status:</span> <span class="value">[Status]</span></div>
|
||||||
|
<div class="item"><span class="key">Type:</span> <span class="value">[Property Type]</span></div>
|
||||||
|
<div class="item"><span class="key">Year Built:</span> <span class="value">[Year Built]</span></div>
|
||||||
|
<div class="item"><span class="key">Floor:</span> <span class="value">[Floor]</span></div>
|
||||||
|
<div class="item"><span class="key">Parking:</span> <span class="value">[Parking]</span></div>
|
||||||
|
<div class="item"><span class="key">Furnishing:</span> <span class="value">[Furnishing]</span></div>
|
||||||
|
<div class="item"><span class="key">Maintenance Fee:</span> <span class="value">[Maintenance Fee]</span></div>
|
||||||
|
<div class="item"><span class="key">Service Charge:</span> <span class="value">[Service Charge]</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">Amenities & Features</h2>
|
||||||
|
<ul class="amenities-list">
|
||||||
|
<li><i class="fa-solid fa-check"></i> Infinity Pool</li><li><i class="fa-solid fa-check"></i> Private Home Theater</li><li><i class="fa-solid fa-check"></i> Gourmet Chef's Kitchen</li><li><i class="fa-solid fa-check"></i> Wine Cellar</li><li><i class="fa-solid fa-check"></i> Smart Home Automation</li><li><i class="fa-solid fa-check"></i> Spa & Sauna Room</li><li><i class="fa-solid fa-check"></i> Landscaped Gardens</li><li><i class="fa-solid fa-check"></i> Outdoor Fire Pit</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
|
||||||
|
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="content-body location-body">
|
||||||
|
<div class="location-map-container"></div>
|
||||||
|
<div class="location-content">
|
||||||
|
<header class="page-header" style="margin-bottom: 0;">
|
||||||
|
<h1 class="title">An Unrivaled <span>Setting</span></h1>
|
||||||
|
<span class="property-name">123 Luxury Lane, Prestige City</span>
|
||||||
|
</header>
|
||||||
|
<div class="poi-grid">
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-school"></i></div>
|
||||||
|
<div class="title">Schools</div>
|
||||||
|
<div class="details">[Schools]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-shopping-cart"></i></div>
|
||||||
|
<div class="title">Shopping</div>
|
||||||
|
<div class="details">[Shopping Centers]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-plane"></i></div>
|
||||||
|
<div class="title">Airport</div>
|
||||||
|
<div class="details">[Airport Distance]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-landmark"></i></div>
|
||||||
|
<div class="title">Landmarks</div>
|
||||||
|
<div class="details">[Nearby Landmarks]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-bus"></i></div>
|
||||||
|
<div class="title">Transportation</div>
|
||||||
|
<div class="details">[Transportation]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-hospital"></i></div>
|
||||||
|
<div class="title">Hospitals</div>
|
||||||
|
<div class="details">[Hospitals]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-umbrella-beach"></i></div>
|
||||||
|
<div class="title">Beach</div>
|
||||||
|
<div class="details">[Beach Distance]</div>
|
||||||
|
</div>
|
||||||
|
<div class="poi-item">
|
||||||
|
<div class="icon"><i class="fa-solid fa-subway"></i></div>
|
||||||
|
<div class="title">Metro</div>
|
||||||
|
<div class="details">[Metro Distance]</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
|
||||||
|
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="content-body">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="title">Layout & <span>Lifestyle</span></h1>
|
||||||
|
<span class="property-name">The Grand Oak Villa</span>
|
||||||
|
</header>
|
||||||
|
<main class="main-content page-split-layout">
|
||||||
|
<section class="gallery-section">
|
||||||
|
<h2 class="section-title">A Glimpse Inside</h2>
|
||||||
|
<div class="photo-grid">
|
||||||
|
<div class="photo-item photo-item-1"></div>
|
||||||
|
<div class="photo-item photo-item-2"></div>
|
||||||
|
<div class="photo-item photo-item-3"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="additional-info-section">
|
||||||
|
<h2 class="section-title">Additional Information</h2>
|
||||||
|
<div class="spec-list">
|
||||||
|
<div class="item"><span class="key">Pet Friendly:</span> <span class="value">[Pet Friendly Status]</span></div>
|
||||||
|
<div class="item"><span class="key">Smoking:</span> <span class="value">[Smoking Allowed]</span></div>
|
||||||
|
<div class="item"><span class="key">Available From:</span> <span class="value">[Available From Date]</span></div>
|
||||||
|
<div class="item"><span class="key">Minimum Contract:</span> <span class="value">[Minimum Contract Duration]</span></div>
|
||||||
|
<div class="item"><span class="key">Security Deposit:</span> <span class="value">[Security Deposit]</span></div>
|
||||||
|
<div class="item"><span class="key">Utilities Included:</span> <span class="value">[Utilities Included]</span></div>
|
||||||
|
<div class="item"><span class="key">Internet Included:</span> <span class="value">[Internet Included]</span></div>
|
||||||
|
<div class="item"><span class="key">Cable Included:</span> <span class="value">[Cable Included]</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<div><strong>Agent:</strong> [Agent Name] | [Agent Phone] | [Agent Email]</div>
|
||||||
|
<div><strong>Owner:</strong> [Owner Name] | [Owner Phone] | [Owner Email]</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
426
sample templates/the serenity house.html
Normal file
426
sample templates/the serenity house.html
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Editorial Real Estate Brochure - Updated</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Cormorant+Garamond:wght@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">
|
||||||
|
<style>
|
||||||
|
/* --- DESIGN SYSTEM & VARIABLES --- */
|
||||||
|
:root {
|
||||||
|
/* Color Palette */
|
||||||
|
--color-bg: #FFFFFF;
|
||||||
|
--color-off-white: #F8F7F5;
|
||||||
|
--color-text-primary: #333333;
|
||||||
|
--color-text-secondary: #777777;
|
||||||
|
--color-accent-beige: #D4C7B8;
|
||||||
|
--color-border: #EAEAEA;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-serif: 'Cormorant Garamond', serif;
|
||||||
|
--font-sans: 'Lato', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GLOBAL & BODY STYLES --- */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px;
|
||||||
|
margin: 0;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure-page {
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 1: FRONT COVER --- */
|
||||||
|
.p1-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.p1-image-side {
|
||||||
|
flex: 1.2;
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.p1-content-side {
|
||||||
|
flex: 1;
|
||||||
|
padding: 70px 60px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.p1-header .collection {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.p1-main-title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.p1-address {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-left: 3px solid var(--color-accent-beige);
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.p1-ref-id {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-left: 23px;
|
||||||
|
}
|
||||||
|
.p1-footer {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.p1-footer strong {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.p1-footer .area {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SHARED STYLES for Content Pages --- */
|
||||||
|
.page-layout {
|
||||||
|
padding: 70px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.page-number {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px; right: 70px;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.page-title-main {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.page-title-sub {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.content-divider {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-border);
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 2: INTRODUCTION & NARRATIVE --- */
|
||||||
|
.p2-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.p2-image {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.p2-text p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.p2-text p:first-of-type::first-letter {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 4rem;
|
||||||
|
float: left;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
color: var(--color-accent-beige);
|
||||||
|
}
|
||||||
|
.pull-quote {
|
||||||
|
border-left: 3px solid var(--color-accent-beige);
|
||||||
|
padding-left: 25px;
|
||||||
|
margin: 30px 0;
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 3: DETAILS & AMENITIES --- */
|
||||||
|
.p3-main-content { flex-grow: 1; }
|
||||||
|
.spec-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 30px;
|
||||||
|
padding: 30px;
|
||||||
|
background-color: var(--color-off-white);
|
||||||
|
}
|
||||||
|
.spec-item { text-align: center; }
|
||||||
|
.spec-item .value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
.spec-item .label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px 40px;
|
||||||
|
}
|
||||||
|
.details-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.details-item .label { color: var(--color-text-secondary); }
|
||||||
|
.details-item .value { color: var(--color-text-primary); font-weight: 700; }
|
||||||
|
.amenities-list { list-style: none; padding: 0; column-count: 2; column-gap: 40px;}
|
||||||
|
.amenities-list li {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
-webkit-column-break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
.amenities-list li i { color: var(--color-accent-beige); margin-right: 12px; }
|
||||||
|
|
||||||
|
/* --- PAGE 4: REVISED LAYOUT --- */
|
||||||
|
.p4-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.p4-floorplan-container {
|
||||||
|
height: 320px;
|
||||||
|
background-color: var(--color-off-white);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-image: url('https://cdn.shopify.com/s/files/1/0024/0495/3953/files/Architect_s_floor_plan_for_a_house_in_black_and_white_large.jpg');
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.p4-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.info-list .info-item, .location-list .item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
.info-list .info-item strong, .location-list .item strong {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.p4-contact-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.contact-card {
|
||||||
|
background-color: var(--color-off-white);
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
.contact-card .title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.contact-card .name { font-family: var(--font-serif); font-size: 1.5rem; font-weight: 600; }
|
||||||
|
.contact-card .phone, .contact-card .email { font-size: 0.9rem; margin: 4px 0; color: var(--color-text-secondary); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="p1-container">
|
||||||
|
<div class="p1-image-side"></div>
|
||||||
|
<div class="p1-content-side">
|
||||||
|
<header class="p1-header">
|
||||||
|
<div class="collection">Elysian Estates Collection</div>
|
||||||
|
<h1 class="p1-main-title">The Serenity House</h1>
|
||||||
|
<p class="p1-address">123 Luxury Lane, Prestige City, PC 45678</p>
|
||||||
|
<p class="p1-ref-id">Reference ID: ES-8821</p>
|
||||||
|
</header>
|
||||||
|
<footer class="p1-footer">
|
||||||
|
<div class="area">6,200 Sq. Ft. • 5 Bedrooms • 6 Bathrooms</div>
|
||||||
|
An architectural marvel of curated living space.
|
||||||
|
<br>
|
||||||
|
<strong>Offered at $4,500,000</strong>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-layout">
|
||||||
|
<span class="page-number">02</span>
|
||||||
|
<h1 class="page-title-main">A Sanctuary of Modern Design</h1>
|
||||||
|
<p class="page-title-sub">Where light, space, and nature converge to create an unparalleled living experience.</p>
|
||||||
|
<div class="p2-grid">
|
||||||
|
<div class="p2-text">
|
||||||
|
<p>Designed by the world-renowned architect, Helena Vance, The Serenity House is more than a home; it is a living sculpture. Every line, material, and detail has been thoughtfully considered to evoke a sense of peace and connection with the surrounding landscape. Soaring ceilings and floor-to-ceiling glass walls dissolve the boundaries between inside and out, flooding the space with natural light.</p>
|
||||||
|
<p class="pull-quote">A timeless residence built not just for living, but for thriving.</p>
|
||||||
|
<p>The interior palette is a harmonious blend of natural oak, Italian travertine, and warm bronze accents, creating an atmosphere of understated luxury. This property represents a unique opportunity to own a piece of architectural history.</p>
|
||||||
|
</div>
|
||||||
|
<div class="p2-image"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-layout">
|
||||||
|
<span class="page-number">03</span>
|
||||||
|
<h1 class="page-title-main">Property Specifications</h1>
|
||||||
|
<p class="page-title-sub">A comprehensive overview of the property’s features, details, and amenities.</p>
|
||||||
|
|
||||||
|
<div class="p3-main-content">
|
||||||
|
<div class="spec-grid">
|
||||||
|
<div class="spec-item"><div class="value">5</div><div class="label">Bedrooms</div></div>
|
||||||
|
<div class="spec-item"><div class="value">6</div><div class="label">Bathrooms</div></div>
|
||||||
|
<div class="spec-item"><div class="value">6,200</div><div class="label">Square Feet</div></div>
|
||||||
|
<div class="spec-item"><div class="value">0.75</div><div class="label">Acres</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="content-divider">
|
||||||
|
|
||||||
|
<h3 class="section-title">Property Details</h3>
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="details-item"><span class="label">Status</span><span class="value">For Sale</span></div>
|
||||||
|
<div class="details-item"><span class="label">Year Built</span><span class="value">2023</span></div>
|
||||||
|
<div class="details-item"><span class="label">Type</span><span class="value">Single-Family Home</span></div>
|
||||||
|
<div class="details-item"><span class="label">Furnishing</span><span class="value">Partially Furnished</span></div>
|
||||||
|
<div class="details-item"><span class="label">Floor</span><span class="value">2 Levels</span></div>
|
||||||
|
<div class="details-item"><span class="label">Maintenance Fee</span><span class="value">$1,200 / month</span></div>
|
||||||
|
<div class="details-item"><span class="label">Parking</span><span class="value">3-Car Garage</span></div>
|
||||||
|
<div class="details-item"><span class="label">Service Charge</span><span class="value">Included</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="content-divider">
|
||||||
|
|
||||||
|
<h3 class="section-title">Amenities & Features</h3>
|
||||||
|
<ul class="amenities-list">
|
||||||
|
<li><i class="fa-solid fa-check"></i> Primary Suite with Spa-Bath</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Radiant Heated Flooring</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Custom Walk-in Closets</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Smart Home Automation</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Infinity Edge Saline Pool</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Private Cinema Room</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Temperature-Controlled Wine Cellar</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Landscaped Gardens & Terrace</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Gourmet Chef's Kitchen</li>
|
||||||
|
<li><i class="fa-solid fa-check"></i> Floor-to-Ceiling Glass Walls</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-layout">
|
||||||
|
<span class="page-number">04</span>
|
||||||
|
<h1 class="page-title-main" style="margin-bottom: 30px;">Floor Plan & Details</h1>
|
||||||
|
|
||||||
|
<div class="p4-info-grid">
|
||||||
|
<div class="location-list">
|
||||||
|
<h2 class="p4-section-title">Location & Nearby</h2>
|
||||||
|
<div class="item"><strong>Schools</strong> <span>5 min drive</span></div>
|
||||||
|
<div class="item"><strong>Shopping</strong> <span>10 min drive</span></div>
|
||||||
|
<div class="item"><strong>Hospitals</strong> <span>12 min drive</span></div>
|
||||||
|
<div class="item"><strong>Country Club</strong> <span>8 min drive</span></div>
|
||||||
|
<div class="item"><strong>Airport</strong> <span>20 min drive</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="info-list">
|
||||||
|
<h2 class="p4-section-title">Additional Information</h2>
|
||||||
|
<div class="info-item"><strong>Pet-Friendly</strong> <span>By Approval</span></div>
|
||||||
|
<div class="info-item"><strong>Smoking</strong> <span>Not Permitted</span></div>
|
||||||
|
<div class="info-item"><strong>Availability</strong> <span>Immediate</span></div>
|
||||||
|
<div class="info-item"><strong>Utilities</strong> <span>Not Included</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="content-divider">
|
||||||
|
|
||||||
|
<h2 class="p4-section-title">Floor Plan & Location</h2>
|
||||||
|
<div class="p4-floorplan-container"></div>
|
||||||
|
|
||||||
|
<div class="p4-contact-row">
|
||||||
|
<div class="contact-card">
|
||||||
|
<div class="title">Owner Information</div>
|
||||||
|
<div class="name">John & Jane Doe</div>
|
||||||
|
<p class="phone">(555) 111-2222</p>
|
||||||
|
<p class="email">owner.serenity@email.com</p>
|
||||||
|
</div>
|
||||||
|
<div class="contact-card">
|
||||||
|
<div class="title">Agent Information</div>
|
||||||
|
<div class="name">Olivia Sterling</div>
|
||||||
|
<p class="phone">(555) 987-6543</p>
|
||||||
|
<p class="email">olivia@elysianestates.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
604
sample templates/the vertice.html
Normal file
604
sample templates/the vertice.html
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Modern Urban Residences Brochure - Updated</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700;800&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
/* --- DESIGN SYSTEM & VARIABLES --- */
|
||||||
|
:root {
|
||||||
|
/* Color Palette */
|
||||||
|
--color-dark: #111111;
|
||||||
|
--color-light: #FFFFFF;
|
||||||
|
--color-accent-green: #0A6847;
|
||||||
|
--color-grey-bg: #F0F0F0;
|
||||||
|
--color-text-dark: #222222;
|
||||||
|
--color-text-light: #EFEFEF;
|
||||||
|
--color-text-muted: #888888;
|
||||||
|
--color-border: #DDDDDD;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-main: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GLOBAL & BODY STYLES --- */
|
||||||
|
body {
|
||||||
|
font-family: var(--font-main);
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px;
|
||||||
|
margin: 0;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brochure-page {
|
||||||
|
width: 210mm;
|
||||||
|
height: 297mm;
|
||||||
|
background-color: var(--color-light);
|
||||||
|
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 1: COVER PAGE --- */
|
||||||
|
.cover-page {
|
||||||
|
background-image: url('https://plus.unsplash.com/premium_photo-1677474827617-6a7269f97574?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
color: var(--color-light);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.cover-overlay {
|
||||||
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
.cover-content {
|
||||||
|
position: relative; z-index: 2; padding: 50px;
|
||||||
|
}
|
||||||
|
.cover-content .subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
.cover-content .main-title {
|
||||||
|
font-size: 6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.cover-content .address {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
border-top: 1px solid var(--color-accent-green);
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.cover-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 40px; right: 40px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SHARED STYLES --- */
|
||||||
|
.page-container {
|
||||||
|
padding: 70px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
.page-title span {
|
||||||
|
color: var(--color-accent-green);
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.page-footer-bar {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.page-footer-bar .property-name {
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-dark);
|
||||||
|
margin-bottom: 25px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.detail-item .label { color: var(--color-text-muted); }
|
||||||
|
.detail-item .value { color: var(--color-text-dark); font-weight: 600; text-align: right; }
|
||||||
|
|
||||||
|
/* --- PAGE 2: THE VISION --- */
|
||||||
|
.vision-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.vision-image {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1626704359446-0de90350b4e7?q=80&w=736&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.vision-text h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-dark);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.vision-text p {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 3: RESIDENCES GALLERY --- */
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: 250px 250px 250px;
|
||||||
|
gap: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
.gallery-item {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
color: var(--color-light);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.gallery-item::after {
|
||||||
|
content: ''; position: absolute; top:0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
||||||
|
}
|
||||||
|
.gallery-item span { font-weight: 600; z-index: 2; }
|
||||||
|
.g-item-1 { grid-column: 1 / 3; grid-row: 1 / 2; background-image: url('https://images.unsplash.com/photo-1616046229478-9901c5536a45?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||||
|
.g-item-2 { grid-column: 3 / 4; grid-row: 1 / 3; background-image: url('https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||||
|
.g-item-3 { grid-column: 1 / 2; grid-row: 2 / 4; background-image: url('https://images.unsplash.com/photo-1600121848594-d8644e57abab?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||||
|
.g-item-4 { grid-column: 2 / 3; grid-row: 2 / 3; background-image: url('https://images.unsplash.com/photo-1595526114035-0d45ed16433d?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||||
|
.g-item-5 { grid-column: 2 / 4; grid-row: 3 / 4; background-image: url('https://images.unsplash.com/photo-1512918728675-ed5a71a580a9?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||||
|
|
||||||
|
/* --- PAGE 4: AMENITIES & FEATURES (REVISED V2) --- */
|
||||||
|
.amenities-intro {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
}
|
||||||
|
.page4-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.2fr;
|
||||||
|
gap: 50px;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.page4-image {
|
||||||
|
background-image: url('https://plus.unsplash.com/premium_photo-1675745330187-a6f001a21abe?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
min-height: 100%;
|
||||||
|
border: 6px solid var(--color-accent-green);
|
||||||
|
}
|
||||||
|
.page4-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.amenities-list-p4 {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
column-count: 2;
|
||||||
|
column-gap: 30px;
|
||||||
|
}
|
||||||
|
.amenities-list-p4 li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
-webkit-column-break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
.amenities-list-p4 i {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-accent-green);
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 20px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 5: FLOOR PLANS & SPECS (REVISED V2) --- */
|
||||||
|
.page5-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
.floorplan-showcase {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px 1fr;
|
||||||
|
gap: 25px;
|
||||||
|
background-color: var(--color-grey-bg);
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 5px solid var(--color-accent-green);
|
||||||
|
}
|
||||||
|
.floorplan-image-p5 {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.floorplan-image-p5.residence {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1740446568651-1d31966b228a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
}
|
||||||
|
.floorplan-image-p5.penthouse {
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1721522281545-fad32dd5107a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||||
|
}
|
||||||
|
.floorplan-info-p5 h4 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
.floorplan-stats-p5 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.floorplan-stats-p5 .stat .value {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.floorplan-stats-p5 .stat .label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.floorplan-info-p5 .description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.additional-specs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.spec-item {
|
||||||
|
background-color: var(--color-grey-bg);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.spec-item .label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.spec-item .value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PAGE 6: LOCATION & CONTACT (REVISED V3) --- */
|
||||||
|
.page6-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
align-content: start;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.page6-main p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.contact-person-p6 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.contact-person-p6 .name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
.contact-person-p6 .title {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.contact-person-p6 .details {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.highlights-list-p6 {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.highlights-list-p6 li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.highlights-list-p6 i {
|
||||||
|
color: var(--color-accent-green);
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 20px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="brochure-page cover-page">
|
||||||
|
<div class="cover-overlay"></div>
|
||||||
|
<div class="cover-content">
|
||||||
|
<div class="subtitle">An Urban Oasis</div>
|
||||||
|
<h1 class="main-title">THE VERTICE</h1>
|
||||||
|
<div class="address">18 Skyline Avenue, Metropolis Centre, MC 90210</div>
|
||||||
|
</div>
|
||||||
|
<div class="cover-footer">
|
||||||
|
<span>Residences Starting from $1,200,000</span>
|
||||||
|
<span>Ref ID: VP-2025-001</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Elevated <span>Living</span></h1>
|
||||||
|
<span class="page-subtitle">Discover Your Sanctuary in the Sky</span>
|
||||||
|
</header>
|
||||||
|
<main class="vision-grid">
|
||||||
|
<div class="vision-text">
|
||||||
|
<h3>Where Design Meets Desire.</h3>
|
||||||
|
<p>The Vertice is not just a building; it's a bold statement on modern urban living. Conceived for the discerning individual, it offers a unique blend of architectural prowess, bespoke interiors, and an unparalleled lifestyle experience right in the heart of the city.</p>
|
||||||
|
<p>Every residence is a testament to quality, featuring panoramic city views from floor-to-ceiling windows, intelligent home systems, and finishes selected from the finest materials around the globe. This is more than a home; it's a new perspective.</p>
|
||||||
|
</div>
|
||||||
|
<div class="vision-image"></div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer-bar">
|
||||||
|
<span class="property-name">THE VERTICE</span>
|
||||||
|
<span>Page 02 / 06</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Exquisite <span>Interiors</span></h1>
|
||||||
|
<span class="page-subtitle">A Canvas for Your Life</span>
|
||||||
|
</header>
|
||||||
|
<main class="gallery-grid">
|
||||||
|
<div class="gallery-item g-item-1"><span>Open-Concept Living Space</span></div>
|
||||||
|
<div class="gallery-item g-item-2"><span>Master Bedroom Suite</span></div>
|
||||||
|
<div class="gallery-item g-item-3"><span>Gourmet Chef's Kitchen</span></div>
|
||||||
|
<div class="gallery-item g-item-4"><span>Spa-Inspired Bathroom</span></div>
|
||||||
|
<div class="gallery-item g-item-5"><span>Private Balcony Views</span></div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer-bar">
|
||||||
|
<span class="property-name">THE VERTICE</span>
|
||||||
|
<span>Page 03 / 06</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Amenities & <span>Features</span></h1>
|
||||||
|
<span class="page-subtitle">Beyond the Expected</span>
|
||||||
|
</header>
|
||||||
|
<p class="amenities-intro">An unrivaled collection of amenities offers residents a resort-style living experience. From the serene rooftop pool to the state-of-the-art wellness center, every detail is crafted for comfort, convenience, and luxury.</p>
|
||||||
|
<main class="page4-grid">
|
||||||
|
<div class="page4-image"></div>
|
||||||
|
<div class="page4-content">
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title">Lifestyle Amenities</h3>
|
||||||
|
<ul class="amenities-list-p4">
|
||||||
|
<li><i class="fa-solid fa-water-ladder"></i> Rooftop Infinity Pool</li>
|
||||||
|
<li><i class="fa-solid fa-dumbbell"></i> Fitness Center</li>
|
||||||
|
<li><i class="fa-solid fa-martini-glass-empty"></i> Residents' Sky Lounge</li>
|
||||||
|
<li><i class="fa-solid fa-film"></i> Private Cinema Room</li>
|
||||||
|
<li><i class="fa-solid fa-spa"></i> Wellness Spa & Sauna</li>
|
||||||
|
<li><i class="fa-solid fa-briefcase"></i> Business Center</li>
|
||||||
|
<li><i class="fa-solid fa-shield-halved"></i> 24/7 Concierge</li>
|
||||||
|
<li><i class="fa-solid fa-car"></i> Secure Parking</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title">Key Specifications</h3>
|
||||||
|
<div class="detail-item"><span class="label">Status</span> <span class="value">New Development</span></div>
|
||||||
|
<div class="detail-item"><span class="label">Property Type</span> <span class="value">Condominium</span></div>
|
||||||
|
<div class="detail-item"><span class="label">Year Built</span> <span class="value">2025</span></div>
|
||||||
|
<div class="detail-item"><span class="label">Technology</span> <span class="value">Integrated Smart Home</span></div>
|
||||||
|
<div class="detail-item"><span class="label">Design</span> <span class="value">Sustainable & Eco-Friendly</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer-bar">
|
||||||
|
<span class="property-name">THE VERTICE</span>
|
||||||
|
<span>Page 04 / 06</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Floor Plans & <span>Details</span></h1>
|
||||||
|
<span class="page-subtitle">Designed for Modern Life</span>
|
||||||
|
</header>
|
||||||
|
<main class="page5-main">
|
||||||
|
<div class="floorplan-showcase">
|
||||||
|
<div class="floorplan-image-p5 residence"></div>
|
||||||
|
<div class="floorplan-info-p5">
|
||||||
|
<h4>Two-Bedroom Residence</h4>
|
||||||
|
<div class="floorplan-stats-p5">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">1,450</div>
|
||||||
|
<div class="label">SQ. FT.</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">2</div>
|
||||||
|
<div class="label">BEDROOMS</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">2</div>
|
||||||
|
<div class="label">BATHROOMS</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">1</div>
|
||||||
|
<div class="label">BALCONY</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="description">A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="floorplan-showcase">
|
||||||
|
<div class="floorplan-image-p5 penthouse"></div>
|
||||||
|
<div class="floorplan-info-p5">
|
||||||
|
<h4>Three-Bedroom Penthouse</h4>
|
||||||
|
<div class="floorplan-stats-p5">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">3,200</div>
|
||||||
|
<div class="label">SQ. FT.</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">3</div>
|
||||||
|
<div class="label">BEDROOMS</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">3.5</div>
|
||||||
|
<div class="label">BATHROOMS</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="value">1</div>
|
||||||
|
<div class="label">TERRACE</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="description">The pinnacle of luxury living, this penthouse offers expansive spaces, premium finishes, and exclusive access to a private rooftop terrace.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title">Additional Information</h3>
|
||||||
|
<div class="additional-specs-grid">
|
||||||
|
<div class="spec-item"><div class="label">Pets</div><div class="value">Allowed (w/ restrictions)</div></div>
|
||||||
|
<div class="spec-item"><div class="label">Smoking</div><div class="value">In designated areas</div></div>
|
||||||
|
<div class="spec-item"><div class="label">Availability</div><div class="value">Q4 2025</div></div>
|
||||||
|
<div class="spec-item"><div class="label">Parking</div><div class="value">2 Spaces per Unit</div></div>
|
||||||
|
<div class="spec-item"><div class="label">Security Deposit</div><div class="value">2 Months</div></div>
|
||||||
|
<div class="spec-item"><div class="label">Utilities</div><div class="value">Sub-metered</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer-bar">
|
||||||
|
<span class="property-name">THE VERTICE</span>
|
||||||
|
<span>Page 05 / 06</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brochure-page">
|
||||||
|
<div class="page-container">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 class="page-title">Location & <span>Inquiry</span></h1>
|
||||||
|
<span class="page-subtitle">Your Future Awaits</span>
|
||||||
|
</header>
|
||||||
|
<main class="page6-main">
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title">Schedule a Private Viewing</h3>
|
||||||
|
<p>Experience The Vertice firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.</p>
|
||||||
|
<div class="contact-person-p6">
|
||||||
|
<div class="name">Alexander Valentine</div>
|
||||||
|
<div class="title">Sales Executive, Elysian Properties</div>
|
||||||
|
<div class="details">
|
||||||
|
(555) 123-9876<br>
|
||||||
|
alex.v@elysian.com
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="section-title">Neighborhood Highlights</h3>
|
||||||
|
<ul class="highlights-list-p6">
|
||||||
|
<li><i class="fa-solid fa-tree-city"></i> <strong>Landmarks:</strong> Central Park (5 min)</li>
|
||||||
|
<li><i class="fa-solid fa-train-subway"></i> <strong>Transportation:</strong> Metro Line A (2 min walk)</li>
|
||||||
|
<li><i class="fa-solid fa-school"></i> <strong>Schools:</strong> Metropolis Intl. (10 min)</li>
|
||||||
|
<li><i class="fa-solid fa-cart-shopping"></i> <strong>Shopping:</strong> The Galleria Mall (8 min)</li>
|
||||||
|
<li><i class="fa-solid fa-plane-departure"></i> <strong>Airport:</strong> 25 min drive</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="page-footer-bar">
|
||||||
|
<span class="property-name">THE VERTICE</span>
|
||||||
|
<span>Page 06 / 06</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user