v1.0.0-alpha

This commit is contained in:
rohit 2025-08-30 17:07:35 +05:30
parent 355f787a81
commit b349cd2cdd
24 changed files with 7840 additions and 9806 deletions

View File

@ -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!**

View File

@ -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
View 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"

View 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());
}
}
}

View File

@ -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>

View File

@ -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());
}
}
} }

View File

@ -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);

View File

@ -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 one or more lines are too long

1262
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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()

View File

@ -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
View 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

View File

@ -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

View File

@ -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)

View File

@ -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()

View 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>

View 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>

View 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 propertys 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>

View 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>