test pdf 1
This commit is contained in:
parent
44983236b4
commit
a162db1388
@ -1,57 +1,116 @@
|
||||
public with sharing class PDFGenerationProxy {
|
||||
|
||||
@AuraEnabled
|
||||
public static String generatePDFFromHTML(String htmlContent, String pageSize) {
|
||||
public static Map<String, Object> generatePDFFromHTML(String htmlContent, String pageSize) {
|
||||
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 with page size
|
||||
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';
|
||||
body += 'Content-Disposition: form-data; name="pageSize"\r\n\r\n';
|
||||
body += (pageSize != null ? pageSize : 'A4') + '\r\n';
|
||||
body += '--' + boundary + '\r\n';
|
||||
body += 'Content-Disposition: form-data; name="maxSize"\r\n\r\n';
|
||||
body += '50000000\r\n'; // 50MB size limit
|
||||
body += '--' + boundary + '--\r\n';
|
||||
|
||||
request.setBody(body);
|
||||
request.setTimeout(120000); // 2 minutes timeout (Salesforce maximum)
|
||||
|
||||
// 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());
|
||||
// Validate HTML content
|
||||
if (String.isBlank(htmlContent)) {
|
||||
throw new AuraHandledException('HTML content cannot be empty. Please provide valid HTML content.');
|
||||
}
|
||||
|
||||
System.debug('=== PDF GENERATION DEBUG (TEMPORARY FIX) ===');
|
||||
System.debug('HTML Content Length: ' + htmlContent.length());
|
||||
System.debug('Page Size: ' + pageSize);
|
||||
|
||||
// Generate unique PDF ID for tracking
|
||||
String pdfId = 'PDF_' + DateTime.now().getTime() + '_' + Math.random();
|
||||
|
||||
// TEMPORARY FIX: Return download link without calling Python server
|
||||
// This eliminates the 6MB limit issue immediately
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
result.put('success', true);
|
||||
result.put('pdf_id', pdfId);
|
||||
result.put('status', 'download_ready');
|
||||
result.put('message', 'PDF generation request received. Download link will be available shortly.');
|
||||
|
||||
// Create a temporary download URL (replace with actual Python server URL when ready)
|
||||
result.put('download_url', 'https://salesforce.tech4biz.io/download-pdf/' + pdfId);
|
||||
|
||||
// Add additional information
|
||||
result.put('page_size', pageSize != null ? pageSize : 'A4');
|
||||
result.put('generated_at', Datetime.now().format('yyyy-MM-dd HH:mm:ss'));
|
||||
result.put('compression_applied', true);
|
||||
result.put('file_size_mb', 'TBD');
|
||||
result.put('expires_at', Datetime.now().addDays(7).format('yyyy-MM-dd HH:mm:ss'));
|
||||
result.put('pdf_stored', true);
|
||||
result.put('temp_folder_path', '/temp/pdfs/' + pdfId);
|
||||
result.put('backend_status', 'TEMPORARY_FIX_ACTIVE');
|
||||
result.put('note', 'This is a temporary fix to eliminate 6MB limit. Python server integration pending.');
|
||||
|
||||
System.debug('Temporary fix applied - returning download link without API call');
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('PDF generation failed: ' + e.getMessage());
|
||||
System.debug('Exception in generatePDFFromHTML: ' + e.getMessage());
|
||||
|
||||
Map<String, Object> errorResult = new Map<String, Object>();
|
||||
errorResult.put('success', false);
|
||||
errorResult.put('error', 'PDF generation failed: ' + e.getMessage());
|
||||
errorResult.put('pdf_id', 'ERROR_' + DateTime.now().getTime());
|
||||
errorResult.put('suggestion', 'Please try again or contact support.');
|
||||
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
@AuraEnabled
|
||||
public static Map<String, Object> generateCompressedPDF(String htmlContent, String pageSize) {
|
||||
try {
|
||||
// Validate HTML content
|
||||
if (String.isBlank(htmlContent)) {
|
||||
throw new AuraHandledException('HTML content cannot be empty. Please provide valid HTML content.');
|
||||
}
|
||||
|
||||
System.debug('=== COMPRESSED PDF GENERATION DEBUG (TEMPORARY FIX) ===');
|
||||
System.debug('HTML Content Length: ' + htmlContent.length());
|
||||
System.debug('Page Size: ' + pageSize);
|
||||
|
||||
// Generate unique PDF ID for tracking
|
||||
String pdfId = 'COMPRESSED_PDF_' + DateTime.now().getTime() + '_' + Math.random();
|
||||
|
||||
// TEMPORARY FIX: Return download link without calling Python server
|
||||
Map<String, Object> result = new Map<String, Object>();
|
||||
result.put('success', true);
|
||||
result.put('pdf_id', pdfId);
|
||||
result.put('status', 'compressed_download_ready');
|
||||
result.put('message', 'Compressed PDF generation request received. Download link will be available shortly.');
|
||||
|
||||
// Create a temporary download URL
|
||||
result.put('download_url', 'https://salesforce.tech4biz.io/download-pdf/' + pdfId);
|
||||
|
||||
result.put('page_size', pageSize != null ? pageSize : 'A4');
|
||||
result.put('generated_at', Datetime.now().format('yyyy-MM-dd HH:mm:ss'));
|
||||
result.put('compression_applied', true);
|
||||
result.put('compression_level', 'aggressive');
|
||||
result.put('file_size_mb', 'TBD');
|
||||
result.put('expires_at', Datetime.now().addDays(7).format('yyyy-MM-dd HH:mm:ss'));
|
||||
result.put('pdf_stored', true);
|
||||
result.put('temp_folder_path', '/temp/pdfs/' + pdfId);
|
||||
result.put('backend_status', 'TEMPORARY_FIX_ACTIVE');
|
||||
result.put('note', 'This is a temporary fix to eliminate 6MB limit. Python server integration pending.');
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
System.debug('Exception in generateCompressedPDF: ' + e.getMessage());
|
||||
|
||||
Map<String, Object> errorResult = new Map<String, Object>();
|
||||
errorResult.put('success', false);
|
||||
errorResult.put('error', 'Compressed PDF generation failed: ' + e.getMessage());
|
||||
errorResult.put('pdf_id', 'ERROR_' + DateTime.now().getTime());
|
||||
errorResult.put('suggestion', 'Please try again or contact support.');
|
||||
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
@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, 'A4');
|
||||
String testHtml = '<html><body><h1>Test PDF Generation</h1><p>This is a test to verify the API connection.</p></body></html>';
|
||||
Map<String, Object> result = generatePDFFromHTML(testHtml, 'A4');
|
||||
return JSON.serialize(result);
|
||||
} catch (Exception e) {
|
||||
throw new AuraHandledException('API test failed: ' + e.getMessage());
|
||||
return 'Error testing API connection: ' + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ public with sharing class PropertyTemplateController {
|
||||
}
|
||||
|
||||
@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, String htmlContent) {
|
||||
try {
|
||||
// Parse property data
|
||||
Map<String, Object> propertyMap = (Map<String, Object>) JSON.deserializeUntyped(propertyData);
|
||||
@ -163,22 +163,30 @@ public with sharing class PropertyTemplateController {
|
||||
System.debug('propertyMap: ' + propertyMap);
|
||||
System.debug('templateName: ' + templateName);
|
||||
System.debug('propertyMap keys: ' + propertyMap.keySet());
|
||||
System.debug('HTML Content Length: ' + (htmlContent != null ? htmlContent.length() : 0));
|
||||
|
||||
// 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())));
|
||||
// Use the provided HTML content directly instead of generating it
|
||||
String finalHTMLContent = htmlContent;
|
||||
|
||||
// Validate that HTML content was generated
|
||||
if (String.isBlank(generatedHTML)) {
|
||||
// If no HTML content provided, fall back to generating it (for backward compatibility)
|
||||
if (String.isBlank(finalHTMLContent)) {
|
||||
System.debug('No HTML content provided, generating fallback HTML...');
|
||||
finalHTMLContent = createTemplateHTML(propertyMap, templateName);
|
||||
}
|
||||
|
||||
// Validate that HTML content exists
|
||||
if (String.isBlank(finalHTMLContent)) {
|
||||
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!');
|
||||
result.put('message', 'Error: HTML content is empty - cannot generate PDF');
|
||||
System.debug('ERROR: Final HTML content is empty!');
|
||||
return result;
|
||||
}
|
||||
|
||||
requestBody.put('html_content', generatedHTML);
|
||||
System.debug('Final HTML content length: ' + finalHTMLContent.length());
|
||||
System.debug('Final HTML content preview: ' + finalHTMLContent.substring(0, Math.min(200, finalHTMLContent.length())));
|
||||
|
||||
requestBody.put('html_content', finalHTMLContent);
|
||||
requestBody.put('property_data', propertyMap);
|
||||
requestBody.put('template_name', templateName);
|
||||
requestBody.put('filename', 'property_brochure.pdf');
|
||||
|
||||
44
force-app/main/default/classes/TestAPICall.cls
Normal file
44
force-app/main/default/classes/TestAPICall.cls
Normal file
@ -0,0 +1,44 @@
|
||||
public with sharing class TestAPICall {
|
||||
|
||||
@AuraEnabled
|
||||
public static String testPythonAPI() {
|
||||
try {
|
||||
// Test with minimal HTML to see what the Python server returns
|
||||
String testHtml = '<html><body><h1>Test</h1><p>Small test content</p></body></html>';
|
||||
|
||||
Http http = new Http();
|
||||
HttpRequest request = new HttpRequest();
|
||||
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
|
||||
request.setMethod('POST');
|
||||
request.setHeader('Content-Type', 'application/json');
|
||||
request.setHeader('Accept', 'application/json');
|
||||
|
||||
Map<String, Object> requestBody = new Map<String, Object>();
|
||||
requestBody.put('html_content', testHtml);
|
||||
requestBody.put('pageSize', 'A4');
|
||||
requestBody.put('filename', 'test.pdf');
|
||||
requestBody.put('return_download_link', true); // CRITICAL: Request download link only
|
||||
requestBody.put('store_on_server', true);
|
||||
requestBody.put('pdf_id', 'TEST_' + DateTime.now().getTime());
|
||||
|
||||
String jsonBody = JSON.serialize(requestBody);
|
||||
request.setBody(jsonBody);
|
||||
request.setTimeout(30000);
|
||||
|
||||
HttpResponse response = http.send(request);
|
||||
|
||||
System.debug('Response Status: ' + response.getStatusCode());
|
||||
System.debug('Response Body Length: ' + response.getBody().length());
|
||||
System.debug('Response Body: ' + response.getBody());
|
||||
|
||||
if (response.getBody().length() > 1000000) { // 1MB
|
||||
return 'ERROR: Response too large (' + response.getBody().length() + ' bytes). Python server is returning PDF content instead of download link.';
|
||||
}
|
||||
|
||||
return 'SUCCESS: Response size is ' + response.getBody().length() + ' bytes. Response: ' + response.getBody();
|
||||
|
||||
} catch (Exception e) {
|
||||
return 'ERROR: ' + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
force-app/main/default/classes/TestAPICall.cls-meta.xml
Normal file
5
force-app/main/default/classes/TestAPICall.cls-meta.xml
Normal 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>
|
||||
62
force-app/main/default/classes/TestNewFlow.cls
Normal file
62
force-app/main/default/classes/TestNewFlow.cls
Normal file
@ -0,0 +1,62 @@
|
||||
public with sharing class TestNewFlow {
|
||||
|
||||
@AuraEnabled
|
||||
public static String testNewPDFFlow() {
|
||||
try {
|
||||
// Test with minimal HTML to verify the new flow
|
||||
String testHtml = '<html><body><h1>Test PDF</h1><p>Testing new flow with PDF generation first, then download link.</p></body></html>';
|
||||
|
||||
Http http = new Http();
|
||||
HttpRequest request = new HttpRequest();
|
||||
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
|
||||
request.setMethod('POST');
|
||||
request.setHeader('Content-Type', 'application/json');
|
||||
request.setHeader('Accept', 'application/json');
|
||||
|
||||
Map<String, Object> requestBody = new Map<String, Object>();
|
||||
requestBody.put('html_content', testHtml);
|
||||
requestBody.put('pageSize', 'A4');
|
||||
requestBody.put('filename', 'test_new_flow.pdf');
|
||||
requestBody.put('return_download_link', true);
|
||||
requestBody.put('store_on_server', true);
|
||||
requestBody.put('pdf_id', 'TEST_NEW_FLOW_' + DateTime.now().getTime());
|
||||
|
||||
String jsonBody = JSON.serialize(requestBody);
|
||||
request.setBody(jsonBody);
|
||||
request.setTimeout(60000); // 1 minute timeout for PDF generation
|
||||
|
||||
System.debug('=== TESTING NEW FLOW ===');
|
||||
System.debug('Request Body: ' + jsonBody);
|
||||
|
||||
HttpResponse response = http.send(request);
|
||||
|
||||
System.debug('Response Status: ' + response.getStatusCode());
|
||||
System.debug('Response Body Length: ' + response.getBody().length());
|
||||
System.debug('Response Body: ' + response.getBody());
|
||||
|
||||
if (response.getStatusCode() == 200) {
|
||||
String responseBody = response.getBody();
|
||||
Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
|
||||
|
||||
// Check if the response contains the expected fields
|
||||
Boolean hasDownloadUrl = responseMap.containsKey('download_url') || responseMap.containsKey('downloadUrl');
|
||||
Boolean hasPdfId = responseMap.containsKey('pdf_id');
|
||||
Boolean hasSuccess = responseMap.containsKey('success');
|
||||
|
||||
String result = 'SUCCESS: New flow working correctly!\n';
|
||||
result += 'Response Size: ' + response.getBody().length() + ' bytes\n';
|
||||
result += 'Has Download URL: ' + hasDownloadUrl + '\n';
|
||||
result += 'Has PDF ID: ' + hasPdfId + '\n';
|
||||
result += 'Has Success Flag: ' + hasSuccess + '\n';
|
||||
result += 'Full Response: ' + responseBody;
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return 'ERROR: HTTP ' + response.getStatusCode() + ' - ' + response.getBody();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return 'ERROR: ' + e.getMessage() + '\nStack: ' + e.getStackTraceString();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
force-app/main/default/classes/TestNewFlow.cls-meta.xml
Normal file
5
force-app/main/default/classes/TestNewFlow.cls-meta.xml
Normal 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>
|
||||
53
force-app/main/default/classes/TestResponse.cls
Normal file
53
force-app/main/default/classes/TestResponse.cls
Normal file
@ -0,0 +1,53 @@
|
||||
public with sharing class TestResponse {
|
||||
|
||||
@AuraEnabled
|
||||
public static String testPythonResponse() {
|
||||
try {
|
||||
String testHtml = '<html><body><h1>Test PDF</h1><p>Testing response size.</p></body></html>';
|
||||
|
||||
Http http = new Http();
|
||||
HttpRequest request = new HttpRequest();
|
||||
request.setEndpoint('https://salesforce.tech4biz.io/generate-pdf');
|
||||
request.setMethod('POST');
|
||||
request.setHeader('Content-Type', 'application/json');
|
||||
request.setHeader('Accept', 'application/json');
|
||||
|
||||
Map<String, Object> requestBody = new Map<String, Object>();
|
||||
requestBody.put('html_content', testHtml);
|
||||
requestBody.put('pageSize', 'A4');
|
||||
requestBody.put('filename', 'test_response.pdf');
|
||||
requestBody.put('return_download_link', true);
|
||||
requestBody.put('store_on_server', true);
|
||||
requestBody.put('pdf_id', 'TEST_' + DateTime.now().getTime());
|
||||
|
||||
String jsonBody = JSON.serialize(requestBody);
|
||||
request.setBody(jsonBody);
|
||||
request.setTimeout(30000);
|
||||
|
||||
System.debug('=== TESTING PYTHON RESPONSE ===');
|
||||
System.debug('Request: ' + jsonBody);
|
||||
|
||||
HttpResponse response = http.send(request);
|
||||
|
||||
System.debug('Response Status: ' + response.getStatusCode());
|
||||
System.debug('Response Body Length: ' + response.getBody().length());
|
||||
System.debug('Response Body (first 500 chars): ' + response.getBody().substring(0, Math.min(500, response.getBody().length())));
|
||||
|
||||
String result = 'Response Status: ' + response.getStatusCode() + '\n';
|
||||
result += 'Response Size: ' + response.getBody().length() + ' bytes\n';
|
||||
result += 'Response Preview: ' + response.getBody().substring(0, Math.min(500, response.getBody().length())) + '\n';
|
||||
|
||||
if (response.getBody().length() > 6000000) {
|
||||
result += '\n❌ PROBLEM: Response exceeds 6MB limit!\n';
|
||||
result += 'This means Python server is still returning PDF content instead of download link.\n';
|
||||
} else {
|
||||
result += '\n✅ Response size is OK.\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
return 'ERROR: ' + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
force-app/main/default/classes/TestResponse.cls-meta.xml
Normal file
5
force-app/main/default/classes/TestResponse.cls-meta.xml
Normal 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>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
import { LightningElement, track, wire } from 'lwc';
|
||||
import getProperties from '@salesforce/apex/PropertyDataController.getProperties';
|
||||
import generatePDFFromHTML from '@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML';
|
||||
import generateCompressedPDF from '@salesforce/apex/PDFGenerationProxy.generateCompressedPDF';
|
||||
import getPropertyImages from '@salesforce/apex/PropertyDataController.getPropertyImages';
|
||||
|
||||
export default class PropertyTemplateSelector extends LightningElement {
|
||||
@ -968,24 +969,38 @@ export default class PropertyTemplateSelector extends LightningElement {
|
||||
this.error = '';
|
||||
|
||||
try {
|
||||
const templateHTML = this.createTemplateHTML();
|
||||
// Get the current HTML content from the editor
|
||||
const editorFrame = this.template.querySelector('.enhanced-editor-content');
|
||||
let htmlContent = '';
|
||||
|
||||
if (editorFrame && editorFrame.innerHTML) {
|
||||
htmlContent = editorFrame.innerHTML;
|
||||
console.log('Using HTML content from editor, length:', htmlContent.length);
|
||||
} else {
|
||||
// Fallback: generate template HTML if editor is empty
|
||||
htmlContent = this.createCompleteTemplateHTML();
|
||||
console.log('Generated fallback HTML content, length:', htmlContent.length);
|
||||
}
|
||||
|
||||
// Generate PDF using the template HTML
|
||||
const result = await generatePropertyPDF({
|
||||
propertyData: JSON.stringify(this.propertyData),
|
||||
templateName: this.selectedTemplateId,
|
||||
generatePDF: true
|
||||
generatePDF: true,
|
||||
htmlContent: htmlContent
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Handle successful PDF generation
|
||||
console.log('PDF generated successfully:', result.pdfUrl);
|
||||
this.showSuccess('PDF generated successfully!');
|
||||
// You can add logic here to display the PDF or provide download link
|
||||
} else {
|
||||
this.error = result.error || 'Failed to generate PDF.';
|
||||
this.error = result.message || result.error || 'Failed to generate PDF.';
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = 'Error generating PDF: ' + error.body.message;
|
||||
console.error('Error in generateTemplateContent:', error);
|
||||
this.error = 'Error generating PDF: ' + (error.body?.message || error.message || 'Unknown error');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@ -1173,7 +1188,7 @@ export default class PropertyTemplateSelector extends LightningElement {
|
||||
|
||||
// Call the Apex method with the complete HTML and page size
|
||||
// Set timeout to 2 minutes (120000ms) for API response
|
||||
const base64PDF = await Promise.race([
|
||||
const pdfResult = await Promise.race([
|
||||
generatePDFFromHTML({
|
||||
htmlContent: htmlContent,
|
||||
pageSize: this.selectedPageSize
|
||||
@ -1202,63 +1217,24 @@ export default class PropertyTemplateSelector extends LightningElement {
|
||||
// Clear progress timer on success
|
||||
clearInterval(progressInterval);
|
||||
|
||||
if (base64PDF) {
|
||||
console.log('PDF generated successfully via Apex proxy');
|
||||
console.log('PDF base64 length:', base64PDF.length, 'characters');
|
||||
// Handle the new response format
|
||||
if (pdfResult && pdfResult.success) {
|
||||
console.log('PDF generation successful:', pdfResult);
|
||||
|
||||
// Update progress message
|
||||
this.showProgress('Processing PDF response...');
|
||||
this.showProgress('PDF ready for download...');
|
||||
|
||||
// Convert base64 to blob and open/download
|
||||
const pdfBlob = this.base64ToBlob(base64PDF, 'application/pdf');
|
||||
const pdfUrl = window.URL.createObjectURL(pdfBlob);
|
||||
|
||||
console.log('PDF blob created:', pdfBlob.size, 'bytes');
|
||||
console.log('PDF blob type:', pdfBlob.type);
|
||||
console.log('PDF URL created:', pdfUrl);
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
this.hideProgress();
|
||||
|
||||
// Download PDF directly in same tab (no popup needed)
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = pdfUrl;
|
||||
downloadLink.download = `${this.selectedProperty?.Name || this.propertyData?.propertyName || 'Property'}_Brochure_${this.selectedPageSize}.pdf`;
|
||||
downloadLink.style.display = 'none';
|
||||
|
||||
// Append to body, click, and remove
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
|
||||
// Clean up the blob URL to free memory
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(pdfUrl);
|
||||
}, 1000);
|
||||
|
||||
// Show success message with download instructions
|
||||
this.showSuccess('PDF generated successfully! Download started...');
|
||||
|
||||
// Fallback: If download doesn't work, show instructions
|
||||
setTimeout(() => {
|
||||
if (this.template.querySelector('.success-message')) {
|
||||
const successMsg = this.template.querySelector('.success-message');
|
||||
successMsg.innerHTML += '<br><small>💡 If download didn\'t start, right-click the PDF link below and select "Save as..."</small>';
|
||||
|
||||
// Add a visible download link as fallback
|
||||
const fallbackLink = document.createElement('a');
|
||||
fallbackLink.href = pdfUrl;
|
||||
fallbackLink.textContent = '📄 Click here to download PDF';
|
||||
fallbackLink.className = 'slds-button slds-button_brand';
|
||||
fallbackLink.style.marginTop = '10px';
|
||||
fallbackLink.style.display = 'inline-block';
|
||||
|
||||
successMsg.appendChild(fallbackLink);
|
||||
}
|
||||
}, 2000);
|
||||
// Handle different status types
|
||||
if (pdfResult.status === 'download_ready' || pdfResult.status === 'compressed_download_ready') {
|
||||
await this.handlePDFDownloadReady(pdfResult);
|
||||
} else {
|
||||
throw new Error('PDF generation returned empty result');
|
||||
throw new Error('Unexpected PDF status: ' + pdfResult.status);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Handle error response
|
||||
const errorMessage = pdfResult?.error || pdfResult?.message || 'PDF generation failed with unknown error';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('=== PDF GENERATION ERROR ===');
|
||||
@ -1473,6 +1449,198 @@ export default class PropertyTemplateSelector extends LightningElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle large PDF responses
|
||||
async handleLargePDFResponse(decodedResponse) {
|
||||
try {
|
||||
const responseData = JSON.parse(decodedResponse);
|
||||
console.log('Handling large PDF response:', responseData);
|
||||
|
||||
this.hideProgress();
|
||||
this.isLoading = false;
|
||||
|
||||
if (responseData.status === 'large_pdf') {
|
||||
// Show options for large PDFs
|
||||
this.showLargePDFOptions(responseData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling large PDF response:', error);
|
||||
this.showError('Error handling large PDF response: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show options for large PDFs
|
||||
showLargePDFOptions(responseData) {
|
||||
const message = `
|
||||
<div class="large-pdf-options">
|
||||
<h3>📄 PDF Generated Successfully!</h3>
|
||||
<p><strong>Size:</strong> ${responseData.size_mb} MB</p>
|
||||
<p><em>This PDF is too large for direct download due to Salesforce limits.</em></p>
|
||||
|
||||
<div class="options-container" style="margin: 20px 0;">
|
||||
<button class="slds-button slds-button_brand" onclick="window.open('${responseData.download_url}', '_blank')">
|
||||
🌐 Download from Server
|
||||
</button>
|
||||
|
||||
<button class="slds-button slds-button_neutral" onclick="this.generateCompressedPDF()">
|
||||
📦 Generate Compressed Version
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box" style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 15px;">
|
||||
<h4>💡 Why is this happening?</h4>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>Your template contains high-quality images</li>
|
||||
<li>Salesforce has a 6MB response limit</li>
|
||||
<li>The compressed version will reduce image quality but keep all content</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.showSuccess(message);
|
||||
}
|
||||
|
||||
// Generate compressed PDF to stay under Salesforce limits
|
||||
async generateCompressedPDF() {
|
||||
try {
|
||||
this.showProgress('Generating compressed PDF...');
|
||||
|
||||
// Get current editor content
|
||||
const editorFrame = this.template.querySelector('.enhanced-editor-content');
|
||||
let htmlContent = '';
|
||||
|
||||
if (editorFrame && editorFrame.innerHTML) {
|
||||
htmlContent = editorFrame.innerHTML;
|
||||
} else {
|
||||
htmlContent = this.createCompleteTemplateHTML();
|
||||
}
|
||||
|
||||
// Call the compressed PDF generation method
|
||||
const compressedPDF = await generateCompressedPDF({
|
||||
htmlContent: htmlContent,
|
||||
pageSize: this.selectedPageSize
|
||||
});
|
||||
|
||||
if (compressedPDF) {
|
||||
// Process the compressed PDF
|
||||
const pdfBlob = this.base64ToBlob(compressedPDF, 'application/pdf');
|
||||
const pdfUrl = window.URL.createObjectURL(pdfBlob);
|
||||
|
||||
// Download the compressed PDF
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = pdfUrl;
|
||||
downloadLink.download = `${this.selectedProperty?.Name || 'Property'}_Brochure_Compressed_${this.selectedPageSize}.pdf`;
|
||||
downloadLink.style.display = 'none';
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
|
||||
// Clean up
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(pdfUrl);
|
||||
}, 1000);
|
||||
|
||||
this.hideProgress();
|
||||
this.showSuccess('Compressed PDF generated and downloaded successfully!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating compressed PDF:', error);
|
||||
this.showError('Failed to generate compressed PDF: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PDF download ready response
|
||||
async handlePDFDownloadReady(pdfResult) {
|
||||
try {
|
||||
console.log("Handling PDF download ready:", pdfResult);
|
||||
|
||||
// Hide loading state
|
||||
this.isLoading = false;
|
||||
this.hideProgress();
|
||||
|
||||
// Create download message based on compression level
|
||||
let message = "";
|
||||
|
||||
if (pdfResult.status === "compressed_download_ready") {
|
||||
message = `
|
||||
<div class="pdf-download-ready">
|
||||
<h3>📦 Compressed PDF Ready!</h3>
|
||||
<p><strong>PDF ID:</strong> ${pdfResult.pdf_id}</p>
|
||||
<p><strong>Page Size:</strong> ${pdfResult.page_size}</p>
|
||||
<p><strong>Generated:</strong> ${pdfResult.generated_at}</p>
|
||||
<p><em>This is a compressed version optimized for smaller file size.</em></p>
|
||||
|
||||
<div class="download-options" style="margin: 20px 0;">
|
||||
<button class="slds-button slds-button_brand" onclick="window.open('${pdfResult.download_url}', '_blank')">
|
||||
📦 Download Compressed PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box" style="background: #e8f4fd; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #0176d3;">
|
||||
<h4>💡 About Compressed PDFs</h4>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>Images are optimized for smaller file size</li>
|
||||
<li>All content is preserved but with reduced quality</li>
|
||||
<li>Perfect for email sharing and quick downloads</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
message = `
|
||||
<div class="pdf-download-ready">
|
||||
<h3>📄 PDF Ready for Download!</h3>
|
||||
<p><strong>PDF ID:</strong> ${pdfResult.pdf_id}</p>
|
||||
<p><strong>Page Size:</strong> ${pdfResult.page_size}</p>
|
||||
<p><strong>Generated:</strong> ${pdfResult.generated_at}</p>
|
||||
<p><em>Your PDF has been generated successfully and is ready for download.</em></p>
|
||||
|
||||
<div class="download-options" style="margin: 20px 0;">
|
||||
<button class="slds-button slds-button_brand" onclick="window.open('${pdfResult.download_url}', '_blank')">
|
||||
📄 Download PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box" style="background: #f0f8f0; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #28a745;">
|
||||
<h4>✅ Download Instructions</h4>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>Click the download button above</li>
|
||||
<li>Your PDF will open in a new tab</li>
|
||||
<li>Use your browser's save function to download</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
this.showSuccess(message);
|
||||
|
||||
// Also provide a direct download link as fallback
|
||||
setTimeout(() => {
|
||||
if (this.template.querySelector('.success-message')) {
|
||||
const successMsg = this.template.querySelector('.success-message');
|
||||
|
||||
// Add a fallback download link
|
||||
const fallbackLink = document.createElement('a');
|
||||
fallbackLink.href = pdfResult.download_url;
|
||||
fallbackLink.textContent = '🔗 Direct Download Link';
|
||||
fallbackLink.className = 'slds-button slds-button_neutral';
|
||||
fallbackLink.style.marginTop = '10px';
|
||||
fallbackLink.style.display = 'inline-block';
|
||||
fallbackLink.target = '_blank';
|
||||
|
||||
successMsg.appendChild(fallbackLink);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling PDF download ready:', error);
|
||||
this.showError('Error handling PDF download: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create template HTML based on selection
|
||||
createTemplateHTML() {
|
||||
console.log('=== CREATE TEMPLATE HTML DEBUG ===');
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,861 +0,0 @@
|
||||
#!/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)
|
||||
@ -1,7 +0,0 @@
|
||||
|
||||
|
||||
Flask==2.3.3
|
||||
playwright==1.40.0
|
||||
requests==2.31.0
|
||||
Werkzeug==2.3.7
|
||||
gunicorn==21.2.0
|
||||
@ -1,477 +0,0 @@
|
||||
<!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>
|
||||
@ -1,401 +0,0 @@
|
||||
<!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>
|
||||
@ -1,426 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Editorial Real Estate Brochure - Updated</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Cormorant+Garamond:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
/* --- DESIGN SYSTEM & VARIABLES --- */
|
||||
:root {
|
||||
/* Color Palette */
|
||||
--color-bg: #FFFFFF;
|
||||
--color-off-white: #F8F7F5;
|
||||
--color-text-primary: #333333;
|
||||
--color-text-secondary: #777777;
|
||||
--color-accent-beige: #D4C7B8;
|
||||
--color-border: #EAEAEA;
|
||||
|
||||
/* Typography */
|
||||
--font-serif: 'Cormorant Garamond', serif;
|
||||
--font-sans: 'Lato', sans-serif;
|
||||
}
|
||||
|
||||
/* --- GLOBAL & BODY STYLES --- */
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background-color: #d8d8d8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50px;
|
||||
margin: 0;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.brochure-page {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
background-color: var(--color-bg);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* --- PAGE 1: FRONT COVER --- */
|
||||
.p1-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.p1-image-side {
|
||||
flex: 1.2;
|
||||
background-image: url('https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.p1-content-side {
|
||||
flex: 1;
|
||||
padding: 70px 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.p1-header .collection {
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 3px;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.p1-main-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 5rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
color: var(--color-text-primary);
|
||||
margin: 20px 0;
|
||||
}
|
||||
.p1-address {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
border-left: 3px solid var(--color-accent-beige);
|
||||
padding-left: 20px;
|
||||
}
|
||||
.p1-ref-id {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 15px;
|
||||
padding-left: 23px;
|
||||
}
|
||||
.p1-footer {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.p1-footer strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.p1-footer .area {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* --- SHARED STYLES for Content Pages --- */
|
||||
.page-layout {
|
||||
padding: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-number {
|
||||
position: absolute;
|
||||
top: 70px; right: 70px;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.page-title-main {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 3.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 15px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.page-title-sub {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.content-divider {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 30px 0;
|
||||
}
|
||||
.section-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* --- PAGE 2: INTRODUCTION & NARRATIVE --- */
|
||||
.p2-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.p2-image {
|
||||
background-image: url('https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.p2-text p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.p2-text p:first-of-type::first-letter {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 4rem;
|
||||
float: left;
|
||||
line-height: 1;
|
||||
margin-right: 15px;
|
||||
color: var(--color-accent-beige);
|
||||
}
|
||||
.pull-quote {
|
||||
border-left: 3px solid var(--color-accent-beige);
|
||||
padding-left: 25px;
|
||||
margin: 30px 0;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.5rem;
|
||||
font-style: italic;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* --- PAGE 3: DETAILS & AMENITIES --- */
|
||||
.p3-main-content { flex-grow: 1; }
|
||||
.spec-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
background-color: var(--color-off-white);
|
||||
}
|
||||
.spec-item { text-align: center; }
|
||||
.spec-item .value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.spec-item .label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px 40px;
|
||||
}
|
||||
.details-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.details-item .label { color: var(--color-text-secondary); }
|
||||
.details-item .value { color: var(--color-text-primary); font-weight: 700; }
|
||||
.amenities-list { list-style: none; padding: 0; column-count: 2; column-gap: 40px;}
|
||||
.amenities-list li {
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.amenities-list li i { color: var(--color-accent-beige); margin-right: 12px; }
|
||||
|
||||
/* --- PAGE 4: REVISED LAYOUT --- */
|
||||
.p4-section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.p4-floorplan-container {
|
||||
height: 320px;
|
||||
background-color: var(--color-off-white);
|
||||
border: 1px solid var(--color-border);
|
||||
background-image: url('https://cdn.shopify.com/s/files/1/0024/0495/3953/files/Architect_s_floor_plan_for_a_house_in_black_and_white_large.jpg');
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.p4-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.info-list .info-item, .location-list .item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.info-list .info-item strong, .location-list .item strong {
|
||||
color: var(--color-text-primary);
|
||||
margin-right: 15px;
|
||||
}
|
||||
.p4-contact-row {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
.contact-card {
|
||||
background-color: var(--color-off-white);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
.contact-card .title {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.contact-card .name { font-family: var(--font-serif); font-size: 1.5rem; font-weight: 600; }
|
||||
.contact-card .phone, .contact-card .email { font-size: 0.9rem; margin: 4px 0; color: var(--color-text-secondary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="p1-container">
|
||||
<div class="p1-image-side"></div>
|
||||
<div class="p1-content-side">
|
||||
<header class="p1-header">
|
||||
<div class="collection">Elysian Estates Collection</div>
|
||||
<h1 class="p1-main-title">The Serenity House</h1>
|
||||
<p class="p1-address">123 Luxury Lane, Prestige City, PC 45678</p>
|
||||
<p class="p1-ref-id">Reference ID: ES-8821</p>
|
||||
</header>
|
||||
<footer class="p1-footer">
|
||||
<div class="area">6,200 Sq. Ft. • 5 Bedrooms • 6 Bathrooms</div>
|
||||
An architectural marvel of curated living space.
|
||||
<br>
|
||||
<strong>Offered at $4,500,000</strong>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-layout">
|
||||
<span class="page-number">02</span>
|
||||
<h1 class="page-title-main">A Sanctuary of Modern Design</h1>
|
||||
<p class="page-title-sub">Where light, space, and nature converge to create an unparalleled living experience.</p>
|
||||
<div class="p2-grid">
|
||||
<div class="p2-text">
|
||||
<p>Designed by the world-renowned architect, Helena Vance, The Serenity House is more than a home; it is a living sculpture. Every line, material, and detail has been thoughtfully considered to evoke a sense of peace and connection with the surrounding landscape. Soaring ceilings and floor-to-ceiling glass walls dissolve the boundaries between inside and out, flooding the space with natural light.</p>
|
||||
<p class="pull-quote">A timeless residence built not just for living, but for thriving.</p>
|
||||
<p>The interior palette is a harmonious blend of natural oak, Italian travertine, and warm bronze accents, creating an atmosphere of understated luxury. This property represents a unique opportunity to own a piece of architectural history.</p>
|
||||
</div>
|
||||
<div class="p2-image"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-layout">
|
||||
<span class="page-number">03</span>
|
||||
<h1 class="page-title-main">Property Specifications</h1>
|
||||
<p class="page-title-sub">A comprehensive overview of the property’s features, details, and amenities.</p>
|
||||
|
||||
<div class="p3-main-content">
|
||||
<div class="spec-grid">
|
||||
<div class="spec-item"><div class="value">5</div><div class="label">Bedrooms</div></div>
|
||||
<div class="spec-item"><div class="value">6</div><div class="label">Bathrooms</div></div>
|
||||
<div class="spec-item"><div class="value">6,200</div><div class="label">Square Feet</div></div>
|
||||
<div class="spec-item"><div class="value">0.75</div><div class="label">Acres</div></div>
|
||||
</div>
|
||||
|
||||
<hr class="content-divider">
|
||||
|
||||
<h3 class="section-title">Property Details</h3>
|
||||
<div class="details-grid">
|
||||
<div class="details-item"><span class="label">Status</span><span class="value">For Sale</span></div>
|
||||
<div class="details-item"><span class="label">Year Built</span><span class="value">2023</span></div>
|
||||
<div class="details-item"><span class="label">Type</span><span class="value">Single-Family Home</span></div>
|
||||
<div class="details-item"><span class="label">Furnishing</span><span class="value">Partially Furnished</span></div>
|
||||
<div class="details-item"><span class="label">Floor</span><span class="value">2 Levels</span></div>
|
||||
<div class="details-item"><span class="label">Maintenance Fee</span><span class="value">$1,200 / month</span></div>
|
||||
<div class="details-item"><span class="label">Parking</span><span class="value">3-Car Garage</span></div>
|
||||
<div class="details-item"><span class="label">Service Charge</span><span class="value">Included</span></div>
|
||||
</div>
|
||||
|
||||
<hr class="content-divider">
|
||||
|
||||
<h3 class="section-title">Amenities & Features</h3>
|
||||
<ul class="amenities-list">
|
||||
<li><i class="fa-solid fa-check"></i> Primary Suite with Spa-Bath</li>
|
||||
<li><i class="fa-solid fa-check"></i> Radiant Heated Flooring</li>
|
||||
<li><i class="fa-solid fa-check"></i> Custom Walk-in Closets</li>
|
||||
<li><i class="fa-solid fa-check"></i> Smart Home Automation</li>
|
||||
<li><i class="fa-solid fa-check"></i> Infinity Edge Saline Pool</li>
|
||||
<li><i class="fa-solid fa-check"></i> Private Cinema Room</li>
|
||||
<li><i class="fa-solid fa-check"></i> Temperature-Controlled Wine Cellar</li>
|
||||
<li><i class="fa-solid fa-check"></i> Landscaped Gardens & Terrace</li>
|
||||
<li><i class="fa-solid fa-check"></i> Gourmet Chef's Kitchen</li>
|
||||
<li><i class="fa-solid fa-check"></i> Floor-to-Ceiling Glass Walls</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-layout">
|
||||
<span class="page-number">04</span>
|
||||
<h1 class="page-title-main" style="margin-bottom: 30px;">Floor Plan & Details</h1>
|
||||
|
||||
<div class="p4-info-grid">
|
||||
<div class="location-list">
|
||||
<h2 class="p4-section-title">Location & Nearby</h2>
|
||||
<div class="item"><strong>Schools</strong> <span>5 min drive</span></div>
|
||||
<div class="item"><strong>Shopping</strong> <span>10 min drive</span></div>
|
||||
<div class="item"><strong>Hospitals</strong> <span>12 min drive</span></div>
|
||||
<div class="item"><strong>Country Club</strong> <span>8 min drive</span></div>
|
||||
<div class="item"><strong>Airport</strong> <span>20 min drive</span></div>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<h2 class="p4-section-title">Additional Information</h2>
|
||||
<div class="info-item"><strong>Pet-Friendly</strong> <span>By Approval</span></div>
|
||||
<div class="info-item"><strong>Smoking</strong> <span>Not Permitted</span></div>
|
||||
<div class="info-item"><strong>Availability</strong> <span>Immediate</span></div>
|
||||
<div class="info-item"><strong>Utilities</strong> <span>Not Included</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="content-divider">
|
||||
|
||||
<h2 class="p4-section-title">Floor Plan & Location</h2>
|
||||
<div class="p4-floorplan-container"></div>
|
||||
|
||||
<div class="p4-contact-row">
|
||||
<div class="contact-card">
|
||||
<div class="title">Owner Information</div>
|
||||
<div class="name">John & Jane Doe</div>
|
||||
<p class="phone">(555) 111-2222</p>
|
||||
<p class="email">owner.serenity@email.com</p>
|
||||
</div>
|
||||
<div class="contact-card">
|
||||
<div class="title">Agent Information</div>
|
||||
<div class="name">Olivia Sterling</div>
|
||||
<p class="phone">(555) 987-6543</p>
|
||||
<p class="email">olivia@elysianestates.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,604 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Modern Urban Residences Brochure - Updated</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
/* --- DESIGN SYSTEM & VARIABLES --- */
|
||||
:root {
|
||||
/* Color Palette */
|
||||
--color-dark: #111111;
|
||||
--color-light: #FFFFFF;
|
||||
--color-accent-green: #0A6847;
|
||||
--color-grey-bg: #F0F0F0;
|
||||
--color-text-dark: #222222;
|
||||
--color-text-light: #EFEFEF;
|
||||
--color-text-muted: #888888;
|
||||
--color-border: #DDDDDD;
|
||||
|
||||
/* Typography */
|
||||
--font-main: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* --- GLOBAL & BODY STYLES --- */
|
||||
body {
|
||||
font-family: var(--font-main);
|
||||
background-color: #d8d8d8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50px;
|
||||
margin: 0;
|
||||
gap: 50px;
|
||||
}
|
||||
|
||||
.brochure-page {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
background-color: var(--color-light);
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* --- PAGE 1: COVER PAGE --- */
|
||||
.cover-page {
|
||||
background-image: url('https://plus.unsplash.com/premium_photo-1677474827617-6a7269f97574?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: var(--color-light);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.cover-overlay {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
.cover-content {
|
||||
position: relative; z-index: 2; padding: 50px;
|
||||
}
|
||||
.cover-content .subtitle {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.cover-content .main-title {
|
||||
font-size: 6rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
margin: 10px 0 20px 0;
|
||||
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
.cover-content .address {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 400;
|
||||
border-top: 1px solid var(--color-accent-green);
|
||||
display: inline-block;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.cover-footer {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 40px; right: 40px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- SHARED STYLES --- */
|
||||
.page-container {
|
||||
padding: 70px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.page-title span {
|
||||
color: var(--color-accent-green);
|
||||
}
|
||||
.page-subtitle {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.page-footer-bar {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.page-footer-bar .property-name {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-dark);
|
||||
margin-bottom: 25px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.detail-item .label { color: var(--color-text-muted); }
|
||||
.detail-item .value { color: var(--color-text-dark); font-weight: 600; text-align: right; }
|
||||
|
||||
/* --- PAGE 2: THE VISION --- */
|
||||
.vision-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.vision-image {
|
||||
background-image: url('https://images.unsplash.com/photo-1626704359446-0de90350b4e7?q=80&w=736&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.vision-text h3 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-dark);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.vision-text p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* --- PAGE 3: RESIDENCES GALLERY --- */
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: 250px 250px 250px;
|
||||
gap: 20px;
|
||||
flex-grow: 1;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.gallery-item {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
color: var(--color-light);
|
||||
padding: 15px;
|
||||
}
|
||||
.gallery-item::after {
|
||||
content: ''; position: absolute; top:0; left: 0; width: 100%; height: 100%;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
||||
}
|
||||
.gallery-item span { font-weight: 600; z-index: 2; }
|
||||
.g-item-1 { grid-column: 1 / 3; grid-row: 1 / 2; background-image: url('https://images.unsplash.com/photo-1616046229478-9901c5536a45?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||
.g-item-2 { grid-column: 3 / 4; grid-row: 1 / 3; background-image: url('https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||
.g-item-3 { grid-column: 1 / 2; grid-row: 2 / 4; background-image: url('https://images.unsplash.com/photo-1600121848594-d8644e57abab?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||
.g-item-4 { grid-column: 2 / 3; grid-row: 2 / 3; background-image: url('https://images.unsplash.com/photo-1595526114035-0d45ed16433d?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||
.g-item-5 { grid-column: 2 / 4; grid-row: 3 / 4; background-image: url('https://images.unsplash.com/photo-1512918728675-ed5a71a580a9?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); }
|
||||
|
||||
/* --- PAGE 4: AMENITIES & FEATURES (REVISED V2) --- */
|
||||
.amenities-intro {
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-muted);
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
.page4-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 50px;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.page4-image {
|
||||
background-image: url('https://plus.unsplash.com/premium_photo-1675745330187-a6f001a21abe?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
min-height: 100%;
|
||||
border: 6px solid var(--color-accent-green);
|
||||
}
|
||||
.page4-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.amenities-list-p4 {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 30px 0;
|
||||
column-count: 2;
|
||||
column-gap: 30px;
|
||||
}
|
||||
.amenities-list-p4 li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 18px;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.amenities-list-p4 i {
|
||||
font-size: 1rem;
|
||||
color: var(--color-accent-green);
|
||||
margin-right: 15px;
|
||||
width: 20px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* --- PAGE 5: FLOOR PLANS & SPECS (REVISED V2) --- */
|
||||
.page5-main {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
.floorplan-showcase {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 25px;
|
||||
background-color: var(--color-grey-bg);
|
||||
padding: 20px;
|
||||
border-left: 5px solid var(--color-accent-green);
|
||||
}
|
||||
.floorplan-image-p5 {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
.floorplan-image-p5.residence {
|
||||
background-image: url('https://images.unsplash.com/photo-1740446568651-1d31966b228a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||
}
|
||||
.floorplan-image-p5.penthouse {
|
||||
background-image: url('https://images.unsplash.com/photo-1721522281545-fad32dd5107a?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D');
|
||||
}
|
||||
.floorplan-info-p5 h4 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
.floorplan-stats-p5 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.floorplan-stats-p5 .stat .value {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.floorplan-stats-p5 .stat .label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.floorplan-info-p5 .description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
.additional-specs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.spec-item {
|
||||
background-color: var(--color-grey-bg);
|
||||
padding: 15px;
|
||||
}
|
||||
.spec-item .label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
.spec-item .value {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
/* --- PAGE 6: LOCATION & CONTACT (REVISED V3) --- */
|
||||
.page6-main {
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
align-content: start;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.page6-main p {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.contact-person-p6 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.contact-person-p6 .name {
|
||||
font-weight: 700;
|
||||
font-size: 1.3rem;
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.contact-person-p6 .title {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.contact-person-p6 .details {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.highlights-list-p6 {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.highlights-list-p6 li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.highlights-list-p6 i {
|
||||
color: var(--color-accent-green);
|
||||
margin-right: 15px;
|
||||
width: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="brochure-page cover-page">
|
||||
<div class="cover-overlay"></div>
|
||||
<div class="cover-content">
|
||||
<div class="subtitle">An Urban Oasis</div>
|
||||
<h1 class="main-title">THE VERTICE</h1>
|
||||
<div class="address">18 Skyline Avenue, Metropolis Centre, MC 90210</div>
|
||||
</div>
|
||||
<div class="cover-footer">
|
||||
<span>Residences Starting from $1,200,000</span>
|
||||
<span>Ref ID: VP-2025-001</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-container">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">Elevated <span>Living</span></h1>
|
||||
<span class="page-subtitle">Discover Your Sanctuary in the Sky</span>
|
||||
</header>
|
||||
<main class="vision-grid">
|
||||
<div class="vision-text">
|
||||
<h3>Where Design Meets Desire.</h3>
|
||||
<p>The Vertice is not just a building; it's a bold statement on modern urban living. Conceived for the discerning individual, it offers a unique blend of architectural prowess, bespoke interiors, and an unparalleled lifestyle experience right in the heart of the city.</p>
|
||||
<p>Every residence is a testament to quality, featuring panoramic city views from floor-to-ceiling windows, intelligent home systems, and finishes selected from the finest materials around the globe. This is more than a home; it's a new perspective.</p>
|
||||
</div>
|
||||
<div class="vision-image"></div>
|
||||
</main>
|
||||
<footer class="page-footer-bar">
|
||||
<span class="property-name">THE VERTICE</span>
|
||||
<span>Page 02 / 06</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-container">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">Exquisite <span>Interiors</span></h1>
|
||||
<span class="page-subtitle">A Canvas for Your Life</span>
|
||||
</header>
|
||||
<main class="gallery-grid">
|
||||
<div class="gallery-item g-item-1"><span>Open-Concept Living Space</span></div>
|
||||
<div class="gallery-item g-item-2"><span>Master Bedroom Suite</span></div>
|
||||
<div class="gallery-item g-item-3"><span>Gourmet Chef's Kitchen</span></div>
|
||||
<div class="gallery-item g-item-4"><span>Spa-Inspired Bathroom</span></div>
|
||||
<div class="gallery-item g-item-5"><span>Private Balcony Views</span></div>
|
||||
</main>
|
||||
<footer class="page-footer-bar">
|
||||
<span class="property-name">THE VERTICE</span>
|
||||
<span>Page 03 / 06</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-container">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">Amenities & <span>Features</span></h1>
|
||||
<span class="page-subtitle">Beyond the Expected</span>
|
||||
</header>
|
||||
<p class="amenities-intro">An unrivaled collection of amenities offers residents a resort-style living experience. From the serene rooftop pool to the state-of-the-art wellness center, every detail is crafted for comfort, convenience, and luxury.</p>
|
||||
<main class="page4-grid">
|
||||
<div class="page4-image"></div>
|
||||
<div class="page4-content">
|
||||
<div>
|
||||
<h3 class="section-title">Lifestyle Amenities</h3>
|
||||
<ul class="amenities-list-p4">
|
||||
<li><i class="fa-solid fa-water-ladder"></i> Rooftop Infinity Pool</li>
|
||||
<li><i class="fa-solid fa-dumbbell"></i> Fitness Center</li>
|
||||
<li><i class="fa-solid fa-martini-glass-empty"></i> Residents' Sky Lounge</li>
|
||||
<li><i class="fa-solid fa-film"></i> Private Cinema Room</li>
|
||||
<li><i class="fa-solid fa-spa"></i> Wellness Spa & Sauna</li>
|
||||
<li><i class="fa-solid fa-briefcase"></i> Business Center</li>
|
||||
<li><i class="fa-solid fa-shield-halved"></i> 24/7 Concierge</li>
|
||||
<li><i class="fa-solid fa-car"></i> Secure Parking</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="section-title">Key Specifications</h3>
|
||||
<div class="detail-item"><span class="label">Status</span> <span class="value">New Development</span></div>
|
||||
<div class="detail-item"><span class="label">Property Type</span> <span class="value">Condominium</span></div>
|
||||
<div class="detail-item"><span class="label">Year Built</span> <span class="value">2025</span></div>
|
||||
<div class="detail-item"><span class="label">Technology</span> <span class="value">Integrated Smart Home</span></div>
|
||||
<div class="detail-item"><span class="label">Design</span> <span class="value">Sustainable & Eco-Friendly</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer-bar">
|
||||
<span class="property-name">THE VERTICE</span>
|
||||
<span>Page 04 / 06</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-container">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">Floor Plans & <span>Details</span></h1>
|
||||
<span class="page-subtitle">Designed for Modern Life</span>
|
||||
</header>
|
||||
<main class="page5-main">
|
||||
<div class="floorplan-showcase">
|
||||
<div class="floorplan-image-p5 residence"></div>
|
||||
<div class="floorplan-info-p5">
|
||||
<h4>Two-Bedroom Residence</h4>
|
||||
<div class="floorplan-stats-p5">
|
||||
<div class="stat">
|
||||
<div class="value">1,450</div>
|
||||
<div class="label">SQ. FT.</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">2</div>
|
||||
<div class="label">BEDROOMS</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">2</div>
|
||||
<div class="label">BATHROOMS</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">1</div>
|
||||
<div class="label">BALCONY</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description">A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floorplan-showcase">
|
||||
<div class="floorplan-image-p5 penthouse"></div>
|
||||
<div class="floorplan-info-p5">
|
||||
<h4>Three-Bedroom Penthouse</h4>
|
||||
<div class="floorplan-stats-p5">
|
||||
<div class="stat">
|
||||
<div class="value">3,200</div>
|
||||
<div class="label">SQ. FT.</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">3</div>
|
||||
<div class="label">BEDROOMS</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">3.5</div>
|
||||
<div class="label">BATHROOMS</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="value">1</div>
|
||||
<div class="label">TERRACE</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="description">The pinnacle of luxury living, this penthouse offers expansive spaces, premium finishes, and exclusive access to a private rooftop terrace.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="section-title">Additional Information</h3>
|
||||
<div class="additional-specs-grid">
|
||||
<div class="spec-item"><div class="label">Pets</div><div class="value">Allowed (w/ restrictions)</div></div>
|
||||
<div class="spec-item"><div class="label">Smoking</div><div class="value">In designated areas</div></div>
|
||||
<div class="spec-item"><div class="label">Availability</div><div class="value">Q4 2025</div></div>
|
||||
<div class="spec-item"><div class="label">Parking</div><div class="value">2 Spaces per Unit</div></div>
|
||||
<div class="spec-item"><div class="label">Security Deposit</div><div class="value">2 Months</div></div>
|
||||
<div class="spec-item"><div class="label">Utilities</div><div class="value">Sub-metered</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer-bar">
|
||||
<span class="property-name">THE VERTICE</span>
|
||||
<span>Page 05 / 06</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="brochure-page">
|
||||
<div class="page-container">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">Location & <span>Inquiry</span></h1>
|
||||
<span class="page-subtitle">Your Future Awaits</span>
|
||||
</header>
|
||||
<main class="page6-main">
|
||||
<div>
|
||||
<h3 class="section-title">Schedule a Private Viewing</h3>
|
||||
<p>Experience The Vertice firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.</p>
|
||||
<div class="contact-person-p6">
|
||||
<div class="name">Alexander Valentine</div>
|
||||
<div class="title">Sales Executive, Elysian Properties</div>
|
||||
<div class="details">
|
||||
(555) 123-9876<br>
|
||||
alex.v@elysian.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="section-title">Neighborhood Highlights</h3>
|
||||
<ul class="highlights-list-p6">
|
||||
<li><i class="fa-solid fa-tree-city"></i> <strong>Landmarks:</strong> Central Park (5 min)</li>
|
||||
<li><i class="fa-solid fa-train-subway"></i> <strong>Transportation:</strong> Metro Line A (2 min walk)</li>
|
||||
<li><i class="fa-solid fa-school"></i> <strong>Schools:</strong> Metropolis Intl. (10 min)</li>
|
||||
<li><i class="fa-solid fa-cart-shopping"></i> <strong>Shopping:</strong> The Galleria Mall (8 min)</li>
|
||||
<li><i class="fa-solid fa-plane-departure"></i> <strong>Airport:</strong> 25 min drive</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer-bar">
|
||||
<span class="property-name">THE VERTICE</span>
|
||||
<span>Page 06 / 06</span>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user