diff --git a/force-app/main/default/classes/PDFGenerationProxy.cls b/force-app/main/default/classes/PDFGenerationProxy.cls index 513cbaa..bcf9cb2 100644 --- a/force-app/main/default/classes/PDFGenerationProxy.cls +++ b/force-app/main/default/classes/PDFGenerationProxy.cls @@ -1,57 +1,116 @@ public with sharing class PDFGenerationProxy { @AuraEnabled - public static String generatePDFFromHTML(String htmlContent, String pageSize) { + public static Map 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 result = new Map(); + 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 errorResult = new Map(); + 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 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 result = new Map(); + 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 errorResult = new Map(); + 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 = 'Test

Test PDF Generation

'; - return generatePDFFromHTML(testHtml, 'A4'); + String testHtml = '

Test PDF Generation

This is a test to verify the API connection.

'; + Map 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(); } } } diff --git a/force-app/main/default/classes/PropertyTemplateController.cls b/force-app/main/default/classes/PropertyTemplateController.cls index aacaaec..e0926cb 100644 --- a/force-app/main/default/classes/PropertyTemplateController.cls +++ b/force-app/main/default/classes/PropertyTemplateController.cls @@ -147,7 +147,7 @@ public with sharing class PropertyTemplateController { } @AuraEnabled - public static Map generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF) { + public static Map generatePropertyPDF(String propertyData, String templateName, Boolean generatePDF, String htmlContent) { try { // Parse property data Map propertyMap = (Map) 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 result = new Map(); 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'); diff --git a/force-app/main/default/classes/TestAPICall.cls b/force-app/main/default/classes/TestAPICall.cls new file mode 100644 index 0000000..7cfc47d --- /dev/null +++ b/force-app/main/default/classes/TestAPICall.cls @@ -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 = '

Test

Small test content

'; + + 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 requestBody = new Map(); + 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(); + } + } +} diff --git a/force-app/main/default/classes/TestAPICall.cls-meta.xml b/force-app/main/default/classes/TestAPICall.cls-meta.xml new file mode 100644 index 0000000..1e7de94 --- /dev/null +++ b/force-app/main/default/classes/TestAPICall.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/force-app/main/default/classes/TestNewFlow.cls b/force-app/main/default/classes/TestNewFlow.cls new file mode 100644 index 0000000..f30fb40 --- /dev/null +++ b/force-app/main/default/classes/TestNewFlow.cls @@ -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 = '

Test PDF

Testing new flow with PDF generation first, then download link.

'; + + 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 requestBody = new Map(); + 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 responseMap = (Map) 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(); + } + } +} diff --git a/force-app/main/default/classes/TestNewFlow.cls-meta.xml b/force-app/main/default/classes/TestNewFlow.cls-meta.xml new file mode 100644 index 0000000..1e7de94 --- /dev/null +++ b/force-app/main/default/classes/TestNewFlow.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/force-app/main/default/classes/TestResponse.cls b/force-app/main/default/classes/TestResponse.cls new file mode 100644 index 0000000..f65a2ca --- /dev/null +++ b/force-app/main/default/classes/TestResponse.cls @@ -0,0 +1,53 @@ +public with sharing class TestResponse { + + @AuraEnabled + public static String testPythonResponse() { + try { + String testHtml = '

Test PDF

Testing response size.

'; + + 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 requestBody = new Map(); + 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(); + } + } +} diff --git a/force-app/main/default/classes/TestResponse.cls-meta.xml b/force-app/main/default/classes/TestResponse.cls-meta.xml new file mode 100644 index 0000000..1e7de94 --- /dev/null +++ b/force-app/main/default/classes/TestResponse.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css.backup b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css.backup new file mode 100644 index 0000000..141973d --- /dev/null +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css.backup @@ -0,0 +1,8792 @@ +.property-brochure-generator { + /* International Standard Font Stack - 2024 Best Practices */ + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, 'Roboto', 'Helvetica Neue', Arial, sans-serif; + background: + radial-gradient(circle at 20% 20%, rgba(120, 119, 198, 0.1) 0%, transparent 50%), + radial-gradient(circle at 80% 80%, rgba(255, 119, 198, 0.1) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.1) 0%, transparent 50%), + linear-gradient(135deg, #f8fafc 0%, #f1f5f9 25%, #e2e8f0 50%, #cbd5e1 100%); + min-height: 100vh; + padding: 0; + margin: 0; + /* Industry Standard Base Font Size - 16px (1rem) */ + font-size: 1rem; + /* Optimal Line Height for Readability (WCAG AA) */ + line-height: 1.5; + /* Modern Letter Spacing for Digital Displays */ + letter-spacing: -0.011em; + /* Advanced Typography Features */ + font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1, 'ss01' 1, 'cv02' 1, 'cv03' 1, 'cv04' 1; + font-variation-settings: 'slnt' 0; + font-optical-sizing: auto; + /* High-DPI Display Optimization */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +/* Header Section */ +.header-section { + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%), + linear-gradient(135deg, #4f46e5 0%, #7c3aed 25%, #ec4899 75%, #f97316 100%); + padding: 4rem 2rem; + text-align: center; + margin-bottom: 0; + position: relative; + overflow: hidden; + border-radius: 24px; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 8px 32px rgba(79, 70, 229, 0.15), + 0 20px 60px rgba(124, 58, 237, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + + + + + + + +.header-content { + max-width: 1200px; + margin: 0 auto; + position: relative; + z-index: 3; +} + + + + + +/* Beautiful Purple Gradient Header for All Steps */ +.step-header-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 0 0 20px 20px; + padding: 40px 30px; + margin: 0 0 30px 0; + text-align: center; + box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; +} + +.step-header-gradient::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); + pointer-events: none; +} + +.main-title { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + /* International Standard: Display Large (3rem = 48px) */ + font-size: 3rem; + font-weight: 700; + margin: 0 0 1rem 0; + /* Optimal Letter Spacing for Large Headings */ + letter-spacing: -0.025em; + position: relative; + z-index: 3; + /* WCAG AA Compliant Line Height for Headings */ + line-height: 1.2; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + /* Variable Font Weight for Modern Browsers */ + font-variation-settings: 'wght' 700; +} + + + + + +.subtitle { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + /* International Standard: Title Medium (1.25rem = 20px) */ + font-size: 1.25rem; + font-weight: 400; + margin: 0; + position: relative; + z-index: 3; + color: rgba(255, 255, 255, 0.9); + /* Moderate Letter Spacing for Subheadings */ + letter-spacing: 0.015em; + text-transform: uppercase; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + /* WCAG AA Line Height */ + line-height: 1.4; + font-variation-settings: 'wght' 400; +} + +.header-features { + display: flex; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; +} + +.feature-badge { + background: rgba(255, 255, 255, 0.2); + padding: 0.5rem 1.5rem; + border-radius: 25px; + font-size: 0.9rem; + font-weight: 500; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Error Message */ +.error-message { + background: #fee; + border: 1px solid #fcc; + color: #c33; + padding: 1rem; + margin: 1rem 2rem; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.error-content { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.error-close { + background: none; + border: none; + color: #c33; + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Step Navigation */ +.step-navigation { + display: flex; + justify-content: center; + gap: 2rem; + padding: 2rem; + background: rgba(255, 255, 255, 0.95); + margin: 0 2rem 0 2rem; + border-radius: 20px; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 8px 32px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + margin-top: 0; + margin-bottom: 0; +} + +.step-nav-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 1rem; + border-radius: 8px; + transition: all 0.3s ease; + min-width: 120px; +} + +.step-nav-item:hover { + background: #f8f9fa; +} + +.step-nav-item.active { + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%); + color: white; + transform: translateY(-2px) scale(1.05); + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.15), + 0 8px 32px rgba(79, 70, 229, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.step-number { + width: 40px; + height: 40px; + border-radius: 50%; + background: #e9ecef; + color: #6c757d; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 1.1rem; + transition: all 0.3s ease; +} + +.step-nav-item.active .step-number { + background: rgba(255, 255, 255, 0.2); + color: white; +} + +.step-label { + font-size: 0.9rem; + font-weight: 500; + text-align: center; +} + +/* Step Content */ +.step-content { + display: none; + max-width: 1400px; + margin: 0 auto 0 auto; + padding: 0 3rem 2rem 3rem; +} + +.step-content.active { + display: block; + animation: fadeInUp 0.5s ease; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Elegant Flow Animation */ +@keyframes elegantFlow { + 0% { + background-position: 0% 50%; + } + 25% { + background-position: 50% 25%; + } + 50% { + background-position: 100% 50%; + } + 75% { + background-position: 50% 75%; + } + 100% { + background-position: 0% 50%; + } +} + +.step-header { + text-align: center; + margin-bottom: 3rem; +} + +.step-header h2 { + font-family: Georgia, 'Times New Roman', Times, serif; + font-size: 3.2rem; + font-weight: 700; + margin: 0 0 1.5rem 0; + color: #2c3e50; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.step-header p { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 1.3rem; + color: #6c757d; + margin: 0; + font-weight: 500; + letter-spacing: 0.01em; +} + +/* Template Grid - Enhanced styling with A4 proportions */ +.template-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); + gap: 35px; + padding: 40px 20px; + max-width: 1600px; + margin: 0 auto; +} + +/* Template Cards - Enhanced with A4 proportions and better visibility */ +.template-card { + background: white; + border-radius: 20px; + padding: 20px; /* Further reduced padding to increase content width more */ + cursor: pointer; + transition: all 0.4s ease; + position: relative; + border: 2px solid transparent; + box-shadow: 0 8px 25px rgba(0,0,0,0.12); + overflow: hidden; + min-height: 420px; /* A4-like height */ + display: flex; + flex-direction: column; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + width: 100%; /* Ensure full width usage */ +} + +.template-card:hover { + transform: translateY(-10px); + box-shadow: 0 15px 40px rgba(0,0,0,0.18); + border-color: #667eea; +} + +.template-card.selected { + border-color: #667eea; + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4); + transform: translateY(-6px); +} + +/* Template Heights with A4 proportions */ +.template-tall { + min-height: 800px; /* Increased from 700px */ + padding: 25px 20px; /* Further reduced padding to increase content width more */ +} + +.template-medium { + min-height: 650px; /* Increased from 550px */ + padding: 20px 15px; /* Further reduced padding to increase content width more */ +} + +.template-small { + min-height: 550px; /* Increased from 480px */ + padding: 15px 10px; /* Further reduced padding to increase content width more */ +} + +/* Enhanced Template Content Styles */ +.template-content { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + color: #333; + line-height: 1.6; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Newsletter Style Template - Enhanced */ +.newsletter-style { + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 16px; + padding: 20px; /* Further reduced padding to increase content width more */ + position: relative; + overflow: hidden; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.newsletter-style::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 120px; + height: 120px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + transform: translate(30px, -30px); + opacity: 0.1; + z-index: 0; +} + +.newsletter-style > * { + position: relative; + z-index: 1; +} + +.newsletter-header h2 { + color: #667eea; + font-size: 22px; + margin-bottom: 25px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.newsletter-main h1 { + color: #2c3e50; + font-size: 32px; + margin-bottom: 25px; + font-weight: 800; + line-height: 1.2; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.newsletter-main p { + color: #5a6c7d; + font-size: 17px; + margin-bottom: 30px; + font-weight: 500; + line-height: 1.6; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.newsletter-section h4 { + color: #667eea; + font-size: 20px; + margin-bottom: 18px; + font-weight: 600; + text-transform: uppercase; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.newsletter-section p { + color: #6c757d; + font-size: 16px; + margin-bottom: 30px; + line-height: 1.6; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.cta-buttons { + margin-top: auto; +} + +.cta-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 15px 35px; + border-radius: 25px; + font-size: 17px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.cta-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); +} + +/* Modern Home Template - Enhanced with Image */ +.modern-home-preview { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border-radius: 16px; + padding: 20px; /* Further reduced padding to increase content width more */ + height: 100%; + position: relative; + overflow: hidden; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.modern-home-preview::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 100px; + height: 100px; + background: url('data:image/svg+xml,') no-repeat center; + background-size: contain; + z-index: 0; +} + +.modern-home-preview > * { + position: relative; + z-index: 1; +} + +.preview-hero-section { + position: relative; + height: 160px; + border-radius: 12px; + overflow: hidden; + margin-bottom: 30px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.preview-hero-image { + width: 100%; + height: 100%; + background: linear-gradient(45deg, #667eea, #764ba2); + position: relative; +} + +.preview-hero-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + text-align: center; + padding: 30px; +} + +.preview-property-name { + font-size: 22px; + font-weight: 700; + margin-bottom: 10px; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-property-address { + font-size: 16px; + opacity: 0.9; + margin-bottom: 12px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-price { + font-size: 24px; + font-weight: 800; + color: #ffd700; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-content-section { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; +} + +.preview-stats { + display: flex; + justify-content: space-around; + gap: 15px; + margin-bottom: 20px; +} + +.preview-stat { + background: #f8f9fa; + padding: 12px 18px; + border-radius: 20px; + font-size: 15px; + font-weight: 600; + color: #495057; + border: 1px solid #e9ecef; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-description { + background: #f8f9fa; + padding: 20px; + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.preview-title { + font-size: 18px; + font-weight: 700; + color: #495057; + margin-bottom: 12px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-text { + font-size: 16px; + color: #6c757d; + line-height: 1.5; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-features { + display: flex; + justify-content: space-around; + gap: 15px; + margin-top: auto; +} + +.feature-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-align: center; +} + +.feature-icon { + font-size: 24px; +} + +.feature-text { + font-size: 14px; + font-weight: 600; + color: #495057; + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Asgar1 Template - Enhanced with Image and Fixed Viewport */ +.asgar1-preview { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 16px; + padding: 20px; /* Further reduced padding to increase content width more */ + height: 100%; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.asgar1-preview::before { + content: ''; + position: absolute; + top: 20px; + right: 20px; + width: 120px; + height: 120px; + background: url('data:image/svg+xml,') no-repeat center; + background-size: contain; + z-index: 0; +} + +.asgar1-preview > * { + position: relative; + z-index: 1; +} + +.preview-hero { + height: auto; + background: none; /* Removed background completely */ + border-radius: 0; + margin-bottom: 30px; + position: relative; + overflow: visible; + box-shadow: none; /* Removed shadow */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 20px; /* Added top margin to bring title into viewport */ + padding: 20px 0; +} + +.preview-hero-overlay { + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + text-align: center; + padding: 20px; + width: 100%; + height: 100%; +} + +.preview-property-name { + font-size: 28px; + font-weight: 800; + margin-bottom: 15px; + text-shadow: none; /* Removed shadow since no background */ + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ff8c00; /* Orange color for title */ + line-height: 1.2; + padding: 0 10px; + word-wrap: break-word; + max-width: 100%; + text-transform: uppercase; + letter-spacing: 1px; +} + +.preview-property-address { + font-size: 15px; + opacity: 0.95; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.2; + padding: 0 10px; + word-wrap: break-word; + max-width: 100%; +} + +.preview-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; +} + +.preview-section { + background: white; + padding: 20px; + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); +} + +.preview-section-title { + font-size: 17px; + font-weight: 700; + color: #2c3e50; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-text { + font-size: 15px; + color: #6c757d; + line-height: 1.5; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.template-preview { + margin-top: auto; +} + +.preview-features { + display: flex; + justify-content: space-around; + gap: 15px; +} + +/* Sample Template - Enhanced with Image and Orange Text */ +.sample-preview { + background: linear-gradient(135deg, #fff5f5 0%, #fed7d7 100%); + border-radius: 16px; + padding: 20px; /* Further reduced padding to increase content width more */ + height: 100%; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.sample-preview::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 100px; + height: 100px; + background: url('data:image/svg+xml,') no-repeat center; + background-size: contain; + z-index: 0; +} + +.sample-preview > * { + position: relative; + z-index: 1; +} + +.preview-header { + text-align: center; + margin-bottom: 30px; + position: relative; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Changed from black to beautiful blue-purple gradient */ + border-radius: 12px; + padding: 20px; /* Reduced padding to increase width more */ + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.preview-triangle { + width: 0; + height: 0; + border-left: 20px solid transparent; + border-right: 20px solid transparent; + border-bottom: 26px solid #ff8c00; /* Changed to orange to match text color */ + margin: 0 auto 20px; +} + +.preview-company-name { + font-size: 22px; + font-weight: 800; + color: #ffffff; /* Changed to white as requested */ + line-height: 1.2; + text-transform: uppercase; + letter-spacing: 1px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.preview-title { + font-size: 24px; + font-weight: 700; + color: #ffffff !important; /* Changed to white as requested with !important */ + margin: 15px 0; + text-transform: uppercase; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.preview-for-sale { + font-size: 16px; + font-weight: 600; + color: #ffffff; /* Changed to white for better contrast on gradient */ + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + text-shadow: 0 1px 3px rgba(0,0,0,0.4); +} + +.preview-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; +} + +.preview-section { + background: white; + padding: 20px; + border-radius: 12px; + border: 1px solid #fed7d7; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); +} + +.preview-section-title { + font-size: 17px; + font-weight: 700; + color: #2d3748; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-icons { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 15px; +} + +.preview-icon { + font-size: 28px; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1)); +} + +/* Luxury Template - Enhanced with Better Text Visibility */ +.luxury-preview { + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + border-radius: 16px; + padding: 20px; /* Further reduced padding to increase content width more */ + height: 100%; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.luxury-preview::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 120px; + height: 120px; + background: url('data:image/svg+xml,') no-repeat center; + background-size: contain; + z-index: 0; +} + +.luxury-preview > * { + position: relative; + z-index: 1; +} + +.preview-luxury-header { + text-align: center; + margin-bottom: 35px; + padding: 20px; /* Reduced padding to increase width */ + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Changed to blue-purple gradient to match 4th grid */ + border-radius: 12px; + color: white; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; +} + +.preview-luxury-header::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); + pointer-events: none; +} + +.preview-luxury-logo { + font-size: 24px; + font-weight: 800; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 1px; + position: relative; + z-index: 1; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ffffff; /* All text should be white as shown in image */ + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.preview-luxury-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 8px; + opacity: 0.95; + position: relative; + z-index: 1; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ffffff; /* Changed to white for better contrast on gradient */ + text-shadow: 0 1px 3px rgba(0,0,0,0.4); +} + +.preview-luxury-subtitle { + font-size: 16px; + opacity: 0.9; + position: relative; + z-index: 1; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ffffff; /* Changed to white for better contrast on gradient */ + text-shadow: 0 1px 3px rgba(0,0,0,0.4); +} + +.preview-luxury-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; +} + +.preview-luxury-features { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin-bottom: 20px; +} + +.preview-feature { + background: white; + padding: 15px; + border-radius: 10px; + font-size: 15px; + font-weight: 600; + color: #2d3748; + text-align: center; + border: 1px solid #e2e8f0; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.preview-luxury-description { + background: white; + padding: 20px; + border-radius: 12px; + border: 1px solid #e2e8f0; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); +} + +.preview-section { + background: white; + padding: 20px; + border-radius: 12px; + border: 1px solid #e2e8f0; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); +} + +.preview-section-title { + font-size: 17px; + font-weight: 700; + color: #2d3748; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.template-preview { + margin-top: auto; +} + +/* Selected Indicator - Enhanced */ +.selected-indicator { + position: absolute; + top: 15px; + right: 15px; + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); + color: white; + padding: 12px 20px; + border-radius: 25px; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 6px 20px rgba(72, 187, 120, 0.4); + animation: slideIn 0.3s ease; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.selected-icon { + font-size: 16px; +} + +.selected-text { + text-transform: uppercase; + letter-spacing: 0.5px; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Template-specific styling - Exact colors from image with enhanced shadows */ +.template-everkind { + background: #f8f9fa; + border: 1px solid #e9ecef; + box-shadow: 0 6px 20px rgba(0,0,0,0.1); + padding: 35px 30px; +} + +.template-shift { + background: #e8f4fd; + border: 1px solid #d1ecf1; + box-shadow: 0 8px 25px rgba(0,0,0,0.12); + padding: 35px 30px; +} + +.template-saintbarts { + background: #fff5f5; + border: 1px solid #fed7d7; + box-shadow: 0 7px 22px rgba(0,0,0,0.11); + padding: 35px 30px; +} + +.template-learnoy { + background: #f0fff4; + border: 1px solid #c6f6d5; + box-shadow: 0 5px 18px rgba(0,0,0,0.09); + padding: 35px 30px; +} + +.template-leafamp { + background: #faf5ff; + border: 1px solid #e9d8fd; + box-shadow: 0 6px 20px rgba(0,0,0,0.1); + padding: 35px 30px; +} + +.template-coreshift { + background: #fffaf0; + border: 1px solid #feebc8; + box-shadow: 0 7px 22px rgba(0,0,0,0.11); + padding: 35px 30px; +} + +.template-pdf2html { + background: #f0f8ff; + border: 1px solid #b3d9ff; + box-shadow: 0 6px 20px rgba(0,0,0,0.12); + padding: 35px 30px; +} + +/* Template Content Styles */ +.template-content { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; +} + +/* Newsletter Style Template */ +.newsletter-style { + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + +.newsletter-header h2 { + color: #667eea; + font-size: 18px; + margin-bottom: 15px; + font-weight: 600; +} + +.newsletter-main h1 { + color: #333; + font-size: 24px; + font-weight: 700; + margin-bottom: 10px; + line-height: 1.2; +} + +.newsletter-main p { + color: #666; + font-size: 14px; + line-height: 1.4; +} + +/* Real Estate Flyer Preview Styles */ +.real-estate-flyer-preview { + display: flex; + height: 100%; + min-height: 200px; + gap: 15px; +} + +.preview-left { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; +} + +.preview-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px; + border-radius: 12px; + position: relative; + min-height: 80px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.preview-triangle { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0 20px 20px; + border-color: transparent transparent #555 transparent; +} + +.preview-company { + margin-top: 8px; +} + +.preview-company-name { + font-size: 8px; + font-weight: bold; + margin-bottom: 2px; + opacity: 0.9; +} + +.preview-title { + font-size: 14px; + font-weight: bold; + margin: 8px 0 4px 0; + color: #ffffff !important; +} + +.preview-for-sale { + font-size: 8px; + opacity: 0.8; +} + +.preview-section { + background: white; + padding: 8px; + border-radius: 6px; + border: 1px solid #e0e0e0; +} + +.preview-section-title { + color: #667eea; + font-size: 10px; + font-weight: bold; + margin-bottom: 6px; +} + +.preview-text { + color: #666; + font-size: 7px; + line-height: 1.3; + margin-bottom: 6px; +} + +.preview-icons { + display: flex; + gap: 8px; + margin-top: 6px; +} + +.preview-icon { + width: 16px; + height: 16px; + background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 8px; +} + +.preview-contact { + color: #667eea; + font-size: 8px; + font-weight: bold; +} + +.preview-right { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; +} + +.preview-image-placeholder { + background: repeating-conic-gradient(#ccc 0% 25%, transparent 0% 50%) 50% / 8px 8px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + color: #999; + font-size: 8px; + border-radius: 6px; + border: 1px solid #e0e0e0; +} + +.preview-price-section { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 12px; + border-radius: 6px; + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.preview-price-label { + font-size: 6px; + opacity: 0.8; + margin-bottom: 4px; +} + +.preview-price { + font-size: 12px; + font-weight: bold; + margin-bottom: 8px; +} + +.preview-benefits { + font-size: 8px; + font-weight: bold; + margin-bottom: 6px; +} + +.preview-benefit { + font-size: 6px; + margin-bottom: 3px; + display: flex; + align-items: center; + gap: 4px; +} + +.preview-website { + font-size: 6px; + opacity: 0.8; + text-align: center; + margin-top: auto; +} + +/* Blank Template - Black theme */ +.template-blank { + background: #2c3e50; + border: 1px solid #34495e; + box-shadow: 0 8px 25px rgba(0,0,0,0.2); + padding: 30px 25px; + color: white; +} + +.template-blank .newsletter-style { + color: white; +} + +.template-blank .newsletter-header h2 { + color: #ecf0f1; +} + +.template-blank .newsletter-main h1 { + color: #bdc3c7; +} + +.template-blank .newsletter-section h4 { + color: #ecf0f1; +} + +.template-blank .newsletter-section p { + color: #bdc3c7; +} + +/* Newsletter Style Templates */ +.newsletter-style { + padding: 30px; + font-family: 'Georgia', 'Times New Roman', serif; + color: #2c3e50; + background: white; +} + +.newsletter-header h2 { + font-size: 1.8rem; + font-weight: 700; + margin: 0 0 20px 0; + color: #34495e; + text-transform: uppercase; + letter-spacing: 2px; +} + +.newsletter-main h1 { + font-size: 2.2rem; + font-weight: 300; + margin: 0 0 15px 0; + color: #2c3e50; + line-height: 1.2; +} + +.newsletter-main h3 { + font-size: 1.4rem; + font-weight: 400; + margin: 0 0 25px 0; + color: #7f8c8d; + letter-spacing: 1px; +} + +.newsletter-main .main-text { + font-size: 1.3rem; + font-weight: 400; + margin: 0 0 20px 0; + color: #2c3e50; + line-height: 1.4; +} + +.newsletter-section { + margin-bottom: 25px; +} + +.newsletter-section h4 { + font-size: 1.1rem; + font-weight: 600; + margin: 0 0 12px 0; + color: #34495e; + text-transform: uppercase; + letter-spacing: 1px; +} + +.newsletter-section p { + font-size: 0.95rem; + font-weight: 400; + margin: 0 0 15px 0; + color: #5a6c7d; + line-height: 1.5; +} + +.newsletter-section h5 { + font-size: 1rem; + font-weight: 500; + margin: 0 0 10px 0; + color: #2c3e50; + line-height: 1.3; +} + +/* Call-to-Action Buttons */ +.cta-buttons { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 15px; +} + +.cta-btn { + background: #2c3e50; + color: white; + border: none; + padding: 10px 16px; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.cta-btn:hover { + background: #34495e; + transform: translateY(-1px); +} + +/* Episode Sections */ +.episode { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + border-left: 3px solid #3498db; +} + +.episode h5 { + margin-bottom: 12px; +} + +/* Destinations */ +.destinations { + display: flex; + gap: 15px; + margin-top: 10px; +} + +.destination { + background: #ecf0f1; + color: #2c3e50; + padding: 8px 16px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Template Content */ +.template-content { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.template-icon { + font-size: 3rem; + text-align: center; + margin-bottom: 1rem; +} + +.template-content h3 { + font-size: 1.8rem; + font-weight: 600; + margin: 0 0 1rem 0; + line-height: 1.2; + text-align: center; +} + +.template-content p { + font-size: 1rem; + margin: 0 0 1.5rem 0; + opacity: 0.9; + line-height: 1.5; + text-align: center; + flex-grow: 1; +} + +.template-meta { + display: flex; + justify-content: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.category, .style { + background: rgba(255, 255, 255, 0.2); + padding: 0.3rem 0.8rem; + border-radius: 15px; + font-size: 0.8rem; + font-weight: 500; + backdrop-filter: blur(10px); +} + +/* Selected Indicator - Enhanced positioning */ +.selected-indicator { + position: absolute; + top: 20px; + right: 20px; + background: #667eea; + color: white; + padding: 10px 15px; + border-radius: 25px; + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + z-index: 100; + opacity: 1; + visibility: visible; + backdrop-filter: blur(10px); +} + +.selected-icon { + font-size: 1.1rem; + color: white; +} + +.selected-text { + color: white; + font-weight: 600; +} + +/* Ensure selected state is visible */ +.template-card.selected .selected-indicator { + opacity: 1; + visibility: visible; + display: flex; +} + +/* Step Actions */ +.step-actions { + display: flex; + justify-content: center; + gap: 1.5rem; + margin-top: 3rem; + margin-bottom: 3rem; + padding: 2rem 0; + position: relative; +} + +.step-actions::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 4px; + background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.3), transparent); + border-radius: 2px; +} + +.btn { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + padding: 1rem 2.5rem; + border: none; + border-radius: 16px; + /* Standard Button Font Size (Body Medium) */ + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + gap: 0.75rem; + text-decoration: none; + min-width: 140px; + justify-content: center; + /* Optimized Letter Spacing for Buttons */ + letter-spacing: 0.005em; + text-transform: none; + position: relative; + overflow: hidden; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + will-change: transform, box-shadow; + transform: translateZ(0); + /* Button Line Height Standard */ + line-height: 1.3; + font-variation-settings: 'wght' 500; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn:hover::before { + left: 100%; +} + +.btn-primary { + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%); + color: white; + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 8px 32px rgba(79, 70, 229, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-1px) scale(1.02); + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.15), + 0 12px 40px rgba(79, 70, 229, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #f472b6 100%); +} + +.btn-primary:active { + transform: translateY(0) scale(0.98); + transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-primary:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); + background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.9); + color: #374151; + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 1); + color: #1f2937; + transform: translateY(-1px) scale(1.02); + border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.15), + 0 2px 4px rgba(0, 0, 0, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 1); +} + +.btn-secondary:active { + transform: translateY(0) scale(0.98); + transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1); +} + +.btn-icon { + font-size: 1.1rem; + transition: transform 0.3s ease; +} + +.btn:hover .btn-icon { + transform: translateX(2px); +} + +.btn-secondary:hover .btn-icon { + transform: translateX(-2px); +} + +/* Image Replacement Modal Styles */ +.image-replacement-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.image-replacement-modal { + background: white; + border-radius: 20px; + width: 90%; + max-width: 800px; + max-height: 85vh; + overflow: hidden; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.3), + 0 8px 32px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: slideUp 0.3s ease; +} + +.image-replacement-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e5e7eb; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-radius: 20px 20px 0 0; +} + +.image-replacement-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + font-family: 'Inter', sans-serif; +} + +.image-replacement-content { + padding: 2rem; + max-height: 60vh; + overflow-y: auto; +} + +/* Source Tabs */ +.source-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 2rem; + border-bottom: 2px solid #f1f5f9; +} + +.source-tab { + padding: 0.75rem 1.5rem; + border: none; + background: transparent; + font-family: 'Inter', sans-serif; + font-weight: 500; + color: #6b7280; + cursor: pointer; + border-radius: 8px 8px 0 0; + transition: all 0.3s ease; + position: relative; +} + +.source-tab.active { + color: #4f46e5; + background: rgba(79, 70, 229, 0.05); +} + +.source-tab.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: #4f46e5; + border-radius: 1px; +} + +/* Category Navigation for Replacement */ +.category-navigation-replacement { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.category-btn-replacement { + padding: 0.5rem 1rem; + border: 1px solid #e5e7eb; + background: white; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + cursor: pointer; + transition: all 0.3s ease; + font-family: 'Inter', sans-serif; +} + +.category-btn-replacement:hover { + background: #f3f4f6; + border-color: #d1d5db; +} + +.category-btn-replacement.active { + background: #4f46e5; + color: white; + border-color: #4f46e5; +} + +/* Image Grid */ +.replacement-image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 1rem; + max-height: 400px; + overflow-y: auto; + padding: 0.5rem; +} + +.replacement-image-item { + border: 2px solid #e5e7eb; + border-radius: 12px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.replacement-image-item:hover { + border-color: #4f46e5; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(79, 70, 229, 0.15); +} + +.replacement-thumbnail { + width: 100%; + height: 120px; + object-fit: cover; + display: block; +} + +.replacement-image-info { + padding: 0.75rem; +} + +.replacement-image-title { + font-size: 0.875rem; + font-weight: 500; + color: #1f2937; + margin-bottom: 0.25rem; + font-family: 'Inter', sans-serif; +} + +.replacement-image-category { + font-size: 0.75rem; + color: #6b7280; + font-family: 'Inter', sans-serif; +} + +/* Local Upload Section */ +.local-upload-section { + text-align: center; +} + +.upload-area { + margin-bottom: 2rem; +} + +.upload-dropzone { + border: 2px dashed #d1d5db; + border-radius: 12px; + padding: 3rem 2rem; + cursor: pointer; + transition: all 0.3s ease; + background: #f9fafb; +} + +.upload-dropzone:hover { + border-color: #4f46e5; + background: #f0f4ff; +} + +.upload-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.upload-text h4 { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + color: #1f2937; + font-family: 'Inter', sans-serif; +} + +.upload-text p { + margin: 0.25rem 0; + color: #6b7280; + font-family: 'Inter', sans-serif; +} + +.upload-formats { + font-size: 0.875rem; + color: #9ca3af; +} + +.uploaded-preview { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin-top: 1.5rem; +} + +.uploaded-image { + max-width: 200px; + max-height: 200px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Action Buttons */ +.image-replacement-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + padding: 1.5rem 2rem; + border-top: 1px solid #e5e7eb; + background: #f9fafb; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translate(-50%, -50%) translateY(20px) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) translateY(0) scale(1); + } +} + +/* Action Buttons Section */ +.action-buttons-section { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; +} + +.action-btn { + min-width: 100px; + padding: 0.75rem 1.5rem; + font-size: 0.8rem; +} + +/* Save/Load/HTML Dialog Styles */ +.save-dialog-overlay, +.load-dialog-overlay, +.html-dialog-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.save-dialog-modal, +.load-dialog-modal, +.html-dialog-modal { + background: white; + border-radius: 20px; + width: 90%; + max-width: 600px; + max-height: 85vh; + overflow: hidden; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.3), + 0 8px 32px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: slideUp 0.3s ease; +} + +.save-dialog-header, +.load-dialog-header, +.html-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e5e7eb; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-radius: 20px 20px 0 0; +} + +.save-dialog-header h3, +.load-dialog-header h3, +.html-dialog-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + font-family: 'Inter Variable', 'Inter', sans-serif; +} + +.save-dialog-content, +.load-dialog-content, +.html-dialog-content { + padding: 2rem; + max-height: 50vh; + overflow-y: auto; +} + +.save-dialog-actions, +.load-dialog-actions, +.html-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + padding: 1.5rem 2rem; + border-top: 1px solid #e5e7eb; + background: #f9fafb; +} + +/* Save Dialog Specific Styles */ +.input-group { + margin-bottom: 1.5rem; +} + +.input-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #374151; + font-family: 'Inter Variable', 'Inter', sans-serif; +} + +.template-name-input { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #d1d5db; + border-radius: 8px; + font-size: 1rem; + font-family: 'Inter Variable', 'Inter', sans-serif; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.template-name-input:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +/* Load Dialog Specific Styles */ +.saved-templates-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.saved-template-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border: 1px solid #e5e7eb; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + background: white; +} + +.saved-template-item:hover { + border-color: #4f46e5; + background: #f0f4ff; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.15); +} + +.template-info { + flex: 1; +} + +.template-name { + font-weight: 600; + color: #1f2937; + margin-bottom: 0.25rem; + font-family: 'Inter Variable', 'Inter', sans-serif; +} + +.template-meta { + display: flex; + gap: 1rem; + margin-bottom: 0.5rem; + font-size: 0.875rem; + color: #6b7280; +} + +.template-preview { + font-size: 0.8rem; + color: #9ca3af; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.delete-template-btn { + background: none; + border: none; + padding: 0.5rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s ease; + font-size: 1.2rem; + color: #ef4444; +} + +.delete-template-btn:hover { + background: #fef2f2; +} + +.no-templates { + text-align: center; + padding: 2rem; + color: #6b7280; +} + +.no-templates p { + margin: 0.5rem 0; +} + +/* HTML Dialog Specific Styles */ +.html-dialog-modal { + max-width: 800px; + width: 95%; +} + +.html-preview { + width: 100%; +} + +.html-textarea { + width: 100%; + height: 400px; + padding: 1rem; + border: 1px solid #d1d5db; + border-radius: 8px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.875rem; + line-height: 1.4; + background: #f9fafb; + color: #374151; + resize: vertical; +} + +.html-textarea:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +/* Responsive Design for Professional Mobile Experience */ +@media (max-width: 768px) { + .step-actions { + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 1.5rem 0; + } + + .btn { + min-width: 200px; + padding: 1.2rem 2rem; + font-size: 1rem; + } + + .step-navigation { + gap: 1rem; + padding: 1.5rem; + margin: 0 1rem; + } + + .step-nav-item { + min-width: 100px; + padding: 0.8rem; + } + + .header-section { + padding: 3rem 1.5rem; + border-radius: 16px; + margin: 0 1rem; + } + + .main-title { + font-size: 2.5rem; + } + + .subtitle { + font-size: 1.1rem; + } +} + +@media (max-width: 480px) { + .btn { + min-width: 100%; + padding: 1.2rem 1.5rem; + } + + .step-actions { + padding: 1rem 1rem; + } + + .step-navigation { + flex-direction: column; + align-items: center; + gap: 0.5rem; + } + + .step-nav-item { + width: 100%; + max-width: 200px; + } + + /* Image Replacement Modal Responsive */ + .image-replacement-modal { + width: 95%; + max-height: 90vh; + } + + .image-replacement-header, + .image-replacement-content, + .image-replacement-actions { + padding: 1rem; + } + + .replacement-image-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 0.75rem; + } + + .category-navigation-replacement { + gap: 0.25rem; + } + + .category-btn-replacement { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + } + + .upload-dropzone { + padding: 2rem 1rem; + } + + /* Save/Load/HTML Dialog Mobile Responsive */ + .action-buttons-section { + flex-direction: column; + gap: 0.5rem; + } + + .action-btn { + width: 100%; + min-width: auto; + } + + .save-dialog-modal, + .load-dialog-modal, + .html-dialog-modal { + width: 95%; + max-height: 90vh; + } + + .save-dialog-header, + .load-dialog-header, + .html-dialog-header, + .save-dialog-content, + .load-dialog-content, + .html-dialog-content, + .save-dialog-actions, + .load-dialog-actions, + .html-dialog-actions { + padding: 1rem; + } + + .html-textarea { + height: 300px; + font-size: 0.8rem; + } + + .template-meta { + flex-direction: column; + gap: 0.25rem; + } +} + +/* Step 2 Content */ +.step2-content { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-bottom: 1rem; + max-width: 1400px; + margin: 0 auto 1rem auto; + padding: 0 1rem; +} + +/* Step 2 Grid Row 1 - Property Selection and Market Analysis */ +.step2-grid-row-1 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 1.5rem; + align-items: start; +} + +/* Step 2 Grid Row 2 - Property Details Full Width */ +.step2-grid-row-2 { + width: 100%; + margin-bottom: 1rem; +} + +/* Property Details Layout Container */ +.property-details-layout { + background: white; + border-radius: 8px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid #e1e5e9; + margin-bottom: 1rem; +} + +.property-details-layout h3 { + font-family: Georgia, 'Times New Roman', Times, serif; + font-size: 1.9rem; + font-weight: 700; + margin: 0 0 1.5rem 0; + color: #2c3e50; + text-align: center; + letter-spacing: -0.02em; + line-height: 1.2; +} + +/* Property Details Top Row - Two Columns */ +.property-details-top-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +/* Property Details Top Left and Right */ +.property-details-top-left, +.property-details-top-right { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Property Details Bottom Row - Enhanced Layout */ +.property-details-bottom-row { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Featured Description Section */ +.featured-description { + background: white; + border: 1px solid #e1e5e9; + padding: 1.5rem; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 0.5rem; +} + +.featured-description h5 { + font-size: 1.5rem !important; + color: #667eea !important; + margin-bottom: 2rem !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Description Field Styling */ +.description-title-field .value.description-title { + font-family: Georgia, 'Times New Roman', Times, serif !important; + font-size: 1.5rem !important; + font-weight: 700 !important; + color: #2c3e50 !important; + line-height: 1.4 !important; + text-align: left !important; + margin-bottom: 1rem; +} + +.description-content-field { + margin-top: 1.5rem; +} + +.description-content-field .value.description-content { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif !important; + font-size: 1.1rem !important; + font-weight: 500 !important; + color: #495057 !important; + line-height: 1.8 !important; + text-align: left !important; + padding: 1.5rem; + background: white; + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + white-space: pre-wrap; + word-wrap: break-word; + min-height: 120px; +} + +/* Secondary Information Grid */ +.secondary-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; + margin-top: 1rem; +} + +/* Property Section Groups */ +.property-section-group { + background: white; + border-radius: 6px; + padding: 1.5rem; + border: 1px solid #e1e5e9; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.property-section-group:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.property-section-group h5 { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.2rem; + font-weight: 700; + color: #2c3e50; + margin: 0 0 1.5rem 0; + text-transform: uppercase; + letter-spacing: 1px; + line-height: 1.3; +} + +/* Simplified Section Styling - All sections have consistent styling */ +.basic-info-section, +.contact-details-section, +.specifications-section, +.pricing-section, +.location-details-section, +.availability-section, +.amenities-section, +.description-section { + border: 1px solid #e1e5e9; +} + +/* Property Grid Layout */ +.property-grid { + display: grid; + grid-template-columns: 1fr; + gap: 0.8rem; +} + +/* Property Field Styling */ +.property-field { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 0.8rem 1rem; + background: white; + border-radius: 8px; + border: 1px solid #e9ecef; + transition: all 0.2s ease; +} + +.property-field:hover { + border-color: #667eea; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.property-field .label { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 600; + color: #495057; + font-size: 1.1rem; + min-width: 160px; + margin-right: 1.5rem; + letter-spacing: 0.01em; + line-height: 1.4; +} + +.property-field .value { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: #2c3e50; + font-size: 1.1rem; + font-weight: 500; + text-align: right; + word-break: break-word; + max-width: 250px; + letter-spacing: 0.01em; + line-height: 1.5; +} + +/* Property Description Special Styling */ +.property-description { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.property-description .property-field { + flex-direction: column; + align-items: flex-start; + padding: 1rem; +} + +.property-description .property-field .label { + margin-bottom: 0.5rem; + min-width: auto; + margin-right: 0; +} + +.property-description .property-field .value { + text-align: left; + max-width: 100%; + line-height: 1.5; +} + +/* Image Review Section Styling */ +.image-review-section { + background: white; + border-radius: 6px; + padding: 2rem; + margin-top: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #e1e5e9; +} + +.image-review-section h3 { + font-family: 'Playfair Display', Georgia, serif; + font-size: 2.8rem; + font-weight: 800; + margin: 0 0 1.5rem 0; + color: #2c3e50; + text-align: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.image-review-section p { + font-family: 'Inter', sans-serif; + color: #6c757d; + text-align: center; + margin-bottom: 2.5rem; + font-size: 1.2rem; + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1.6; +} + +/* Category Navigation for Step 2 - Industry Standard Design */ +.category-navigation-step2 { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; + margin-bottom: 2rem; + padding: 1.5rem 2rem; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border: 1px solid rgba(226, 232, 240, 0.8); + border-radius: 16px; + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.category-btn-step2 { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); + border: 1px solid rgba(226, 232, 240, 0.6); + color: #475569; + padding: 0.875rem 1.75rem; + border-radius: 12px; + font-size: 0.875rem; + font-weight: 500; + font-variation-settings: 'wght' 500; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + text-transform: capitalize; + letter-spacing: 0.025em; + position: relative; + overflow: hidden; + min-width: 100px; + text-align: center; + box-shadow: + 0 1px 3px 0 rgba(0, 0, 0, 0.1), + 0 1px 2px 0 rgba(0, 0, 0, 0.06); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +.category-btn-step2::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); + transition: left 0.5s ease; + z-index: 1; +} + +.category-btn-step2:hover::before { + left: 100%; +} + +.category-btn-step2:hover { + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); + border-color: rgba(99, 102, 241, 0.3); + color: #334155; + transform: translateY(-2px); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06), + 0 10px 15px -3px rgba(99, 102, 241, 0.1); +} + +.category-btn-step2.active { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + border-color: #6366f1; + color: white; + font-weight: 600; + font-variation-settings: 'wght' 600; + transform: translateY(-1px); + box-shadow: + 0 4px 6px -1px rgba(99, 102, 241, 0.4), + 0 2px 4px -1px rgba(99, 102, 241, 0.2), + 0 10px 15px -3px rgba(99, 102, 241, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} + +.category-btn-step2.active::before { + display: none; +} + +/* Image Display Area for Step 2 - Premium Design */ +.image-display-area-step2 { + background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 20px; + padding: 2.5rem; + min-height: 380px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; + position: relative; + overflow: hidden; + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.image-display-area-step2::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.2), transparent); +} + +.image-display-area-step2:hover { + transform: translateY(-2px); + box-shadow: + 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +.image-container-step2 { + text-align: center; + max-width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + position: relative; +} + +.property-image-step2 { + max-width: 100%; + max-height: 280px; + width: auto; + border-radius: 16px; + object-fit: cover; + margin-bottom: 1.5rem; + position: relative; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.25), + 0 10px 20px -8px rgba(0, 0, 0, 0.1); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid rgba(255, 255, 255, 0.9); +} + +.property-image-step2:hover { + transform: scale(1.02) translateY(-4px); + box-shadow: + 0 35px 60px -12px rgba(0, 0, 0, 0.3), + 0 15px 25px -8px rgba(0, 0, 0, 0.15); +} + +.image-info-step2 { + margin-top: 1.5rem; + padding: 1.5rem 2rem; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%); + border-radius: 16px; + border: 1px solid rgba(226, 232, 240, 0.5); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + min-width: 300px; +} + +.image-info-step2 h4 { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.25rem; + font-weight: 600; + font-variation-settings: 'wght' 600; + color: #1e293b; + margin: 0 0 0.75rem 0; + letter-spacing: -0.025em; + line-height: 1.4; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.image-category-step2 { + color: #64748b; + font-family: 'Inter Variable', 'Inter', system-ui, sans-serif; + font-size: 0.875rem; + font-weight: 500; + font-variation-settings: 'wght' 500; + text-transform: uppercase; + letter-spacing: 0.1em; + text-align: center; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); + border-radius: 8px; + border: 1px solid rgba(226, 232, 240, 0.6); + display: inline-block; +} + +.no-image-message-step2 { + text-align: center; + color: #64748b; + font-family: 'Inter Variable', 'Inter', system-ui, sans-serif; + font-style: italic; + font-size: 1.125rem; + font-weight: 500; + letter-spacing: 0.025em; + line-height: 1.6; + padding: 3rem 2rem; + background: linear-gradient(135deg, rgba(248, 250, 252, 0.8) 0%, rgba(241, 245, 249, 0.8) 100%); + border-radius: 16px; + border: 2px dashed rgba(203, 213, 225, 0.6); + margin: 2rem 0; +} + +/* Image Navigation for Step 2 - Modern Design */ +.image-navigation-step2 { + display: flex; + justify-content: center; + align-items: center; + gap: 1.5rem; + padding: 1.25rem 2rem; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%); + border-radius: 16px; + margin-top: auto; + border: 1px solid rgba(226, 232, 240, 0.6); + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.nav-btn-step2 { + background: linear-gradient(135deg, #64748b 0%, #475569 100%); + border: none; + color: white; + padding: 0.875rem 1.125rem; + border-radius: 12px; + font-size: 1.125rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + align-items: center; + justify-content: center; + min-width: 48px; + height: 48px; + position: relative; + overflow: hidden; + box-shadow: + 0 4px 6px -1px rgba(100, 116, 139, 0.4), + 0 2px 4px -1px rgba(100, 116, 139, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.nav-btn-step2::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + transition: left 0.6s ease; +} + +.nav-btn-step2:hover::before { + left: 100%; +} + +.nav-btn-step2:hover:not(:disabled) { + background: linear-gradient(135deg, #475569 0%, #334155 100%); + transform: translateY(-2px) scale(1.05); + box-shadow: + 0 10px 15px -3px rgba(100, 116, 139, 0.4), + 0 4px 6px -2px rgba(100, 116, 139, 0.25); +} + +.nav-btn-step2:active:not(:disabled) { + transform: translateY(0) scale(0.98); + box-shadow: + 0 4px 6px -1px rgba(100, 116, 139, 0.4), + 0 2px 4px -1px rgba(100, 116, 139, 0.2); +} + +.nav-btn-step2:disabled { + background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%); + cursor: not-allowed; + transform: none; + opacity: 0.5; + box-shadow: none; +} + +.nav-btn-step2:disabled::before { + display: none; +} + +.nav-icon-step2 { + font-size: 1.125rem; + line-height: 1; +} + +.image-counter-step2 { + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + padding: 0.75rem 1.5rem; + border-radius: 12px; + font-family: 'Inter Variable', 'Inter', system-ui, sans-serif; + font-weight: 600; + font-variation-settings: 'wght' 600; + color: #475569; + border: 1px solid rgba(226, 232, 240, 0.8); + box-shadow: + 0 2px 4px -1px rgba(0, 0, 0, 0.1), + 0 1px 2px -1px rgba(0, 0, 0, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + font-size: 0.875rem; + letter-spacing: 0.025em; + min-width: 80px; + text-align: center; +} + +.property-section, .market-analysis-section { + background: white; + padding: 2rem; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #e1e5e9; + margin-bottom: 1.5rem; + transition: all 0.2s ease; + height: 100%; + display: flex; + flex-direction: column; +} + +.property-section:hover, .market-analysis-section:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.property-section h3, .market-analysis-section h3 { + font-family: 'Playfair Display', Georgia, serif; + font-size: 2.2rem; + font-weight: 700; + margin: 0 0 1.5rem 0; + color: #2c3e50; + letter-spacing: -0.01em; + line-height: 1.3; +} + +.property-section p, .market-analysis-section p { + font-family: 'Inter', sans-serif; + color: #6c757d; + margin-bottom: 2rem; + line-height: 1.7; + font-size: 1.1rem; + font-weight: 500; + letter-spacing: 0.01em; +} + +/* Market Analysis Options */ +.market-options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 0.8rem; + cursor: pointer; + padding: 1rem; + border-radius: 8px; + transition: all 0.3s ease; + border: 1px solid #e9ecef; + background: #f8f9fa; +} + +.checkbox-item:hover { + background: white; + border-color: #667eea; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.checkbox-item input[type="checkbox"] { + display: none; +} + +.checkmark { + width: 20px; + height: 20px; + border: 2px solid #dee2e6; + border-radius: 4px; + position: relative; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.checkbox-item input[type="checkbox"]:checked + .checkmark { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; +} + +.checkbox-item input[type="checkbox"]:checked + .checkmark::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 14px; + font-weight: bold; +} + +.label-text { + font-family: 'Inter', sans-serif; + font-size: 1.2rem; + color: #2c3e50; + font-weight: 600; + line-height: 1.5; + letter-spacing: 0.01em; +} + +/* Responsive Design for Step 2 */ +@media (max-width: 1200px) { + .step2-grid-row-1 { + grid-template-columns: 1fr; + gap: 2rem; + } + + .property-details-top-row { + grid-template-columns: 1fr; + gap: 2rem; + } +} + +@media (max-width: 768px) { + .step2-content { + padding: 0 1rem; + gap: 2rem; + } + + .property-details-layout { + padding: 1.5rem; + } + + .property-details-layout h3 { + font-size: 2.2rem; + margin-bottom: 2rem; + } + + .image-review-section { + padding: 1.5rem; + } + + .image-review-section h3 { + font-size: 2.2rem; + } + + .category-navigation-step2 { + padding: 1rem 1.5rem; + gap: 0.5rem; + border-radius: 12px; + } + + .category-btn-step2 { + padding: 0.75rem 1.25rem; + font-size: 0.8125rem; + border-radius: 10px; + min-width: 80px; + } + + .image-display-area-step2 { + padding: 1.5rem; + min-height: 320px; + border-radius: 16px; + } + + .property-image-step2 { + max-height: 240px; + border-radius: 12px; + } + + .image-info-step2 { + padding: 1rem 1.5rem; + min-width: 250px; + border-radius: 12px; + } + + .image-info-step2 h4 { + font-size: 1.125rem; + } + + .image-navigation-step2 { + padding: 1rem 1.5rem; + gap: 1rem; + border-radius: 12px; + } + + .nav-btn-step2 { + min-width: 44px; + height: 44px; + padding: 0.75rem 1rem; + border-radius: 10px; + } + + .image-counter-step2 { + padding: 0.625rem 1.25rem; + font-size: 0.8125rem; + border-radius: 10px; + min-width: 70px; + } + + .property-field { + flex-direction: column; + align-items: flex-start; + gap: 0.8rem; + padding: 1rem 1.2rem; + } + + .property-field .label { + min-width: auto; + margin-right: 0; + font-size: 1rem; + } + + .property-field .value { + text-align: left; + max-width: 100%; + font-size: 1rem; + } + + .secondary-info-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } + + .featured-description { + padding: 1.8rem; + } + + .featured-description h5 { + font-size: 1.5rem !important; + } + + .description-title-field .value.description-title { + font-size: 1.4rem !important; + } + + .description-content-field .value.description-content { + font-size: 1.1rem !important; + padding: 1.2rem; + } + + .property-section h3, .market-analysis-section h3 { + font-size: 1.8rem; + } + + .btn { + padding: 1rem 2rem; + font-size: 1rem; + min-width: 140px; + } +} + +.property-dropdown { + width: 100%; + padding: 1rem; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1rem; + background: white; + transition: border-color 0.3s ease; +} + +.property-dropdown:focus { + outline: none; + border-color: #667eea; +} + +/* Editor Container */ +.editor-container { + display: flex; + gap: 20px; + margin-top: 20px; +} + +/* Left Toolbar - Original Layout */ +.editor-toolbar.left { + width: 300px; + flex-shrink: 0; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 20px; + height: fit-content; +} + +.toolbar-section { + margin-bottom: 25px; +} + +.toolbar-section:last-child { + margin-bottom: 0; +} + +.toolbar-section-title { + font-weight: 600; + color: #495057; + margin-bottom: 15px; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid #dee2e6; + padding-bottom: 5px; +} + +.toolbar-group { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 15px; +} + +.toolbar-group:last-child { + margin-bottom: 0; +} + +.toolbar-button { + background: #ffffff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 10px 15px; + font-size: 13px; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + box-sizing: border-box; + margin-bottom: 0.5rem; +} + +.toolbar-button:hover { + background: #e9ecef; + border-color: #adb5bd; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.toolbar-button:active { + background: #dee2e6; + transform: translateY(0); +} + +/* Insert Content Section - Fixed Layout */ +.insert-content-section .toolbar-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 5px; +} + +.insert-content-section .toolbar-group:first-child { + grid-template-columns: 1fr 1fr; +} + +.insert-content-section .toolbar-group:last-child { + grid-template-columns: 1fr 1fr; +} + +/* Text Formatting */ +.text-formatting-section .toolbar-group { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; +} + +.text-formatting-section label { + font-size: 12px; + color: #6c757d; + min-width: 80px; +} + +.text-formatting-section select { + background: white; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 6px 8px; + font-size: 12px; + color: #495057; + min-width: 120px; + flex: 1; +} + +/* Text Styling */ +.text-styling-section .toolbar-group { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px; +} + +.text-styling-section .toolbar-button { + justify-content: center; + text-align: center; + min-height: 40px; + font-weight: 600; +} + +/* Text Alignment */ +.text-alignment-section .toolbar-group { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px; +} + +.text-alignment-section .toolbar-button { + justify-content: center; + text-align: center; + min-height: 40px; +} + +/* Right Editor Area */ +.editor-right { + flex: 1; + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 20px; + min-height: 600px; +} + +/* Preview frame with exact A4 dimensions for accurate PDF generation */ +.preview-frame { + position: relative; + width: 210mm; + height: 297mm; + margin: 0 auto; + padding: 20mm; + border: 2px solid #ddd; + border-radius: 8px; + background: white; + overflow: visible; + box-sizing: border-box; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + transform: scale(0.8); + transform-origin: top center; + /* A4 Content Optimization */ + min-height: 297mm; + max-width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; +} + +/* A4 Content Scaling and Responsiveness */ +.preview-frame * { + max-width: 100%; + box-sizing: border-box; + word-wrap: break-word; + overflow-wrap: break-word; +} + +/* Ensure all content fits within A4 dimensions */ +.preview-frame img, +.preview-frame video, +.preview-frame iframe { + max-width: 100%; + height: auto; + object-fit: contain; +} + +/* A4 Grid Layout Optimization - REMOVED */ + +/* A4 Text Optimization */ +.preview-frame h1 { + font-size: clamp(20px, 4vw, 28px); + line-height: 1.2; + margin-bottom: 15px; +} + +.preview-frame h2 { + font-size: clamp(16px, 3vw, 18px); + line-height: 1.3; + margin-bottom: 12px; +} + +.preview-frame p { + font-size: clamp(12px, 2.5vw, 14px); + line-height: 1.4; + margin-bottom: 10px; +} + +/* A4 page indicator */ +.preview-frame::before { + content: 'A4 Preview (210mm × 297mm)'; + position: absolute; + top: -30px; + left: 50%; + transform: translateX(-50%); + background: #667eea; + color: white; + padding: 5px 15px; + border-radius: 15px; + font-size: 12px; + font-weight: 500; +} + +/* Template Preview Content Styles */ +.preview-left, .preview-right { + flex: 1; + padding: 15px; +} + +.preview-header { + margin-bottom: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px; + border-radius: 12px; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.preview-triangle { + width: 0; + height: 0; + border-left: 20px solid #555; + border-top: 20px solid transparent; + border-bottom: 20px solid transparent; + margin-bottom: 15px; +} + +.preview-company-name { + font-size: 12px; + font-weight: 600; + color: #ffffff; + margin-bottom: 5px; +} + +.preview-title { + font-size: 20px; + font-weight: 700; + color: #ffffff; + margin-bottom: 5px; +} + +.preview-for-sale { + font-size: 14px; + color: #ffffff; + font-weight: 500; +} + +.preview-section { + margin-bottom: 15px; +} + +.preview-section-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.preview-text { + font-size: 12px; + color: #666; + line-height: 1.4; + margin-bottom: 10px; +} + +.preview-icons { + display: flex; + gap: 8px; +} + +.preview-icon { + font-size: 16px; +} + +.preview-price-section { + text-align: center; + padding: 15px; + background: rgba(255,255,255,0.9); + border-radius: 8px; +} + +.preview-price-label { + font-size: 10px; + color: #666; + margin-bottom: 5px; +} + +.preview-price { + font-size: 18px; + font-weight: 700; + color: #ff6b35; + margin-bottom: 10px; +} + +.preview-benefits { + font-size: 12px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.preview-benefit { + font-size: 11px; + color: #666; + margin-bottom: 5px; +} + +.preview-website { + font-size: 10px; + color: #999; + margin-top: 10px; +} + +.preview-image-placeholder { + background: #f0f0f0; + height: 120px; + display: flex; + align-items: center; + justify-content: center; + color: #999; + font-size: 12px; + border-radius: 8px; + margin-bottom: 15px; +} + +/* A4 Page Break Indicators */ +.a4-page-break { + page-break-before: always; + break-before: page; + height: 0; + margin: 0; + padding: 0; + border: none; + position: relative; +} + +.a4-page-break div { + text-align: center; + color: #999; + font-size: 12px; + margin: 10px 0; + background: #f8f9fa; + padding: 5px; + border-radius: 4px; + border: 1px dashed #ddd; +} + +/* A4 Content Overflow Handling */ +.preview-frame { + overflow-x: hidden; + overflow-y: auto; +} + +/* Ensure content fits within A4 bounds */ +.preview-frame > * { + max-width: 170mm; /* 210mm - 40mm padding */ + margin-left: auto; + margin-right: auto; +} + +/* Additional Template Preview Styles */ +.asgar1-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-hero { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; + text-align: center; +} + +.preview-hero-overlay { + position: relative; +} + +.preview-property-name { + font-size: 18px; + font-weight: 700; + margin-bottom: 5px; +} + +.preview-property-address { + font-size: 14px; + opacity: 0.9; +} + +.preview-content { + flex: 1; +} + +.preview-section { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + margin-bottom: 15px; +} + +.preview-section-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.preview-text { + font-size: 12px; + color: #666; + line-height: 1.4; +} + +/* Luxury and Modern Template Styles */ +.luxury-mansion-preview, +.modern-apartment-preview { + height: 100%; + display: flex; + flex-direction: column; + text-align: center; +} + +.luxury-mansion-preview h2, +.modern-apartment-preview h2 { + font-size: 20px; + font-weight: 700; + margin-bottom: 10px; + color: #333; +} + +.luxury-mansion-preview h3, +.modern-apartment-preview h3 { + font-size: 16px; + color: #666; + margin-bottom: 15px; +} + +.luxury-mansion-preview p, +.modern-apartment-preview p { + font-size: 12px; + color: #888; + line-height: 1.4; + margin-bottom: 10px; +} + +.feature-list { + display: flex; + flex-direction: column; + gap: 8px; + margin: 15px 0; +} + +.feature-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: #555; +} + +.feature-icon { + font-size: 16px; +} + +/* Enhanced Luxury Template Styles */ +.luxury-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-luxury-header { + text-align: center; + margin-bottom: 20px; +} + +.preview-crown { + font-size: 32px; + margin-bottom: 10px; +} + +.preview-luxury-title { + font-size: 18px; + font-weight: 700; + color: #8B4513; + margin-bottom: 5px; +} + +.preview-luxury-subtitle { + font-size: 14px; + color: #A0522D; + font-weight: 500; +} + +.preview-luxury-features { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +.preview-feature { + font-size: 12px; + color: #666; + display: flex; + align-items: center; + gap: 8px; +} + +.preview-luxury-description { + margin-bottom: 20px; +} + +.preview-text { + font-size: 11px; + color: #777; + line-height: 1.4; + margin-bottom: 8px; +} + +/* Enhanced Modern Template Styles */ +.modern-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-modern-header { + text-align: center; + margin-bottom: 20px; +} + +.preview-modern-icon { + font-size: 32px; + margin-bottom: 10px; +} + +.preview-modern-title { + font-size: 18px; + font-weight: 700; + color: #2E86AB; + margin-bottom: 5px; +} + +.preview-modern-subtitle { + font-size: 14px; + color: #A23B72; + font-weight: 500; +} + +.preview-modern-features { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +.preview-modern-description { + margin-bottom: 20px; +} + +/* Serenity Shores Template Styles (sample5.html) */ +.serenity-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-serenity-header { + text-align: center; + margin-bottom: 20px; +} + +.preview-logo { + font-size: 16px; + font-weight: 700; + color: #82A09A; + border: 2px solid #82A09A; + padding: 6px 12px; + display: inline-block; + margin-bottom: 15px; + border-radius: 4px; +} + +.preview-serenity-title { + font-size: 20px; + font-weight: 700; + color: #333; + margin-bottom: 5px; +} + +.preview-serenity-subtitle { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.preview-serenity-features { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +.preview-serenity-description { + margin-bottom: 20px; +} + +/* Prestige Real Estate Template Styles (sample4.html) */ +.prestige-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-prestige-header { + text-align: center; + margin-bottom: 20px; +} + +.preview-prestige-logo { + font-size: 16px; + font-weight: 700; + color: #C0A062; + border: 2px solid #C0A062; + padding: 6px 12px; + display: inline-block; + margin-bottom: 15px; + border-radius: 4px; +} + +.preview-prestige-title { + font-size: 18px; + font-weight: 700; + color: #121212; + margin-bottom: 5px; +} + +.preview-prestige-subtitle { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.preview-prestige-features { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +.preview-prestige-description { + margin-bottom: 20px; +} + +/* Property Brochure Template Styles (sample3.html) */ +.brochure-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-brochure-header { + text-align: center; + margin-bottom: 20px; +} + +.preview-brochure-icon { + font-size: 32px; + margin-bottom: 10px; +} + +.preview-brochure-title { + font-size: 18px; + font-weight: 700; + color: #003366; + margin-bottom: 5px; +} + +.preview-brochure-subtitle { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.preview-brochure-features { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 20px; +} + +.preview-brochure-description { + margin-bottom: 20px; +} + +/* Template Preview Features */ +.template-preview { + margin-top: auto; + padding-top: 20px; + border-top: 1px solid rgba(0,0,0,0.1); +} + +.preview-features { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.preview-features .feature-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + text-align: center; +} + +.preview-features .feature-icon { + font-size: 20px; +} + +.preview-features .feature-text { + font-size: 10px; + color: #666; + font-weight: 500; +} + +/* Enhanced padding for last 3 grids */ +.template-serenity, +.template-prestige, +.template-brochure { + padding: 40px 35px; + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); + border: 2px solid #e9ecef; + position: relative; + min-height: 320px; +} + +.template-serenity::before, +.template-prestige::before, +.template-brochure::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(248,249,250,0.9) 100%); + border-radius: 16px; + z-index: -1; +} + +.template-serenity .template-content, +.template-prestige .template-content, +.template-brochure .template-content { + position: relative; + z-index: 1; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .editor-container { + flex-direction: column; + } + + .editor-toolbar.left { + width: 100%; + order: 2; + } + + .editor-right { + order: 1; + } + + .preview-frame { + transform: scale(0.7); + margin: 20px auto; + width: 95%; + max-width: 210mm; + } + + .template-grid { + /* Grid removed - using default layout */ + gap: 25px; + padding: 30px 15px; + } + + .step-content { + padding: 1.5rem 2rem; + } +} + +@media (max-width: 768px) { + .preview-frame { + transform: scale(0.6); + margin: 10px auto; + width: 90%; + max-width: 210mm; + padding: 15mm; + } + + /* Mobile A4 Optimization */ + .preview-frame h1 { + font-size: clamp(18px, 5vw, 24px); + } + + .preview-frame h2 { + font-size: clamp(14px, 4vw, 16px); + } + + .preview-frame p { + font-size: clamp(11px, 3vw, 13px); + } + + .template-grid { + grid-template-columns: 1fr; + gap: 20px; + padding: 20px 15px; + } + + .template-card { + padding: 25px 20px; + min-height: 250px; + } + + .step-content { + padding: 1rem 1.5rem; + } +} + +@media (max-width: 480px) { + .preview-frame { + transform: scale(0.5); + width: 85%; + padding: 10mm; + } + + /* Small Mobile A4 Optimization */ + .preview-frame h1 { + font-size: clamp(16px, 6vw, 20px); + } + + .preview-frame h2 { + font-size: clamp(12px, 5vw, 14px); + } + + .preview-frame p { + font-size: clamp(10px, 4vw, 12px); + } +} + +@media (max-width: 768px) { + .insert-content-section .toolbar-group { + grid-template-columns: 1fr; + } + + .text-styling-section .toolbar-group { + grid-template-columns: repeat(2, 1fr); + } + + .text-alignment-section .toolbar-group { + grid-template-columns: repeat(3, 1fr); + } +} + +/* Hide header in step 3 */ +.step-3 .step-header { + display: none; +} + +/* Page Management */ +.page-container { + margin-bottom: 30px; + border: 2px solid #e0e0e0; + border-radius: 8px; + background: white; + position: relative; + overflow: visible; +} + +.page-header { + background: #f8f9fa; + padding: 15px 20px; + border-bottom: 2px solid #e0e0e0; + border-radius: 6px 6px 0 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.page-title { + font-size: 1.1rem; + font-weight: 600; + color: #495057; + margin: 0; +} + +.page-number { + background: #6f42c1; + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; +} + +.page-content { + padding: 20px; + min-height: 400px; + overflow: visible; +} + +.page-content[contenteditable="true"] { + outline: none; + cursor: text; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; +} + +.page-content[contenteditable="true"]:focus { + border: 2px solid #6f42c1; + border-radius: 6px; +} + +/* Page Navigation */ +.page-navigation { + display: none; +} + +/* Custom Popup Styling */ +.custom-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(4px); +} + +.custom-popup-content { + background: white; + border-radius: 12px; + padding: 30px; + max-width: 500px; + width: 90%; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); + position: relative; + border: 1px solid #e0e0e0; +} + +.custom-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #6f42c1; +} + +.custom-popup-title { + font-size: 1.5rem; + font-weight: 600; + color: #2c3e50; + margin: 0; +} + +.custom-popup-close { + background: #6f42c1; + color: white; + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + transition: all 0.2s ease; +} + +.custom-popup-close:hover { + background: #8e44ad; + transform: scale(1.1); +} + +.custom-popup-body { + margin-bottom: 25px; +} + +.custom-popup-footer { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.custom-popup-btn { + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + font-size: 0.9rem; +} + +.custom-popup-btn-primary { + background: #6f42c1; + color: white; +} + +.custom-popup-btn-primary:hover { + background: #8e44ad; + transform: translateY(-1px); +} + +.custom-popup-btn-secondary { + background: #f8f9fa; + color: #495057; + border: 1px solid #d0d0d0; +} + +.custom-popup-btn-secondary:hover { + background: #e9ecef; +} + +/* Success/Error Notifications */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + color: white; + font-weight: 500; + z-index: 10001; + animation: slideInRight 0.3s ease; + max-width: 350px; +} + +.notification.success { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.notification.error { + background: linear-gradient(135deg, #dc3545 0%, #e74c3c 100%); + box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; +} + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Loading Spinner */ +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid #ffffff; + border-top: 2px solid transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + display: inline-block; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Header and Footer Edit Buttons */ +.header-section, .footer-section { + position: relative; +} + +.header-section .edit-btn, .footer-section .edit-btn { + position: absolute; + top: 10px; + right: 10px; + background: rgba(255,255,255,0.2); + padding: 5px 10px; + border-radius: 15px; + font-size: 0.8rem; + cursor: pointer; + color: white; + border: none; + transition: background 0.2s ease; +} + +.header-section .edit-btn:hover, .footer-section .edit-btn:hover { + background: rgba(255,255,255,0.3); +} + +/* Property Details Grid */ +.property-details-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 30px; +} + +.property-detail-item { + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #667eea; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.property-detail-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2); +} + +.property-detail-label { + font-weight: 600; + color: #667eea; + margin-bottom: 5px; + font-size: 0.9rem; +} + +.property-detail-value { + font-size: 1.1rem; + color: #2c3e50; +} + +/* Template-specific sections */ +.template-section { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border-left: 4px solid #667eea; +} + +.template-section h3 { + color: #2c3e50; + margin-top: 0; + font-size: 1.3rem; +} + +.template-section ul { + color: #495057; + margin: 0; + padding-left: 20px; +} + +.template-section li { + margin-bottom: 8px; + line-height: 1.4; +} + +/* Editable content areas */ +.editable-area { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 2px dashed #dee2e6; + transition: border-color 0.2s ease; +} + +.editable-area:hover { + border-color: #667eea; +} + +.editable-area h3 { + color: #6c757d; + margin-top: 0; + font-size: 1.3rem; +} + +.editable-area p { + color: #6c757d; + font-style: italic; + margin: 0; +} + +/* Responsive Toolbar */ +@media (max-width: 1200px) { + .editor-toolbar { + flex-direction: column; + align-items: stretch; +} + + .toolbar-group { + justify-content: center; + flex-wrap: wrap; + } +} + +@media (max-width: 768px) { + .toolbar-group { + flex-direction: column; + align-items: stretch; +} + + .toolbar-btn { + width: 100%; + justify-content: center; +} +} + +@media (max-width: 768px) { + .step-header-gradient { + padding: 30px 20px; + margin: -15px -15px 25px -15px; + } + + .main-title { + font-size: 3rem; + } + + .subtitle { + font-size: 1.3rem; +} + + .step-navigation { + flex-direction: column; + gap: 1rem; + margin: 0 1rem 1rem 1rem; +} + + .step2-content { + grid-template-columns: 1fr; + gap: 2rem; + } + + .template-grid { + grid-template-columns: 1fr; + gap: 1.5rem; +} + + .step-content { + padding: 0 1rem; + } + + .header-section { + padding: 2rem 1rem; +} +} + +@media (max-width: 480px) { + .main-title { + font-size: 2.5rem; + } + + .subtitle { + font-size: 1.2rem; +} + + + + .header-features { + flex-direction: column; + align-items: center; +} + + .btn { + padding: 0.8rem 1.5rem; + font-size: 0.9rem; +} +} + +/* PDF Preview Popup */ +.pdf-preview-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + backdrop-filter: blur(3px); +} + +.pdf-preview-popup { + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 95%; + max-height: 95%; + width: 1200px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.template-preview-frame { + background: white; + padding: 20px; + border-radius: 8px; + max-height: 70vh; + overflow-y: auto; + border: 1px solid #e9ecef; + box-shadow: inset 0 2px 8px rgba(0,0,0,0.05); +} + +.template-preview-frame::-webkit-scrollbar { + width: 8px; +} + +.template-preview-frame::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.template-preview-frame::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; +} + +.template-preview-frame::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%); +} + +.pdf-preview-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 12px 12px 0 0; +} + +.pdf-preview-header h3 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.close-btn { + background: none; + border: none; + color: white; + font-size: 1.5rem; + cursor: pointer; + padding: 5px; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; +} + +.close-btn:hover { + background: rgba(255, 255, 255, 0.2); +} + +.pdf-preview-content { + flex: 1; + padding: 30px; + overflow-y: auto; + max-height: 500px; +} + +.pdf-preview-content .preview-frame { + border: 2px solid #dee2e6; + border-radius: 8px; + padding: 20px; + background: white; + min-height: 400px; +} + +.pdf-preview-actions { + padding: 20px 30px; + background: #f8f9fa; + border-top: 1px solid #dee2e6; + display: flex; + gap: 15px; + justify-content: flex-end; + border-radius: 0 0 12px 12px; +} + +.pdf-preview-actions .export-pdf-btn { + position: static !important; + top: auto !important; + right: auto !important; +} + +/* Page Navigation Buttons in Toolbar */ +.page-nav-btn { + background: #f8f9fa; + border: 1px solid #d0d0d0; + color: #495057; + width: 35px; + height: 35px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9rem; + font-weight: 600; + transition: all 0.2s ease; +} + +.page-nav-btn:hover { + background: #6f42c1; + color: white; + border-color: #6f42c1; + transform: translateY(-1px); +} + +.page-nav-btn.active { + background: #6f42c1; + color: white; + border-color: #6f42c1; + box-shadow: 0 2px 8px rgba(111, 66, 193, 0.3); +} + +/* Active page styling */ +.active-page { + display: block !important; +} + +.page-container:not(.active-page) { + display: none; +} + +/* Enhanced Dropdown Styling for Step 2 */ +.property-selector { + margin: 20px 0; +} + +.property-selector label { + font-family: 'Inter', sans-serif; + display: block; + margin-bottom: 12px; + font-weight: 700; + color: #2c3e50; + font-size: 1.2rem; + letter-spacing: 0.02em; +} + +.property-selector select { + font-family: 'Inter', sans-serif; + width: 100%; + padding: 16px 20px; + border: 2px solid #e0e0e0; + border-radius: 12px; + font-size: 1.1rem; + font-weight: 500; + background: white; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + letter-spacing: 0.01em; +} + +.property-selector select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.property-selector select option { + padding: 8px; +} + +/* Enhanced Property Details Display */ +.property-details { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 25px; + margin-top: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} + +.property-details h4 { + color: #2c3e50; + margin: 0 0 20px 0; + font-size: 1.2rem; + font-weight: 600; +} + +.property-grid { + /* Grid removed - using default layout */ + gap: 15px; +} + +.property-field { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 15px; + background: white; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.property-field .label { + font-weight: 600; + color: #6c757d; + font-size: 0.9rem; +} + +.property-field .value { + font-weight: 500; + color: #2c3e50; + font-size: 0.9rem; +} + +/* Export PDF Button - Improved - Fixed positioning */ +.export-pdf-section { + position: relative; + margin: 20px 0; + z-index: 1000; + background: rgba(255, 255, 255, 0.9); + padding: 16px 20px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + border: 2px solid #e3f2fd; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + min-width: 200px; + max-width: 220px; + backdrop-filter: blur(10px); +} + +/* A4 Page Size Detection and Styling */ +.a4-page { + width: 210mm; + height: 297mm; + margin: 0 auto; + background: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.a4-page-content { + padding: 20mm; + box-sizing: border-box; + height: 100%; + overflow: hidden; +} + +.page-break { + page-break-after: always; + break-after: page; + height: 297mm; + width: 210mm; + position: relative; + margin-bottom: 10mm; +} + +.page-indicator { + position: absolute; + bottom: 5mm; + right: 5mm; + font-size: 10px; + color: #666; + background: rgba(255, 255, 255, 0.8); + padding: 2px 6px; + border-radius: 3px; +} + +.export-pdf-btn { + background: transparent; + color: #667eea; + border: 2px solid #667eea; + padding: 12px 24px; + border-radius: 25px; + font-weight: 600; + font-size: 11px; + cursor: pointer; + box-shadow: none; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.3px; + margin-bottom: 0; + white-space: nowrap; + width: 100%; + text-align: center; + line-height: 1; + min-width: 140px; +} + +.export-pdf-btn:hover { + background: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.export-pdf-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + + + +/* Page Count Display */ +.page-count-display { + text-align: center; + margin-top: 4px; +} + +.page-count-text { + color: #28a745; + font-weight: 500; + font-size: 10px; + padding: 4px 8px; + background: rgba(40, 167, 69, 0.1); + border: 1px solid rgba(40, 167, 69, 0.3); + border-radius: 12px; + display: inline-block; +} + +/* Complete Property Details Styling */ +.complete-property-details { + margin-top: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.category-section { + margin-bottom: 25px; +} + +.category-heading { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 15px; + padding-bottom: 8px; + border-bottom: 2px solid #e9ecef; +} + +.category-fields { + /* Grid removed - using default layout */ + gap: 12px; +} + +.field-item { + display: flex; + align-items: flex-start; + padding: 8px 12px; + background: white; + border-radius: 6px; + border: 1px solid #e9ecef; + transition: all 0.2s ease; +} + +.field-item:hover { + border-color: #667eea; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.field-label { + font-weight: 600; + color: #495057; + min-width: 120px; + margin-right: 12px; + font-size: 13px; +} + +.field-value { + color: #333; + font-size: 13px; + word-break: break-word; + flex: 1; +} + +/* Progress Text Display */ +.progress-text-display { + text-align: center; + margin-top: 6px; +} + +.progress-message { + color: #007bff; + font-weight: 500; + font-size: 11px; + padding: 6px 12px; + background: rgba(248, 249, 250, 0.9); + border: 1px solid rgba(222, 226, 230, 0.7); + border-radius: 15px; + display: inline-block; +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .export-pdf-section { + position: relative; + top: auto; + right: auto; + margin: 20px auto; + width: 90%; + max-width: 190px; + justify-content: center; + } + + .export-pdf-btn { + padding: 12px 24px; + font-size: 11px; + margin-right: 0; + } +} + + + +/* PDF Generation Progress Indicator */ +.pdf-progress-indicator { + text-align: center; + padding: 15px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.pdf-progress-indicator .spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid #f3f3f3; + border-top: 3px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; + flex-shrink: 0; +} + +.pdf-progress-indicator .progress-text { + color: #007bff; + font-weight: 500; + font-size: 14px; + margin: 0; +} + +/* Progress Bar for PDF Generation */ +.progress-bar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} + +.progress-content { + background: white; + padding: 40px; + border-radius: 15px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + text-align: center; + max-width: 400px; + width: 90%; +} + +.progress-spinner { + width: 60px; + height: 60px; + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +.progress-text { + font-size: 18px; + font-weight: 600; + color: #333; + margin-bottom: 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Progress bar animation */ +.progress-bar .progress-content { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); +} +} + +/* Loading Indicator */ +.loading-indicator { + text-align: center; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; + margin: 15px 0; + color: #6c757d; + font-style: italic; +} + +.loading-indicator::before { + content: "⏳ "; + margin-right: 8px; +} + +/* Debug Info - Hide in production */ +.debug-info { + display: none !important; +} + +/* Ensure clean page bottom and hide any stray content */ +.property-brochure-generator::after { + content: ""; + display: block; + clear: both; + height: 0; + overflow: hidden; +} + +/* Hide any potential stray text elements */ +.property-brochure-generator { + position: relative; +} + +/* Additional cleanup for text nodes */ +.property-brochure-generator > *:last-child::after { + content: ""; + display: block; + height: 0; + clear: both; +} + +/* Image Review Modal Overlay */ +.image-review-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.image-review-modal { + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 90%; + max-height: 90%; + width: 1200px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.image-review-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 12px 12px 0 0; +} + +.image-review-header h3 { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.image-review-content { + flex: 1; + padding: 30px; + overflow-y: auto; + max-height: 600px; +} + +.category-navigation { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + justify-content: center; +} + +.category-btn { + background: #f8f9fa; + border: 2px solid #e9ecef; + color: #495057; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.category-btn:hover, +.category-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; + color: white; + transform: translateY(-1px); +} + +.image-display-area { + background: #f8f9fa; + border-radius: 12px; + padding: 20px; + min-height: 200px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + + + +/* Editor Toolbar - Fixed Layout */ +.editor-toolbar { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-width: 100%; + overflow: hidden; +} + +.editor-toolbar.left { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.toolbar-section { + margin-bottom: 20px; + padding: 10px; + background: white; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +.toolbar-section:last-child { + margin-bottom: 0; +} + +.toolbar-section-title { + font-weight: 600; + color: #495057; + margin-bottom: 10px; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.toolbar-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-bottom: 10px; +} + +.toolbar-group:last-child { + margin-bottom: 0; +} + +.toolbar-button { + background: #ffffff; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 8px 12px; + font-size: 12px; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; + min-width: fit-content; + max-width: 100%; + box-sizing: border-box; +} + +.toolbar-button:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.toolbar-button:active { + background: #dee2e6; +} + +.button-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + width: 100%; +} + +.button-grid .toolbar-button { + justify-content: center; + text-align: center; + min-height: 36px; +} + +/* Form Controls */ +.toolbar-group label { + font-size: 12px; + color: #6c757d; + margin-right: 8px; + white-space: nowrap; +} + +.toolbar-group select { + background: white; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 6px 8px; + font-size: 12px; + color: #495057; + min-width: 100px; + max-width: 150px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .editor-toolbar { + padding: 10px; + } + + .toolbar-section { + padding: 8px; + } + + .toolbar-group { + gap: 6px; + } + + .toolbar-button { + padding: 6px 10px; + font-size: 11px; + } + + .button-grid { + grid-template-columns: 1fr; + } +} + +/* Step 3: HTML Editor - Fixed Layout */ +.step3 { + display: block; + position: relative; + height: calc(100vh - 160px); + overflow: hidden; + padding-bottom: 1rem; +} + +.step3.hidden { + display: none; +} + +/* Editor Container - Fixed Layout */ +.editor-container { + display: flex; + flex-direction: row; + gap: 20px; + height: 100%; + padding: 10px 20px; + box-sizing: border-box; + margin-bottom: 0; + max-height: calc(100vh - 180px); +} + +/* Quill Editor Container - Replaces Left Toolbar */ +.quill-editor-container { + width: 340px; + flex-shrink: 0; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border: 1px solid #dee2e6; + border-radius: 12px; + padding: 15px; + height: calc(100vh - 180px); + max-height: calc(100vh - 180px); + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + position: relative; +} + +/* Hide default scrollbar */ +.quill-editor-container::-webkit-scrollbar { + display: none; +} + +.quill-editor-container { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Custom scroll indicators for sidebar */ +.quill-editor-container::before { + content: ''; + position: absolute; + top: 10px; + right: 10px; + width: 4px; + height: 30px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 10; +} + +.quill-editor-container::after { + content: ''; + position: absolute; + bottom: 10px; + right: 10px; + width: 4px; + height: 30px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 10; +} + +.quill-editor-container:hover::before, +.quill-editor-container:hover::after { + opacity: 0.6; +} + +/* Quill Toolbar Wrapper */ +.quill-toolbar-wrapper { + margin-bottom: 25px; +} + +.quill-section-title { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.2rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 15px; + text-align: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 0.02em; +} + +/* Sticky Enhanced Toolbar Container */ +.enhanced-toolbar-sticky { + position: sticky; + top: 0; + z-index: 100; + background: white; + border-bottom: 2px solid #e9ecef; + padding: 15px 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +/* Horizontal Enhanced Toolbar */ +.enhanced-toolbar-horizontal { + display: flex; + flex-wrap: wrap; + gap: 15px; + align-items: center; + justify-content: flex-start; + max-width: 100%; + overflow-x: auto; + padding: 10px 0; +} + +/* Custom Scrollbar Area for Toolbar */ +.enhanced-toolbar-scroll { + height: 100%; + overflow-y: auto; + padding-right: 10px; + margin-right: -10px; +} + +/* Hide native scrollbars but keep functionality */ +.enhanced-toolbar-scroll::-webkit-scrollbar { + width: 0px; + background: transparent; +} + +.enhanced-toolbar-scroll { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Enhanced Toolbar - Sidebar Style */ +.enhanced-toolbar { + height: calc(100vh - 200px); + display: flex; + flex-direction: column; + gap: 1rem; + overflow-y: auto; + padding: 1rem; + background: #ffffff; + border: 1px solid #e1e5e9; + border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + max-height: 800px; + position: relative; +} + +/* Custom scroll indicator */ +.enhanced-toolbar::after { + content: ''; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 40px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 2px; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.enhanced-toolbar:hover::after { + opacity: 0.3; + animation: scrollIndicator 2s ease-in-out infinite; +} + +@keyframes scrollIndicator { + 0%, 100% { transform: translateY(-70%); } + 50% { transform: translateY(-30%); } +} + +.enhanced-toolbar .toolbar-section { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.enhanced-toolbar .toolbar-section:last-child { + margin-bottom: 0; +} + +.enhanced-toolbar .toolbar-section-title { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-weight: 600; + color: #495057; + margin-bottom: 12px; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid #dee2e6; + padding-bottom: 4px; +} + +.enhanced-toolbar .toolbar-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 12px; +} + +.enhanced-toolbar .toolbar-group:last-child { + margin-bottom: 0; +} + +.enhanced-toolbar .toolbar-button { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #ffffff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 8px 12px; + font-size: 12px; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; + width: 100%; + box-sizing: border-box; +} + +.enhanced-toolbar .toolbar-button:hover { + background: #e9ecef; + border-color: #adb5bd; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.enhanced-toolbar .toolbar-button:active { + background: #dee2e6; + transform: translateY(0); +} + +.enhanced-toolbar .toolbar-group label { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 12px; + color: #6c757d; + margin-bottom: 4px; + font-weight: 500; +} + +.enhanced-toolbar .toolbar-group select { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: white; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 6px 8px; + font-size: 12px; + color: #495057; + transition: all 0.2s ease; +} + +.enhanced-toolbar .toolbar-group select:hover { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.enhanced-toolbar .toolbar-group input[type="color"] { + width: 100%; + height: 32px; + border: 1px solid #dee2e6; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.enhanced-toolbar .toolbar-group input[type="color"]:hover { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.enhanced-toolbar .button-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.enhanced-toolbar .alignment-buttons { + display: flex; + flex-direction: column; + gap: 6px; +} + +.enhanced-toolbar .alignment-btn { + justify-content: flex-start; +} + +/* Property Insert Section */ +.property-insert-section { + margin-bottom: 25px; + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.property-insert-title { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 15px; + text-align: center; + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 0.02em; +} + +.property-insert-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.property-insert-btn { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 10px 12px; + font-size: 11px; + font-weight: 600; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.property-insert-btn:hover { + background: #28a745; + border-color: #28a745; + color: white; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2); +} + +/* Document Actions Section */ +.document-actions-section { + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.document-actions-title { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1rem; + font-weight: 700; + color: #2c3e50; + margin-bottom: 15px; + text-align: center; + background: linear-gradient(135deg, #dc3545 0%, #e74c3c 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 0.02em; +} + +.document-actions-grid { + display: flex; + flex-direction: column; + gap: 10px; +} + +.doc-action-btn { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 12px 15px; + font-size: 12px; + font-weight: 600; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; +} + +.doc-action-btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.doc-action-btn.save-btn:hover { + background: #28a745; + border-color: #28a745; + color: white; +} + +.doc-action-btn.load-btn:hover { + background: #007bff; + border-color: #007bff; + color: white; +} + +.doc-action-btn.reset-btn:hover { + background: #dc3545; + border-color: #dc3545; + color: white; +} + +.doc-action-btn.preview-btn:hover { + background: #667eea; + border-color: #667eea; + color: white; +} + +.doc-action-btn.undo-btn { + background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); + border-color: #9ca3af; + color: #374151; +} + +.doc-action-btn.undo-btn:hover { + background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%); + border-color: #6b7280; + color: #111827; +} + +.doc-action-btn.redo-btn { + background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); + border-color: #9ca3af; + color: #374151; +} + +.doc-action-btn.redo-btn:hover { + background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%); + border-color: #6b7280; + color: #111827; +} + +/* Generate PDF Section in Template Header */ +.generate-pdf-section { + display: flex; + align-items: center; +} + +.generate-pdf-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; +} + +.generate-pdf-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.generate-pdf-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Enhanced Editor Content Area */ +.enhanced-editor-content { + background: white; + border: 1px solid #dee2e6; + border-radius: 12px; + height: calc(100vh - 200px); + max-height: calc(100vh - 200px); + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + line-height: 1.6; + color: #333; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + overflow-y: auto; + position: relative; + scroll-behavior: smooth; + user-select: text; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; +} + +.enhanced-editor-content:focus { + outline: none; +} + +/* Image Click Detection Enhancements */ +.enhanced-editor-content img { + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.enhanced-editor-content img:hover { + opacity: 0.9; + transform: scale(1.02); + box-shadow: 0 4px 15px rgba(79, 70, 229, 0.2); + border-radius: 4px; +} + +/* Enhanced hover effects for draggable image containers */ +.enhanced-editor-content .draggable-element:has(img):hover { + outline: 2px dashed #4f46e5; + outline-offset: 4px; +} + +/* Only show cursor pointer for actual images and image containers */ +.enhanced-editor-content *[style*="background-image"]:hover { + cursor: pointer; +} + +@keyframes imageHintFade { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} + +/* International Typography Scale - Following Material Design 3 & Apple HIG */ +.display-large { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 3.5rem; /* 56px */ + font-weight: 400; + line-height: 1.12; + letter-spacing: -0.025em; + font-variation-settings: 'wght' 400; +} + +.display-medium { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 2.8125rem; /* 45px */ + font-weight: 400; + line-height: 1.15; + letter-spacing: -0.015em; + font-variation-settings: 'wght' 400; +} + +.display-small { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 2.25rem; /* 36px */ + font-weight: 400; + line-height: 1.22; + letter-spacing: -0.005em; + font-variation-settings: 'wght' 400; +} + +.headline-large { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 2rem; /* 32px */ + font-weight: 600; + line-height: 1.25; + letter-spacing: -0.005em; + font-variation-settings: 'wght' 600; +} + +.headline-medium { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.75rem; /* 28px */ + font-weight: 600; + line-height: 1.28; + letter-spacing: 0; + font-variation-settings: 'wght' 600; +} + +.headline-small { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.5rem; /* 24px */ + font-weight: 600; + line-height: 1.33; + letter-spacing: 0; + font-variation-settings: 'wght' 600; +} + +.title-large { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.375rem; /* 22px */ + font-weight: 500; + line-height: 1.27; + letter-spacing: 0; + font-variation-settings: 'wght' 500; +} + +.title-medium { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.125rem; /* 18px */ + font-weight: 500; + line-height: 1.33; + letter-spacing: 0.009em; + font-variation-settings: 'wght' 500; +} + +.title-small { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.875rem; /* 14px */ + font-weight: 500; + line-height: 1.43; + letter-spacing: 0.007em; + font-variation-settings: 'wght' 500; +} + +.body-large { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1rem; /* 16px */ + font-weight: 400; + line-height: 1.5; + letter-spacing: 0.031em; + font-variation-settings: 'wght' 400; +} + +.body-medium { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.875rem; /* 14px */ + font-weight: 400; + line-height: 1.43; + letter-spacing: 0.017em; + font-variation-settings: 'wght' 400; +} + +.body-small { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.75rem; /* 12px */ + font-weight: 400; + line-height: 1.33; + letter-spacing: 0.033em; + font-variation-settings: 'wght' 400; +} + +.label-large { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.875rem; /* 14px */ + font-weight: 500; + line-height: 1.43; + letter-spacing: 0.007em; + font-variation-settings: 'wght' 500; +} + +.label-medium { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.75rem; /* 12px */ + font-weight: 500; + line-height: 1.33; + letter-spacing: 0.042em; + font-variation-settings: 'wght' 500; +} + +.label-small { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 0.6875rem; /* 11px */ + font-weight: 500; + line-height: 1.45; + letter-spacing: 0.045em; + font-variation-settings: 'wght' 500; +} + +.enhanced-editor-content[contenteditable="true"] { + cursor: text; + overflow-y: auto; + padding-right: 30px; + margin-right: -10px; +} + +/* Prevent jumping to top when interacting with draggable elements */ +.enhanced-editor-content * { + scroll-margin-top: 0; +} + +/* Ensure text content is editable */ +.enhanced-editor-content p, +.enhanced-editor-content div:not(.draggable-element), +.enhanced-editor-content span, +.enhanced-editor-content h1, +.enhanced-editor-content h2, +.enhanced-editor-content h3, +.enhanced-editor-content h4, +.enhanced-editor-content h5, +.enhanced-editor-content h6 { + user-select: text; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + cursor: text; +} + +/* Draggable and Resizable Elements */ +.draggable-element { + position: absolute; + border: 2px dashed transparent; + cursor: move; + min-width: 50px; + min-height: 20px; + z-index: 1000; + box-sizing: border-box; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.draggable-element.draggable-text { + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; + cursor: text; +} + +.draggable-element:hover, +.draggable-element.selected { + border-color: #667eea; + background: rgba(102, 126, 234, 0.1); + box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3); +} + +.draggable-element.dragging { + opacity: 0.8; + z-index: 1001; + cursor: move !important; +} + +.draggable-element.resizing { + z-index: 1001; +} + +/* Resize Handles */ +.resize-handle { + position: absolute; + background: #667eea; + border: 1px solid white; + width: 8px; + height: 8px; + border-radius: 50%; + opacity: 0; + transition: opacity 0.2s; + z-index: 1002; +} + +.draggable-element:hover .resize-handle, +.draggable-element.selected .resize-handle { + opacity: 1; +} + +.resize-handle.nw { top: -4px; left: -4px; cursor: nw-resize; } +.resize-handle.ne { top: -4px; right: -4px; cursor: ne-resize; } +.resize-handle.sw { bottom: -4px; left: -4px; cursor: sw-resize; } +.resize-handle.se { bottom: -4px; right: -4px; cursor: se-resize; } +.resize-handle.n { top: -4px; left: 50%; margin-left: -4px; cursor: n-resize; } +.resize-handle.s { bottom: -4px; left: 50%; margin-left: -4px; cursor: s-resize; } +.resize-handle.w { top: 50%; left: -4px; margin-top: -4px; cursor: w-resize; } +.resize-handle.e { top: 50%; right: -4px; margin-top: -4px; cursor: e-resize; } + +/* Draggable Image */ +.draggable-image { + max-width: 100%; + height: auto; + display: block; +} + +/* Draggable Text */ +.draggable-text { + padding: 8px; + min-height: 30px; + outline: none; + word-wrap: break-word; + z-index: 1000; + position: absolute; + background: rgba(255, 255, 255, 0.9); + border-radius: 4px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.4; +} + +.draggable-text:focus { + outline: 2px solid #667eea; + background: rgba(255, 255, 255, 1); +} + +/* Loading Spinner */ +.loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #ffffff; + animation: spin 1s ease-in-out infinite; + margin-right: 8px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.generate-pdf-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + background: linear-gradient(135deg, #999 0%, #666 100%); +} + +.enhanced-editor-content:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.enhanced-editor-content[contenteditable="true"] { + cursor: text; + overflow-y: auto; + padding-right: 30px; + margin-right: -10px; +} + +/* Hide scrollbars on editor content */ +.enhanced-editor-content::-webkit-scrollbar { + width: 0px; + background: transparent; +} + +.enhanced-editor-content { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Custom scroll indicator for editor */ +.editor-right::after { + content: ''; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 60px; + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + border-radius: 2px; + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: 10; +} + +.editor-right:hover::after { + opacity: 0.4; + animation: editorScrollIndicator 2s ease-in-out infinite; +} + +@keyframes editorScrollIndicator { + 0%, 100% { transform: translateY(-70%); } + 50% { transform: translateY(-30%); } +} + +.enhanced-editor-content[contenteditable="true"]:empty::before { + content: "Start editing your template here..."; + color: #6c757d; + font-style: italic; +} + +.toolbar-section { + margin-bottom: 20px; +} + +.toolbar-section:last-child { + margin-bottom: 0; +} + +.toolbar-section-title { + font-weight: 600; + color: #495057; + margin-bottom: 12px; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid #dee2e6; + padding-bottom: 4px; +} + +.toolbar-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 12px; +} + +.toolbar-group:last-child { + margin-bottom: 0; +} + +.toolbar-button { + background: #ffffff; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 8px 12px; + font-size: 12px; + color: #495057; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; + width: 100%; + box-sizing: border-box; +} + +.toolbar-button:hover { + background: #e9ecef; + border-color: #adb5bd; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.toolbar-button:active { + background: #dee2e6; + transform: translateY(0); +} + +/* Insert Content Section - Compact Layout */ +.insert-content-section .toolbar-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.insert-content-section .toolbar-group:first-child { + grid-template-columns: 1fr 1fr; +} + +.insert-content-section .toolbar-group:last-child { + grid-template-columns: 1fr 1fr; +} + +/* Text Formatting - Compact */ +.text-formatting-section .toolbar-group { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +.text-formatting-section label { + font-size: 11px; + color: #6c757d; + min-width: 70px; +} + +.text-formatting-section select { + background: white; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 5px 6px; + font-size: 11px; + color: #495057; + min-width: 100px; + flex: 1; +} + +/* Text Styling - Compact */ +.text-styling-section .toolbar-group { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; +} + +.text-styling-section .toolbar-button { + justify-content: center; + text-align: center; + min-height: 32px; + font-weight: 600; +} + +/* Text Alignment - Compact */ +.text-alignment-section .toolbar-group { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; +} + +.text-alignment-section .toolbar-button { + justify-content: center; + text-align: center; + min-height: 32px; +} + +/* Right Editor Area - Takes Remaining Space */ +.editor-right { + flex: 1; + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 20px; + height: 100%; + max-height: calc(100vh - 250px); + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; +} + +.preview-frame { + flex: 1; + width: 100%; + border: none; + outline: none; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background: white; + resize: none; + overflow-y: auto; + padding: 20px; + box-sizing: border-box; + max-height: 100%; +} + +/* Export PDF Button - Positioned at Template Level */ +.export-pdf-section { + position: absolute; + top: 20px; + right: 20px; + z-index: 10; +} + +.export-pdf-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + text-transform: uppercase; + font-size: 14px; + transition: all 0.3s ease; +} + +.export-pdf-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.export-pdf-btn:active { + transform: translateY(0); +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .editor-container { + flex-direction: column; + height: auto; + } + + .editor-toolbar.left { + width: 100%; + height: auto; + order: 2; + } + + .editor-right { + order: 1; + height: 500px; + } +} + +@media (max-width: 768px) { + .insert-content-section .toolbar-group { + grid-template-columns: 1fr; + } + + .text-styling-section .toolbar-group { + grid-template-columns: repeat(2, 1fr); + } + + .text-alignment-section .toolbar-group { + grid-template-columns: repeat(3, 1fr); + } +} + +/* Export PDF Button - Positioned at Template Header with Animation */ +.export-pdf-section { + position: absolute; + top: 40px; + right: 40px; + z-index: 1000; + animation: pulse-grow 2s ease-in-out infinite; +} + +.export-pdf-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + text-transform: uppercase; + font-size: 14px; + transition: all 0.3s ease; + transform-origin: center; +} + +.export-pdf-btn:hover { + transform: translateY(-2px) scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); + animation: none; +} + +.export-pdf-btn:active { + transform: translateY(0) scale(0.95); +} + +/* Pulse Animation: Small -> Big -> Small -> Big */ +@keyframes pulse-grow { + 0% { + transform: scale(1); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + } + 25% { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); + } + 50% { + transform: scale(1); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + } + 75% { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); + } + 100% { + transform: scale(1); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + } +} + +/* Template Header Area - Position for Export Button */ +.template-header-area { + position: relative; + margin-bottom: 20px; + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + border: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 20px; + flex-shrink: 0; +} + + + +/* Enhanced visual feedback for scrollable areas */ +.enhanced-toolbar-scroll:hover, +.enhanced-editor-content:hover { + background: linear-gradient(135deg, #fafbfc 0%, #ffffff 100%); +} + +/* Subtle border animation on hover */ +.enhanced-toolbar:hover { + border-color: #667eea; + box-shadow: 0 4px 20px rgba(102, 126, 234, 0.1); +} + +.editor-right:hover { + border-color: #28a745; + box-shadow: 0 4px 20px rgba(40, 167, 69, 0.1); +} + +/* Page Size Section */ +.page-size-section { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; +} + +.page-size-label { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 1.1rem; + font-weight: 600; + color: #2c3e50; + margin: 0; + letter-spacing: 0.01em; +} + +.page-size-options { + display: flex; + gap: 12px; + align-items: center; +} + +.page-size-option { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 8px 16px; + background: #f8f9fa; + border: 2px solid #e9ecef; + border-radius: 10px; + transition: all 0.3s ease; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-weight: 500; + color: #495057; + min-width: 60px; + justify-content: center; +} + +.page-size-option:hover { + background: #e9ecef; + border-color: #667eea; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.page-size-option input[type="radio"] { + display: none; +} + +.page-size-option input[type="radio"]:checked + .page-size-text { + color: white; +} + +.page-size-option:has(input:checked) { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; + color: white; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); +} + +.page-size-text { + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; + transition: color 0.3s ease; +} + +/* Custom Popup Styles - Matching Our Theme */ +.custom-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + +.popup-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); +} + +.popup-container { + position: relative; + background: white; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + animation: popupSlideIn 0.3s ease-out; +} + +@keyframes popupSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.popup-header { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 20px; + border-radius: 12px 12px 0 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.popup-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.popup-close { + background: none; + border: none; + color: white; + font-size: 24px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.popup-close:hover { + background: rgba(255, 255, 255, 0.2); +} + +.popup-content { + padding: 25px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #495057; + font-size: 14px; +} + +.form-control { + width: 100%; + padding: 10px 12px; + border: 1px solid #dee2e6; + border-radius: 6px; + font-size: 14px; + color: #495057; + background: white; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + box-sizing: border-box; +} + +.form-control:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-control[type="color"] { + height: 40px; + padding: 5px; + cursor: pointer; +} + +.popup-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 25px; + padding-top: 20px; + border-top: 1px solid #e9ecef; +} + +.popup-actions .btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + min-width: 80px; +} + +.popup-actions .btn-secondary { + background: #6c757d; + color: white; +} + +.popup-actions .btn-secondary:hover { + background: #5a6268; + transform: translateY(-1px); +} + +.popup-actions .btn-primary { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; +} + +.popup-actions .btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +/* Responsive popup */ +@media (max-width: 768px) { + .popup-container { + width: 95%; + margin: 20px; + } + + .popup-content { + padding: 20px; + } + + .popup-actions { + flex-direction: column; + } + + .popup-actions .btn { + width: 100%; + } +} + +/* General button spacing to prevent merging with page bottom */ +button, .btn, .toolbar-button, .export-pdf-btn { + margin-bottom: 0.5rem !important; +} + +/* Ensure proper spacing for button containers */ +.step-actions, .toolbar-section, .popup-actions { + margin-bottom: 1rem; +} + +/* Reduce bottom spacing in step 3 specifically */ +.step3 .step-actions, +.step3 .toolbar-section, +.step3 .popup-actions { + margin-bottom: 0.5rem; +} + +.step3 button, +.step3 .btn, +.step3 .toolbar-button { + margin-bottom: 0.3rem !important; +} + +/* Draggable image container styles */ +.draggable-image-container { + position: absolute; + cursor: move; + user-select: none; + z-index: 1000; + min-width: 100px; + min-height: 100px; + border: 2px dashed #667eea; + border-radius: 8px; + background: rgba(102, 126, 234, 0.1); + padding: 5px; + transition: all 0.2s ease; + transform: translate3d(0, 0, 0); +} + +.draggable-image-container:hover { + border-color: #764ba2; + background: rgba(102, 126, 234, 0.15); + transform: translate3d(0, -2px, 0); + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2); +} + +.draggable-image-container.dragging { + border-color: #764ba2; + background: rgba(102, 126, 234, 0.2); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); + transform: scale(1.02); + transition: none; + z-index: 1001; +} + +.draggable-image-container:focus { + outline: 2px solid #667eea; + outline-offset: 2px; +} + +.draggable-image-container img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 6px; + pointer-events: none; + user-select: none; +} + +/* Resize handle */ +.resize-handle { + position: absolute; + bottom: -8px; + right: -8px; + width: 24px; + height: 24px; + background: #667eea; + color: white; + border-radius: 50%; + cursor: se-resize; + border: 2px solid white; + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: bold; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + transition: all 0.2s ease; + user-select: none; +} + +.resize-handle:hover { + background: #764ba2; + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0,0,0,0.4); +} + +.resize-handle:active { + background: #5a4fcf; + transform: scale(0.95); +} + +/* Delete button */ +.delete-image-btn { + position: absolute; + top: -10px; + right: -10px; + width: 24px; + height: 24px; + background: #ff4757; + color: white; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; + border: 2px solid white; + z-index: 1002; + transition: background-color 0.3s ease; +} + +.delete-image-btn:hover { + background: #ff3742; +} + +/* Image info display */ +.image-info { + position: absolute; + top: -25px; + left: 0; + background: rgba(0,0,0,0.8); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + white-space: nowrap; + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; + z-index: 1003; +} + +/* Dragging state */ +.draggable-image-container.dragging { + border-color: #764ba2; + background: rgba(102, 126, 234, 0.2); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); +} + +/* Resizing state */ +.draggable-image-container.resizing { + border-color: #ff6b6b; + background: rgba(255, 107, 107, 0.1); +} + +/* Text Alignment Section - Fixed Width */ +.text-alignment-section .toolbar-group { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.alignment-buttons { + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} + +.alignment-btn { + width: 100%; + justify-content: flex-start; + padding: 8px 12px; + font-size: 12px; + min-height: 36px; +} + +/* Toolbar Section Ordering */ +.toolbar-section:nth-child(1) { order: 1; } /* Document Actions */ +.toolbar-section:nth-child(2) { order: 2; } /* Text Formatting */ +.toolbar-section:nth-child(3) { order: 3; } /* Text Styling */ +.toolbar-section:nth-child(4) { order: 4; } /* Text Alignment */ +.toolbar-section:nth-child(5) { order: 5; } /* Colors */ +.toolbar-section:nth-child(6) { order: 6; } /* Lists & Indentation */ +.toolbar-section:nth-child(7) { order: 7; } /* Element Positioning */ +.toolbar-section:nth-child(8) { order: 8; } /* Insert Content */ + +/* Asgar-1 Template Preview Styles */ +.template-asgar1 { + background: linear-gradient(135deg, #003366, #004080); + border: 1px solid #002244; + box-shadow: 0 6px 20px rgba(0,51,102,0.15); + padding: 30px 25px; +} + +.asgar1-preview { + display: flex; + flex-direction: column; + height: 100%; + min-height: 200px; + gap: 15px; +} + +.preview-hero { + background: linear-gradient(135deg, #003366, #004080); + color: white; + padding: 20px; + border-radius: 8px; + text-align: center; + position: relative; + overflow: hidden; +} + +.preview-hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +.preview-hero-overlay { + position: relative; + z-index: 1; +} + +.preview-property-name { + font-size: 18px; + font-weight: bold; + margin-bottom: 8px; + color: #f39c12; +} + +.preview-property-address { + font-size: 12px; + opacity: 0.9; +} + +.preview-content { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.preview-section { + background: white; + padding: 12px; + border-radius: 6px; + border-left: 4px solid #f39c12; +} + +.preview-section-title { + color: #003366; + font-size: 11px; + font-weight: bold; + margin-bottom: 6px; +} + +.preview-text { + color: #666; + font-size: 9px; + line-height: 1.3; +} + +/* Preview Frame */ +.preview-frame { + width: 100%; + height: 600px; + border: 1px solid #ddd; + border-radius: 8px; + background: white; + overflow-y: auto; + overflow-x: hidden; /* Prevent horizontal scrollbar */ + position: relative; +} + +.preview-frame iframe { + width: 100%; + height: 100%; + border: none; + border-radius: 8px; +} + +/* Brochure Content Responsiveness */ +.brochure { + max-width: 100% !important; + width: 100% !important; + box-sizing: border-box; + overflow-x: hidden; +} + +/* Ensure all content fits within viewport */ +.preview-frame * { + max-width: 100% !important; + box-sizing: border-box; +} + +/* Fix for grid layouts that might cause overflow */ +.content.grid-layout { + max-width: 100%; + overflow-x: hidden; +} + +/* Ensure amenities grid doesn't overflow */ +.amenities-grid { + max-width: 100%; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +/* Fix for location section grid */ +.location-section { + max-width: 100%; + grid-template-columns: 1fr 1fr; + gap: 30px; +} + +/* Responsive adjustments for smaller screens */ +@media (max-width: 1200px) { + .amenities-grid { + grid-template-columns: 1fr; + } + + .location-section { + grid-template-columns: 1fr; + } + + .content.grid-layout { + grid-template-columns: 1fr; + } +} + +/* Sample Template Preview Styles */ +.template-sample { + background: linear-gradient(135deg, #667eea, #764ba2); + border: 1px solid #5a6fd8; + box-shadow: 0 6px 20px rgba(102,126,234,0.15); + padding: 30px 25px; +} + +.sample-preview { + display: flex; + flex-direction: column; + height: 100%; + min-height: 200px; + gap: 15px; +} + +.preview-header { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 20px; + border-radius: 8px; + text-align: center; + position: relative; + overflow: hidden; +} + +.preview-triangle { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + border-style: solid; + border-width: 0 0 30px 30px; + border-color: transparent transparent rgba(255,255,255,0.2) transparent; +} + +.preview-company { + margin-bottom: 10px; +} + +.preview-company-name { + font-size: 10px; + font-weight: bold; + margin-bottom: 2px; + opacity: 0.9; +} + +.preview-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; +} + +.preview-for-sale { + font-size: 9px; + opacity: 0.8; +} + +.preview-content { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.preview-section { + background: white; + padding: 12px; + border-radius: 6px; + border-left: 4px solid #667eea; +} + +.preview-section-title { + color: #667eea; + font-size: 11px; + font-weight: bold; + margin-bottom: 6px; +} + +.preview-text { + color: #666; + font-size: 9px; + line-height: 1.3; + margin-bottom: 8px; +} + +.preview-icons { + display: flex; + gap: 8px; + justify-content: center; +} + +.preview-icon { + width: 20px; + height: 20px; + background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 10px; +} + +/* Luxury Mansion Template Preview Styles */ +.template-luxury { + background: white; + border: 1px solid #e5e7eb; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + padding: 30px 25px; +} + +.luxury-preview { + display: flex; + flex-direction: column; + height: 100%; + min-height: 200px; + gap: 15px; +} + +.preview-luxury-header { + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + color: #1f2937; + padding: 20px; + border-radius: 8px; + text-align: center; + position: relative; + overflow: hidden; + border: 1px solid #e5e7eb; +} + +.preview-crown { + font-size: 24px; + margin-bottom: 8px; +} + +.preview-luxury-title { + font-size: 14px; + font-weight: bold; + margin-bottom: 4px; + text-shadow: 1px 1px 2px rgba(255,255,255,0.5); +} + +.preview-luxury-subtitle { + font-size: 10px; + opacity: 0.8; + font-weight: 500; +} + +.preview-luxury-content { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.preview-luxury-features { + display: flex; + flex-direction: column; + gap: 8px; +} + +.preview-feature { + background: white; + padding: 8px 12px; + border-radius: 6px; + font-size: 9px; + color: #8B4513; + font-weight: 500; + border-left: 3px solid #FFD700; +} + +.preview-luxury-description { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* Modern Apartment Template Preview Styles */ +.template-modern { + background: linear-gradient(135deg, #00CED1, #20B2AA); + border: 1px solid #00CED1; + box-shadow: 0 6px 20px rgba(0,206,209,0.2); + padding: 30px 25px; +} + +.modern-preview { + display: flex; + flex-direction: column; + height: 100%; + min-height: 200px; + gap: 15px; +} + +.preview-modern-header { + background: linear-gradient(135deg, #00CED1, #20B2AA); + color: white; + padding: 20px; + border-radius: 8px; + text-align: center; + position: relative; + overflow: hidden; +} + +.preview-modern-icon { + font-size: 24px; + margin-bottom: 8px; +} + +.preview-modern-title { + font-size: 14px; + font-weight: bold; + margin-bottom: 4px; +} + +.preview-modern-subtitle { + font-size: 10px; + opacity: 0.8; + font-weight: 500; +} + +.preview-modern-content { + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.preview-modern-features { + display: flex; + flex-direction: column; + gap: 8px; +} + +.preview-modern-description { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* Luxury Data Overview Styles */ +.luxury-data-grid { + /* Grid removed - using default layout */ + gap: 20px; + margin-top: 25px; +} + +.data-card { + background: linear-gradient(135deg, #fafafa, #f0f0f0); + padding: 25px; + border-radius: 12px; + border: 2px solid #FFD700; + text-align: center; + transition: all 0.3s ease; +} + +.data-card:hover { + transform: translateY(-5px); + box-shadow: 0 15px 30px rgba(255,215,0,0.2); + border-color: #FFA500; +} + +.data-icon { + font-size: 36px; + margin-bottom: 15px; + color: #8B4513; +} + +.data-card h3 { + color: #8B4513; + font-size: 18px; + margin-bottom: 12px; + font-weight: 600; +} + +.data-value { + color: #333; + font-size: 16px; + font-weight: 500; + line-height: 1.4; +} + +/* Luxury Field Overview Styles */ +.luxury-field-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 15px; + margin-top: 25px; +} + +.field-item { + background: white; + padding: 18px 20px; + border-radius: 10px; + border-left: 4px solid #FFD700; + box-shadow: 0 4px 15px rgba(0,0,0,0.08); + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.3s ease; +} + +.field-item:hover { + transform: translateX(8px); + box-shadow: 0 8px 25px rgba(0,0,0,0.15); +} + +.field-label { + color: #8B4513; + font-weight: 600; + font-size: 14px; +} + +.field-value { + color: #333; + font-weight: 500; + font-size: 14px; + text-align: right; +} + +/* Modern Data Overview Styles */ +.modern-data-grid { + /* Grid removed - using default layout */ + gap: 18px; + margin-top: 25px; +} + +.modern-data-grid .data-card { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 16px; + padding: 22px; + text-align: center; + transition: all 0.3s ease; +} + +.modern-data-grid .data-card:hover { + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(0,206,209,0.15); + border-color: #00CED1; + background: white; +} + +.modern-data-grid .data-icon { + font-size: 32px; + margin-bottom: 15px; + color: #00CED1; +} + +.modern-data-grid .data-card h3 { + color: #1e293b; + font-size: 16px; + margin-bottom: 10px; + font-weight: 600; +} + +.modern-data-grid .data-value { + color: #64748b; + font-size: 15px; + font-weight: 500; + line-height: 1.4; +} + +/* Modern Field Overview Styles */ +.modern-field-grid { + /* Grid removed - using default layout */ + gap: 15px; + margin-top: 25px; +} + +.modern-field-grid .field-item { + background: white; + padding: 16px 18px; + border-radius: 12px; + border-left: 4px solid #00CED1; + box-shadow: 0 4px 15px rgba(0,0,0,0.05); + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.3s ease; +} + +.modern-field-grid .field-item:hover { + transform: translateX(6px); + box-shadow: 0 8px 25px rgba(0,0,0,0.1); +} + +.modern-field-grid .field-label { + color: #1e293b; + font-weight: 600; + font-size: 13px; +} + +.modern-field-grid .field-value { + color: #64748b; + font-weight: 500; + font-size: 13px; + text-align: right; +} + +/* PDF Generation Progress Spinner */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.pdf-progress-indicator { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Modern Home Template Preview Styles */ +.template-modern-home { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: none; + position: relative; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + min-height: 280px; +} + +.template-modern-home::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0.01) 100%); + pointer-events: none; +} + +.modern-home-preview { + position: relative; + z-index: 1; + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-hero-section { + height: 65%; + background: linear-gradient(135deg, #000000 0%, #333333 100%); + position: relative; + border-radius: 12px 12px 0 0; + overflow: hidden; +} + +.preview-hero-image { + width: 100%; + height: 100%; + background-image: url('https://images.unsplash.com/photo-1568605114967-8130f3a36994?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800'); + background-size: cover; + background-position: center; + opacity: 0.8; +} + +.preview-hero-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.1) 100%); + padding: 20px; + color: white; +} + +.preview-property-name { + font-size: 18px; + font-weight: 700; + margin-bottom: 5px; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.preview-property-address { + font-size: 12px; + opacity: 0.9; + margin-bottom: 8px; +} + +.preview-price { + font-size: 16px; + font-weight: 700; + color: #ffffff; +} + +.preview-content-section { + padding: 20px; + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.preview-stats { + display: flex; + justify-content: space-around; + margin-bottom: 15px; +} + +.preview-stat { + font-size: 11px; + color: #000000; + font-weight: 600; + text-align: center; +} + +.preview-description { + flex-grow: 1; +} + +.preview-title { + font-size: 14px; + font-weight: 700; + color: #000000; + margin-bottom: 8px; +} + +.preview-text { + font-size: 11px; + color: #666666; + line-height: 1.4; +} + +/* The Grand Oak Villa Template - Remove border and increase height */ +.template-asgar1 { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: none; + position: relative; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + min-height: 320px; +} + +.template-asgar1::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0.01) 100%); + pointer-events: none; +} + +.asgar1-preview { + position: relative; + z-index: 1; + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-hero { + height: 50%; + background: linear-gradient(135deg, #000000 0%, #333333 100%); + position: relative; + border-radius: 12px 12px 0 0; + overflow: hidden; +} + +.preview-hero-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.1) 100%); + padding: 20px; + color: white; +} + +.preview-property-name { + font-size: 18px; + font-weight: 700; + margin-bottom: 5px; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.preview-property-address { + font-size: 12px; + opacity: 0.9; +} + +.preview-content { + padding: 20px; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 15px; +} + +.preview-section { + background: rgba(255,255,255,0.8); + padding: 15px; + border-radius: 8px; + border-left: 3px solid #f39c12; +} + +.preview-section-title { + font-size: 14px; + font-weight: 700; + color: #000000; + margin-bottom: 8px; +} + +.preview-text { + font-size: 11px; + color: #666666; + line-height: 1.4; +} + +.template-preview { + margin-top: auto; + padding: 15px 20px; +} + +.preview-features { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.feature-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.feature-icon { + font-size: 16px; +} + +.feature-text { + font-size: 10px; + font-weight: 600; + color: #000000; + text-align: center; +} + +/* The Serenity House Template - Remove border and increase height */ +.template-sample { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: none; + position: relative; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + min-height: 280px; +} + +.template-sample::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0.01) 100%); + pointer-events: none; +} + +.sample-preview { + position: relative; + z-index: 1; + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-header { + height: 50%; + background: linear-gradient(135deg, #000000 0%, #333333 100%); + position: relative; + border-radius: 12px 12px 0 0; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + color: white; +} + +.preview-triangle { + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 20px solid #f39c12; + margin-bottom: 15px; +} + +.preview-company-name { + font-size: 16px; + font-weight: 700; + margin: 2px 0; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.preview-title { + font-size: 14px; + font-weight: 600; + margin: 8px 0 5px 0; + opacity: 0.9; +} + +.preview-for-sale { + font-size: 12px; + opacity: 0.8; + background: rgba(243, 156, 18, 0.2); + padding: 5px 10px; + border-radius: 15px; +} + +.preview-content { + padding: 20px; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 15px; +} + +.preview-section { + background: rgba(255,255,255,0.8); + padding: 15px; + border-radius: 8px; + border-left: 3px solid #f39c12; +} + +.preview-section-title { + font-size: 14px; + font-weight: 700; + color: #000000; + margin-bottom: 8px; +} + +.preview-text { + font-size: 11px; + color: #666666; + line-height: 1.4; + margin-bottom: 10px; +} + +.preview-icons { + display: flex; + justify-content: center; + gap: 15px; +} + +.preview-icon { + font-size: 18px; +} + +/* The Vertice Template - Remove border and increase height */ +.template-luxury { + background: white; + border: none; + position: relative; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + min-height: 320px; +} + +.template-luxury::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(0,0,0,0.02) 0%, rgba(0,0,0,0.01) 100%); + pointer-events: none; +} + +.luxury-preview { + position: relative; + z-index: 1; + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-luxury-header { + height: 45%; + background: linear-gradient(135deg, #000000 0%, #333333 100%); + position: relative; + border-radius: 12px 12px 0 0; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + color: white; +} + +.preview-luxury-logo { + font-size: 18px; + font-weight: 700; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); +} + +.preview-luxury-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 5px; + opacity: 0.9; +} + +.preview-luxury-subtitle { + font-size: 12px; + opacity: 0.8; + background: rgba(243, 156, 18, 0.2); + padding: 5px 10px; + border-radius: 15px; +} + +.preview-luxury-content { + padding: 20px; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 15px; +} + +.preview-luxury-features { + display: flex; + flex-direction: column; + gap: 8px; +} + +.preview-feature { + background: rgba(255,255,255,0.8); + padding: 10px 15px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + color: #000000; + border-left: 3px solid #f39c12; +} + +.preview-luxury-description { + background: rgba(255,255,255,0.8); + padding: 15px; + border-radius: 8px; + border-left: 3px solid #f39c12; +} + +.preview-text { + font-size: 11px; + color: #666666; + line-height: 1.4; + margin-bottom: 5px; +} + +.preview-text:last-child { + margin-bottom: 0; +} + +.template-preview { + margin-top: auto; + padding: 15px 20px; +} + +.preview-features { + display: flex; + justify-content: space-around; + gap: 10px; +} + +.feature-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.feature-icon { + font-size: 16px; +} + +.feature-text { + font-size: 10px; + font-weight: 600; + color: #000000; + text-align: center; +} + +/* Table Dialog Styles */ +.table-dialog-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.table-dialog-modal { + background: white; + border-radius: 20px; + width: 90%; + max-width: 500px; + max-height: 85vh; + overflow: hidden; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.3), + 0 8px 32px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); + animation: slideUp 0.3s ease; +} + +.table-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 2rem; + border-bottom: 1px solid #e5e7eb; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border-radius: 20px 20px 0 0; +} + +.table-dialog-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + font-family: 'Inter Variable', 'Inter', sans-serif; +} + +.table-dialog-content { + padding: 2rem; +} + +.table-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; + padding: 1.5rem 2rem; + border-top: 1px solid #e5e7eb; + background: #f9fafb; +} + +.table-options { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.table-input { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #d1d5db; + border-radius: 8px; + font-size: 1rem; + font-family: 'Inter Variable', 'Inter', sans-serif; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.table-input:focus { + outline: none; + border-color: #4f46e5; + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +/* Table Styles in Editor */ +.inserted-table { + border-collapse: collapse; + width: 100%; + margin: 1rem 0; + border: 1px solid #ddd; + font-family: 'Inter Variable', 'Inter', sans-serif; + font-size: 0.9rem; +} + +.inserted-table th, +.inserted-table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.inserted-table th { + background-color: #f2f2f2; + font-weight: 600; +} + +.inserted-table tr:nth-child(even) { + background-color: #f9f9f9; +} + +.inserted-table tr:hover { + background-color: #f0f4ff; +} + +/* Mobile Responsive for Table Dialog */ +@media (max-width: 768px) { + .table-dialog-modal { + width: 95%; + max-height: 90vh; + } + + .table-dialog-header, + .table-dialog-content, + .table-dialog-actions { + padding: 1rem; + } + + .table-options { + gap: 1rem; + } +} + +/* Draggable Table Button Styles */ +.draggable-table-btn { + position: relative; + transition: all 0.2s ease; +} + +.draggable-table-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); +} + +.draggable-table-btn:active { + cursor: grabbing; + transform: scale(0.95); +} + +.draggable-table-btn.dragging { + opacity: 0.5; + transform: rotate(5deg); +} + +/* Editor Drag Over Styles */ +.enhanced-editor-content.drag-over { + background: linear-gradient(45deg, #f0f4ff 25%, transparent 25%), + linear-gradient(-45deg, #f0f4ff 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #f0f4ff 75%), + linear-gradient(-45deg, transparent 75%, #f0f4ff 75%); + background-size: 20px 20px; + background-position: 0 0, 0 10px, 10px -10px, -10px 0px; + border: 2px dashed #4f46e5; + border-radius: 8px; +} + +/* Table Drop Indicator */ +.table-drop-indicator { + position: absolute; + border: 2px dashed #4f46e5; + background: rgba(79, 70, 229, 0.1); + border-radius: 4px; + pointer-events: none; + z-index: 1000; + display: none; +} + +.table-drop-indicator.show { + display: block; +} + +/* Editable Table Container Styles */ +.editable-table-container { + position: relative; + margin: 1rem 0; + border: 2px solid transparent; + border-radius: 8px; + transition: all 0.2s ease; +} + +.editable-table-container:hover { + border-color: #4f46e5; + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.15); +} + +.editable-table-container.dragging { + opacity: 0.5; + transform: rotate(2deg); + z-index: 1000; +} + +/* Table Controls */ +.table-controls { + position: absolute; + top: -40px; + left: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 0.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + display: flex; + gap: 0.5rem; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 100; +} + +.editable-table-container:hover .table-controls { + opacity: 1; +} + +.table-control-group { + display: flex; + gap: 0.25rem; + align-items: center; +} + +.table-control-group:not(:last-child) { + border-right: 1px solid #e2e8f0; + padding-right: 0.5rem; + margin-right: 0.5rem; +} + +.table-control-btn { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + color: #475569; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + user-select: none; +} + +.table-control-btn:hover { + background: #e2e8f0; + border-color: #cbd5e1; + color: #334155; + transform: translateY(-1px); +} + +.table-control-btn:active { + transform: translateY(0); + background: #cbd5e1; +} + +.table-control-btn.delete { + background: #fef2f2; + border-color: #fecaca; + color: #dc2626; +} + +.table-control-btn.delete:hover { + background: #fee2e2; + border-color: #fca5a5; + color: #b91c1c; +} + +.table-control-btn.delete:active { + background: #fecaca; +} + +/* Table Container Drag Styles */ +.editable-table-container[draggable="true"] { + cursor: move; +} + +.editable-table-container[draggable="true"]:hover { + cursor: grab; +} + +.editable-table-container[draggable="true"]:active { + cursor: grabbing; +} + +/* Prevent text selection on controls */ +.table-controls * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +/* Responsive table controls */ +@media (max-width: 768px) { + .table-controls { + position: relative; + top: 0; + left: 0; + opacity: 1; + margin-bottom: 0.5rem; + flex-wrap: wrap; + } + + .table-control-btn { + font-size: 0.6875rem; + padding: 0.25rem 0.375rem; + } + + .table-control-group:not(:last-child) { + border-right: none; + padding-right: 0; + margin-right: 0; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; + } +} + +/* Draggable and resizable image styles */ +img[draggable="true"] { + position: relative; + z-index: 1000; + cursor: move; +} + +.resize-handle { + position: absolute; + width: 8px; + height: 8px; + background: #007bff; + border: 1px solid white; + z-index: 1001; +} + +.resize-handle.resize-nw { + top: -4px; + left: -4px; + cursor: nw-resize; +} + +.resize-handle.resize-ne { + top: -4px; + right: -4px; + cursor: ne-resize; +} + +.resize-handle.resize-sw { + bottom: -4px; + left: -4px; + cursor: sw-resize; +} + +.resize-handle.resize-se { + bottom: -4px; + right: -4px; + cursor: se-resize; +} + +/* Selector mode styles */ +.enhanced-editor-content.selector-mode { + cursor: crosshair; +} + +.selector-options-panel { + position: fixed; + top: 10px; + right: 10px; + background: white; + border: 2px solid #007bff; + border-radius: 8px; + padding: 15px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10000; + min-width: 200px; + max-width: 250px; +} + +.property-image-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border: 2px solid #007bff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + max-width: 400px; + max-height: 500px; + overflow-y: auto; +} + +/* Bullet and numbering styles */ +ul, ol { + list-style-type: none; + padding-left: 0; +} + +ul li, ol li { + margin-left: 20px; + position: relative; +} + +ul li:before { + content: "* "; + position: absolute; + left: -20px; +} + +ol li:before { + content: "1. "; + position: absolute; + left: -20px; +} + +/* Image Insertion Modal Styles */ +.image-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + backdrop-filter: blur(5px); +} + +.image-modal { + background: white; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 800px; + max-height: 80vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.image-modal-header { + padding: 20px 24px; + border-bottom: 1px solid #e5e7eb; + display: flex; + justify-content: space-between; + align-items: center; + background: #f9fafb; +} + +.image-modal-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #111827; +} + +.image-modal-content { + flex: 1; + padding: 24px; + overflow-y: auto; +} + +.image-source-tabs { + display: flex; + gap: 8px; + margin-bottom: 24px; + border-bottom: 1px solid #e5e7eb; +} + +.tab-btn { + padding: 12px 20px; + border: none; + background: transparent; + color: #6b7280; + font-weight: 500; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s ease; +} + +.tab-btn.active { + color: #4f46e5; + border-bottom-color: #4f46e5; + background: #f0f4ff; +} + +.tab-btn:hover { + color: #4f46e5; + background: #f9fafb; +} + +.property-images-section { + display: flex; + flex-direction: column; + gap: 20px; +} + +.image-categories { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.category-btn { + padding: 8px 16px; + border: 1px solid #d1d5db; + background: white; + color: #374151; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.category-btn.active { + background: #4f46e5; + color: white; + border-color: #4f46e5; +} + +.category-btn:hover { + background: #f3f4f6; + border-color: #9ca3af; +} + +.property-images-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 16px; + max-height: 300px; + overflow-y: auto; +} + +.property-image-item { + position: relative; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + cursor: pointer; + border: 2px solid transparent; + transition: all 0.2s ease; +} + +.property-image-item:hover { + border-color: #4f46e5; + transform: scale(1.02); +} + +.property-image-item.selected { + border-color: #4f46e5; + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +.property-image-item img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); + color: white; + padding: 8px; + font-size: 12px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.property-image-item:hover .image-overlay { + opacity: 1; +} + +.local-upload-section { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} + +.upload-area { + border: 2px dashed #d1d5db; + border-radius: 12px; + padding: 40px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + background: #f9fafb; + width: 100%; + max-width: 400px; +} + +.upload-area:hover { + border-color: #4f46e5; + background: #f0f4ff; +} + +.upload-area.dragover { + border-color: #4f46e5; + background: #e0e7ff; +} + +.file-input { + display: none; +} + +.upload-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.upload-content p { + margin: 0; + font-weight: 500; + color: #374151; +} + +.upload-content small { + color: #6b7280; +} + +.image-modal-actions { + padding: 20px 24px; + border-top: 1px solid #e5e7eb; + display: flex; + justify-content: flex-end; + gap: 12px; + background: #f9fafb; +} + +/* Enhanced Draggable Image Styles */ +.draggable-image-container { + position: absolute; + border: 2px solid transparent; + cursor: move; + z-index: 1000; + min-width: 50px; + min-height: 50px; + user-select: none; +} + +.draggable-image-container.selected { + border-color: #4f46e5; + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +.draggable-image-container img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.resize-handle { + position: absolute; + background: #4f46e5; + border: 2px solid white; + border-radius: 50%; + width: 12px; + height: 12px; + z-index: 1001; +} + +.resize-handle.nw { top: -6px; left: -6px; cursor: nw-resize; } +.resize-handle.ne { top: -6px; right: -6px; cursor: ne-resize; } +.resize-handle.sw { bottom: -6px; left: -6px; cursor: sw-resize; } +.resize-handle.se { bottom: -6px; right: -6px; cursor: se-resize; } +.resize-handle.n { top: -6px; left: 50%; transform: translateX(-50%); cursor: n-resize; } +.resize-handle.s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: s-resize; } +.resize-handle.w { top: 50%; left: -6px; transform: translateY(-50%); cursor: w-resize; } +.resize-handle.e { top: 50%; right: -6px; transform: translateY(-50%); cursor: e-resize; } + +.delete-handle { + position: absolute; + top: -8px; + right: -8px; + background: #ef4444; + color: white; + border: none; + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 12px; + cursor: pointer; + z-index: 1002; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.delete-handle:hover { + background: #dc2626; + transform: scale(1.1); +} + +/* Enhanced Draggable Table Styles */ +.draggable-table-container { + position: absolute; + border: 2px solid transparent; + cursor: move; + z-index: 1000; + min-width: 200px; + min-height: 150px; + user-select: none; + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; +} + +.draggable-table-container.selected { + border-color: #4f46e5; + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +.draggable-table-container table { + width: 100%; + height: 100%; + border-collapse: collapse; + margin: 0; + background: white; +} + +.draggable-table-container th, +.draggable-table-container td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} + +.draggable-table-container th { + background: #f8f9fa; + font-weight: 600; +} + +.draggable-table-container td { + background: white; +} + +.draggable-table-container td:focus { + outline: 2px solid #4f46e5; + outline-offset: -2px; +} + +.table-controls-overlay { + position: absolute; + top: -40px; + left: 0; + background: white; + padding: 8px; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.2s ease; + display: flex; + gap: 4px; + z-index: 1002; +} + +.table-controls-overlay button { + padding: 4px 8px; + font-size: 12px; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.table-controls-overlay button:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + + diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js index 3c5b432..46e00bc 100644 --- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js @@ -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); + // Handle different status types + if (pdfResult.status === 'download_ready' || pdfResult.status === 'compressed_download_ready') { + await this.handlePDFDownloadReady(pdfResult); + } else { + throw new Error('Unexpected PDF status: ' + pdfResult.status); + } - 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 += '
💡 If download didn\'t start, right-click the PDF link below and select "Save as..."'; - - // 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); } else { - throw new Error('PDF generation returned empty result'); + // 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 = ` +
+

📄 PDF Generated Successfully!

+

Size: ${responseData.size_mb} MB

+

This PDF is too large for direct download due to Salesforce limits.

+ +
+ + + +
+ +
+

💡 Why is this happening?

+
    +
  • Your template contains high-quality images
  • +
  • Salesforce has a 6MB response limit
  • +
  • The compressed version will reduce image quality but keep all content
  • +
+
+
+ `; + + 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 = ` +
+

📦 Compressed PDF Ready!

+

PDF ID: ${pdfResult.pdf_id}

+

Page Size: ${pdfResult.page_size}

+

Generated: ${pdfResult.generated_at}

+

This is a compressed version optimized for smaller file size.

+ +
+ +
+ +
+

💡 About Compressed PDFs

+
    +
  • Images are optimized for smaller file size
  • +
  • All content is preserved but with reduced quality
  • +
  • Perfect for email sharing and quick downloads
  • +
+
+
+ `; + } else { + message = ` +
+

📄 PDF Ready for Download!

+

PDF ID: ${pdfResult.pdf_id}

+

Page Size: ${pdfResult.page_size}

+

Generated: ${pdfResult.generated_at}

+

Your PDF has been generated successfully and is ready for download.

+ +
+ +
+ +
+

✅ Download Instructions

+
    +
  • Click the download button above
  • +
  • Your PDF will open in a new tab
  • +
  • Use your browser's save function to download
  • +
+
+
+ `; + } + + 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 ==='); diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js.backup b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js.backup index 2c96f18..0e38098 100644 --- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js.backup +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js.backup @@ -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 { @@ -38,13 +39,14 @@ export default class PropertyTemplateSelector extends LightningElement { // Image review properties @track showImageReview = false; - @track selectedCategory = 'Interior'; + @track selectedCategory = 'Interior'; // Will be updated when images load @track currentImageIndex = 0; @track totalImages = 0; @track currentImage = null; // Real property images from Salesforce @track realPropertyImages = []; + @track propertyImages = []; @track imagesByCategory = { 'Interior': [ { url: 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200', title: 'Interior View 1', category: 'Interior' }, @@ -85,10 +87,23 @@ export default class PropertyTemplateSelector extends LightningElement { @track showImageReplacement = false; @track selectedImageElement = null; @track replacementActiveTab = 'property'; // 'property' or 'upload' - @track replacementSelectedCategory = 'Interior'; + @track replacementSelectedCategory = 'Interior'; // Will be updated when images load @track filteredReplacementImages = []; @track uploadedImagePreview = null; + // Triple click detection for image replacement + @track imageClickCount = 0; + @track lastClickedImage = null; + @track clickTimeout = null; + + // Undo/Redo functionality + @track undoStack = []; + @track redoStack = []; + @track maxUndoSteps = 20; + + // Category selection tracking + @track initialCategorySelected = false; + // Template Save/Load Variables @track showSaveDialog = false; @track showLoadDialog = false; @@ -102,6 +117,22 @@ export default class PropertyTemplateSelector extends LightningElement { @track tableRows = 3; @track tableCols = 3; @track includeHeader = true; + + // Image insertion modal properties + @track showImageModal = false; + @track imageSource = 'property'; // 'property' or 'local' + @track selectedImageCategory = 'all'; + @track selectedImageUrl = ''; + @track selectedImageName = ''; + @track uploadedImageData = ''; + @track renderKey = 0; // For forcing re-renders + @track insertButtonDisabled = true; // Explicit button state + + // Table Drag and Drop Variables + @track isDraggingTable = false; + @track draggedTableData = null; + @track selectorMode = false; + @track selectedElement = null; // Undo functionality @track undoStack = []; @@ -125,6 +156,76 @@ export default class PropertyTemplateSelector extends LightningElement { return this.replacementActiveTab === 'upload'; } + // Image insertion modal getters + get isImageModalOpen() { + return this.showImageModal; + } + + get imageCategories() { + const categories = [ + { label: 'All Images', value: 'all' }, + { label: 'Exterior', value: 'exterior' }, + { label: 'Interior', value: 'interior' }, + { label: 'Kitchen', value: 'kitchen' }, + { label: 'Bedroom', value: 'bedroom' }, + { label: 'Bathroom', value: 'bathroom' }, + { label: 'Living Room', value: 'living' }, + { label: 'Maps', value: 'maps' }, + { label: 'None', value: 'none' } + ]; + return categories; + } + + get filteredPropertyImages() { + console.log('filteredPropertyImages called'); + console.log('propertyImages:', this.propertyImages); + console.log('selectedImageCategory:', this.selectedImageCategory); + + if (!this.propertyImages || this.propertyImages.length === 0) { + console.log('No property images available'); + return []; + } + + if (this.selectedImageCategory === 'all') { + console.log('Returning all images:', this.propertyImages); + return this.propertyImages; + } + + const filtered = this.propertyImages.filter(image => { + const category = image.category ? image.category.toLowerCase() : 'none'; + return category === this.selectedImageCategory; + }); + + console.log('Filtered images:', filtered); + return filtered; + } + + get propertyTabClass() { + return this.imageSource === 'property' ? 'tab-btn active' : 'tab-btn'; + } + + get localTabClass() { + return this.imageSource === 'local' ? 'tab-btn active' : 'tab-btn'; + } + + getCategoryButtonClass(categoryValue) { + return this.selectedImageCategory === categoryValue ? 'category-btn active' : 'category-btn'; + } + + get showPropertyImagesSection() { + return this.imageSource === 'property'; + } + + get showLocalUploadSection() { + return this.imageSource === 'local'; + } + + get isInsertButtonDisabled() { + const disabled = !this.selectedImageUrl || this.selectedImageUrl === ''; + console.log('isInsertButtonDisabled check:', disabled, 'selectedImageUrl:', this.selectedImageUrl, 'renderKey:', this.renderKey); + return disabled; + } + // Computed properties for template selection get isBlankTemplateSelected() { return this.selectedTemplateId === 'blank-template'; @@ -480,6 +581,8 @@ export default class PropertyTemplateSelector extends LightningElement { nextStep() { if (this.currentStep < 3) { this.currentStep++; + // Reset click tracking when changing steps + this.resetImageClickTracking(); // If moving to step 3, automatically load the template if (this.currentStep === 3) { this.loadTemplateInStep3(); @@ -491,6 +594,8 @@ export default class PropertyTemplateSelector extends LightningElement { previousStep() { if (this.currentStep > 1) { this.currentStep--; + // Reset click tracking when changing steps + this.resetImageClickTracking(); this.scrollToTop(); } } @@ -498,6 +603,8 @@ export default class PropertyTemplateSelector extends LightningElement { goToStep(event) { const step = parseInt(event.currentTarget.dataset.step); this.currentStep = step; + // Reset click tracking when changing steps + this.resetImageClickTracking(); // If going directly to step 3, load the template if (this.currentStep === 3) { this.loadTemplateInStep3(); @@ -514,15 +621,31 @@ export default class PropertyTemplateSelector extends LightningElement { } // Load template content into step 3 enhanced editor - loadTemplateInStep3() { + async loadTemplateInStep3() { if (this.selectedTemplateId && this.selectedPropertyId) { try { + console.log('=== LOADING TEMPLATE IN STEP 3 ==='); + console.log('Template ID:', this.selectedTemplateId); + console.log('Property ID:', this.selectedPropertyId); + console.log('Property data available:', !!this.propertyData); + console.log('Real property images available:', this.realPropertyImages?.length || 0); + + // Ensure property images are loaded before creating template + if (this.realPropertyImages.length === 0) { + console.log('No property images loaded, loading them now...'); + await this.loadPropertyImages(); + } + const templateHTML = this.createTemplateHTML(); - console.log('Loading template into step 3:', this.selectedTemplateId); console.log('Template HTML length:', templateHTML.length); + console.log('Template HTML preview:', templateHTML.substring(0, 500) + '...'); + + // Replace any hardcoded background-image URLs with property images + const processedTemplateHTML = this.replaceBackgroundImagesInHTML(templateHTML); + console.log('Processed template HTML length:', processedTemplateHTML.length); // Set the HTML content for the template binding - this.htmlContent = templateHTML; + this.htmlContent = processedTemplateHTML; // Also find the enhanced editor content and load the template const editorContent = this.template.querySelector('.enhanced-editor-content'); @@ -532,7 +655,7 @@ export default class PropertyTemplateSelector extends LightningElement { // Create a temporary container to parse the HTML const tempDiv = document.createElement('div'); - tempDiv.innerHTML = templateHTML; + tempDiv.innerHTML = processedTemplateHTML; // Append the parsed content to the editor while (tempDiv.firstChild) { @@ -541,80 +664,22 @@ export default class PropertyTemplateSelector extends LightningElement { console.log('Template loaded successfully into enhanced editor'); - // Add edit icons to ALL images (simple solution) + // Update any remaining CSS background-image rules setTimeout(() => { - const previewFrame = this.template.querySelector('.enhanced-editor-content'); - if (previewFrame) { - const images = previewFrame.querySelectorAll('img'); - console.log(`Found ${images.length} images to add edit icons to`); - - images.forEach((img, index) => { - // Skip if already has edit button - if (img.closest('.template-image-container')) { - console.log(`Image ${index + 1} already has edit button, skipping`); - return; - } - - console.log(`Adding edit button to image ${index + 1}:`, img.src); - - // Create edit button - const editBtn = document.createElement('button'); - editBtn.className = 'template-image-edit-btn'; - editBtn.innerHTML = '✏️'; - editBtn.title = 'Edit Image'; - editBtn.style.position = 'absolute'; - editBtn.style.top = '5px'; - editBtn.style.right = '5px'; - editBtn.style.background = 'rgba(0, 123, 255, 0.9)'; - editBtn.style.border = 'none'; - editBtn.style.borderRadius = '50%'; - editBtn.style.width = '30px'; - editBtn.style.height = '30px'; - editBtn.style.cursor = 'pointer'; - editBtn.style.fontSize = '14px'; - editBtn.style.zIndex = '1001'; - editBtn.style.display = 'flex'; - editBtn.style.alignItems = 'center'; - editBtn.style.justifyContent = 'center'; - editBtn.style.transition = 'all 0.3s ease'; - editBtn.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.2)'; - - // Create container - const container = document.createElement('div'); - container.className = 'template-image-container'; - container.style.position = 'relative'; - container.style.display = 'inline-block'; - container.style.width = '100%'; - container.style.height = '100%'; - - // Replace image with container - img.parentNode.insertBefore(container, img); - container.appendChild(img); - container.appendChild(editBtn); - - // Add hover effects - editBtn.addEventListener('mouseenter', () => { - editBtn.style.background = 'rgba(0, 123, 255, 1)'; - editBtn.style.transform = 'scale(1.1)'; - }); - - editBtn.addEventListener('mouseleave', () => { - editBtn.style.background = 'rgba(0, 123, 255, 0.9)'; - editBtn.style.transform = 'scale(1)'; - }); - - // Add click handler with custom popup - editBtn.addEventListener('click', (e) => { - e.stopPropagation(); - this.showCustomImageEditModal(img); - }); - - console.log(`Successfully added edit button to image ${index + 1}`); - }); - - console.log(`Successfully processed ${images.length} images`); - } - }, 1000); + this.updateCSSBackgroundImages(); + }, 200); + + // Make images draggable and resizable + setTimeout(() => { + this.makeImagesDraggableAndResizable(); + }, 300); + + // Save initial state for undo functionality + setTimeout(() => { + this.saveUndoState(); + }, 100); + + // Images are now displayed without edit icons for clean presentation // Set initial page size class and data attribute this.updatePreviewFrameSize(this.selectedPageSize); @@ -839,6 +904,7 @@ export default class PropertyTemplateSelector extends LightningElement { async loadPropertyImages() { if (!this.selectedPropertyId) { this.realPropertyImages = []; + this.initialCategorySelected = false; // Reset flag when no property selected return; } @@ -849,15 +915,35 @@ export default class PropertyTemplateSelector extends LightningElement { const images = await getPropertyImages({ propertyId: this.selectedPropertyId }); console.log('Loaded images:', images); - // Group images by category - this.realPropertyImages = images; + // Transform the data to match expected format + this.realPropertyImages = images.map(img => ({ + id: img.id, + name: img.name, + title: img.name, + category: img.category, + url: img.url + })); console.log('Real property images loaded:', this.realPropertyImages); if (this.realPropertyImages && this.realPropertyImages.length > 0) { - const firstCategory = this.realPropertyImages[0].category; - console.log('Auto-selecting first available category:', firstCategory); - this.selectCategory(firstCategory); + // Find the first category that has images + const firstAvailableCategory = this.findFirstAvailableCategory(); + console.log('Loading images, auto-selecting first available category:', firstAvailableCategory); + this.filterImagesByCategory(firstAvailableCategory); + this.selectedCategory = firstAvailableCategory; + this.initialCategorySelected = true; + + // Update active button visually + setTimeout(() => { + const categoryButtons = this.template.querySelectorAll('.category-btn-step2'); + categoryButtons.forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.category === firstAvailableCategory) { + btn.classList.add('active'); + } + }); + }, 100); } } catch (error) { @@ -883,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; } @@ -965,7 +1065,20 @@ export default class PropertyTemplateSelector extends LightningElement { } catch (error) { console.error('=== PDF GENERATION ERROR ==='); console.error('Error generating PDF:', error); - this.showError('PDF generation failed: ' + error.message); + + // Provide more user-friendly error messages + let errorMessage = 'PDF generation failed. '; + if (error.message && error.message.includes('timeout')) { + errorMessage += 'The service is taking longer than expected. Please try again.'; + } else if (error.message && error.message.includes('unavailable')) { + errorMessage += 'The service is temporarily unavailable. Please try again in a few minutes.'; + } else if (error.message && error.message.includes('connection')) { + errorMessage += 'Please check your internet connection and try again.'; + } else { + errorMessage += error.message || 'Please try again.'; + } + + this.showError(errorMessage); } finally { // Reset loading state this.isGeneratingPdf = false; @@ -1061,72 +1174,67 @@ export default class PropertyTemplateSelector extends LightningElement { console.log('Final HTML content to send:', htmlContent); console.log('Final HTML content length:', htmlContent.length, 'characters'); - // Update progress message - this.showProgress('Wait, our AI is generating report...'); + // Update progress message with timeout information + this.showProgress('Wait, our AI is generating report... (This may take up to 2 minutes)'); + + // Start progress timer + const startTime = Date.now(); + const progressInterval = setInterval(() => { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const minutes = Math.floor(elapsed / 60); + const seconds = elapsed % 60; + this.showProgress(`Generating PDF... (${minutes}:${seconds.toString().padStart(2, '0')} elapsed)`); + }, 1000); // Call the Apex method with the complete HTML and page size - const base64PDF = await generatePDFFromHTML({ + // Set timeout to 2 minutes (120000ms) for API response + const pdfResult = await Promise.race([ + generatePDFFromHTML({ htmlContent: htmlContent, pageSize: this.selectedPageSize + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('PDF generation timeout - service took too long to respond')), 120000) + ) + ]).catch(error => { + // Clear progress timer + clearInterval(progressInterval); + console.error('Apex method error:', error); + console.error('Error details:', error.body?.message || error.message); + + // Provide more specific error messages + if (error.message && error.message.includes('timeout')) { + throw new Error('PDF generation timed out. The service is taking longer than expected. Please try again.'); + } else if (error.message && error.message.includes('unavailable')) { + throw new Error('PDF generation service is temporarily unavailable. Please try again in a few minutes.'); + } else if (error.body && error.body.message) { + throw new Error(`PDF generation failed: ${error.body.message}`); + } else { + throw new Error('PDF generation failed. Please check your internet connection and try again.'); + } }); - if (base64PDF) { - console.log('PDF generated successfully via Apex proxy'); - console.log('PDF base64 length:', base64PDF.length, 'characters'); + // Clear progress timer on success + clearInterval(progressInterval); + + // 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); + // Handle different status types + if (pdfResult.status === 'download_ready' || pdfResult.status === 'compressed_download_ready') { + await this.handlePDFDownloadReady(pdfResult); + } else { + throw new Error('Unexpected PDF status: ' + pdfResult.status); + } - 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 += '
💡 If download didn\'t start, right-click the PDF link below and select "Save as..."'; - - // 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); } else { - throw new Error('PDF generation returned empty result'); + // 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 ==='); @@ -1341,11 +1449,115 @@ 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 = ` +
+

📄 PDF Generated Successfully!

+

Size: ${responseData.size_mb} MB

+

This PDF is too large for direct download due to Salesforce limits.

+ +
+ + + +
+ +
+

💡 Why is this happening?

+
    +
  • Your template contains high-quality images
  • +
  • Salesforce has a 6MB response limit
  • +
  • The compressed version will reduce image quality but keep all content
  • +
+
+
+ `; + + 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); + } + } + // Create template HTML based on selection createTemplateHTML() { console.log('=== CREATE TEMPLATE HTML DEBUG ==='); console.log('this.selectedTemplateId:', this.selectedTemplateId); console.log('typeof this.selectedTemplateId:', typeof this.selectedTemplateId); + console.log('this.propertyData:', this.propertyData); + console.log('this.realPropertyImages:', this.realPropertyImages); switch (this.selectedTemplateId) { case 'blank-template': @@ -1391,36 +1603,86 @@ export default class PropertyTemplateSelector extends LightningElement { } } + // Generate amenities HTML from property data + generateAmenitiesHTML(data) { + const amenities = []; + + // Check for common amenity fields in the property data + const amenityFields = [ + 'amenities', 'features', 'facilities', 'amenitiesList', 'propertyAmenities', + 'Amenities__c', 'Features__c', 'Facilities__c', 'Property_Amenities__c', + // Add the actual fields that are available in propertyData + 'parkingSpaces', 'furnished', 'offeringType' + ]; + + // Try to find amenities in various field formats + for (const field of amenityFields) { + if (data[field] && data[field] !== 'N/A') { + if (Array.isArray(data[field])) { + amenities.push(...data[field]); + } else if (typeof data[field] === 'string') { + // For specific fields, format them properly + if (field === 'parkingSpaces') { + amenities.push(`Parking: ${data[field]} spaces`); + } else if (field === 'furnished') { + amenities.push(`Furnished: ${data[field]}`); + } else if (field === 'offeringType') { + amenities.push(`Offering Type: ${data[field]}`); + } else { + // Split by common delimiters for other fields + const amenityList = data[field].split(/[,;|\n]/).map(a => a.trim()).filter(a => a); + amenities.push(...amenityList); + } + } + } + } + + // If no amenities found, return empty string + if (amenities.length === 0) { + return '
No amenities specified
'; + } + + // Generate HTML for amenities + return amenities.map(amenity => + `
${amenity}
` + ).join(''); + } + // Template methods createBlankTemplate() { const data = this.propertyData || {}; - const propertyName = data.propertyName || "Property Name"; - const location = data.location || "Location"; - const price = data.price || "Price"; - const bedrooms = data.bedrooms || "N/A"; - const bathrooms = data.bathrooms || "N/A"; - const size = data.size || "N/A"; + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const price = data.Price__c || data.price || "Price"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; const sizeUnit = data.sizeUnit || "sq ft"; - const status = data.status || "Available"; - const propertyType = data.propertyType || "Property Type"; - const city = data.city || "City"; - const community = data.community || "Community"; - const subCommunity = data.subCommunity || "Sub Community"; - const furnished = data.furnished || "N/A"; - const parkingSpaces = data.parkingSpaces || "N/A"; - const buildYear = data.buildYear || "N/A"; - const titleEnglish = data.titleEnglish || "Property Title"; - const descriptionEnglish = data.descriptionEnglish || "Property Description"; - const rentPriceMin = data.rentPriceMin || "N/A"; - const salePriceMin = data.salePriceMin || "N/A"; + const status = data.Status__c || data.status || "Available"; + const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; + const city = data.City__c || data.city || "City"; + const community = data.Community__c || data.community || "Community"; + const subCommunity = data.Sub_Community__c || data.subCommunity || "Sub Community"; + const furnished = data.Furnished__c || data.furnished || "N/A"; + const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A"; + const buildYear = data.Build_Year__c || data.buildYear || "N/A"; + const titleEnglish = data.Title_English__c || data.titleEnglish || "Property Title"; + const descriptionEnglish = data.Description_English__c || data.descriptionEnglish || "Property Description"; + const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; + const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; return `
-
-

${propertyName}

-

${location}

-

${price}

+
+ Property Header Image +
+
+

${propertyName}

+

${location}

+

${price}

+
@@ -1496,13 +1758,21 @@ export default class PropertyTemplateSelector extends LightningElement {
-

Description

-
- Title: ${titleEnglish} +

Property Description

+
+

${titleEnglish}

-
+
${descriptionEnglish}
+
+
+
Property Type: ${propertyType}
+
Status: ${status}
+
Furnished: ${furnished}
+
Build Year: ${buildYear}
+
+
@@ -1516,6 +1786,14 @@ export default class PropertyTemplateSelector extends LightningElement {
+ +
+

Property Gallery

+ +
+

Custom Content Area

@@ -1531,37 +1809,113 @@ export default class PropertyTemplateSelector extends LightningElement { } createShiftTemplate() { - const data = this.propertyData || {}; - const propertyName = data.propertyName || "SHIFT PROPERTY"; - const location = data.location || "Modern Living"; - const price = data.price || "Starting from $1,500,000"; - const referenceId = data.referenceNumber || "SP-2025-001"; + console.log('=== CREATE SHIFT TEMPLATE CALLED ==='); + console.log('Property data:', this.propertyData); + console.log('Real property images:', this.realPropertyImages); - return `Shift Property - Modern Living

${propertyName}

${location}

${price}

About Shift Property

Experience the future of living with Shift Property, where innovation meets comfort in perfect harmony.

Modern Features

Innovation
Smart Design
Eco-Friendly
Connected Living

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Innovation Specialist"}

Phone: ${data.agentPhone || "(555) 789-0123"}

`; + const data = this.propertyData || {}; + const propertyName = data.Name || data.propertyName || "SHIFT PROPERTY"; + const location = data.Address__c || data.location || "Modern Living"; + const price = data.Price__c || data.price || "Starting from $1,500,000"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "SP-2025-001"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; + const description = data.Description_English__c || data.descriptionEnglish || "Modern living at its finest."; + + // Get smart images - use direct method for exterior + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const kitchenImage = this.getSmartImageForSection('kitchen', 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Debug logging + console.log('=== SHIFT TEMPLATE DEBUG ==='); + console.log('Exterior image URL:', exteriorImage); + console.log('Interior image URL:', interiorImage); + console.log('Kitchen image URL:', kitchenImage); + + // Generate property gallery for uncategorized images + const propertyGallery = this.generatePropertyGalleryHTML(); + + return `Shift Property - Modern Living
Property Exterior

${propertyName}

${location}

${price}

EXTERIOR IMAGE TEST: ${exteriorImage}

About Shift Property

Experience the future of living with Shift Property, where innovation meets comfort in perfect harmony.

Interior View

Modern Features

Innovation
Smart Design
Eco-Friendly
Connected Living
Kitchen View

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Innovation Specialist"}

Phone: ${data.agentPhone || "(555) 789-0123"}

${propertyGallery}
`; } createSaintbartsTemplate() { const data = this.propertyData || {}; - const propertyName = data.propertyName || "SAINT BARTS VILLA"; - const location = data.location || "Caribbean Paradise"; - const price = data.price || "Starting from $3,200,000"; - const referenceId = data.referenceNumber || "SB-2025-001"; + const propertyName = data.Name || data.propertyName || "SAINT BARTS VILLA"; + const location = data.Address__c || data.location || "Caribbean Paradise"; + const price = data.Price__c || data.price || "Starting from $3,200,000"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "SB-2025-001"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; + const description = data.Description_English__c || data.descriptionEnglish || "Caribbean paradise awaits."; - return `Saint Barts Villa - Caribbean Paradise

${propertyName}

${location}

${price}

About Saint Barts Villa

Discover paradise at Saint Barts Villa, where tropical luxury meets Caribbean charm in an idyllic setting.

Tropical Features

Beach Access
Tropical Gardens
Ocean Views
Luxury Amenities

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Caribbean Specialist"}

Phone: ${data.agentPhone || "(555) 456-7890"}

`; + // Get smart images - use direct method for exterior + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const bedroomImage = this.getSmartImageForSection('bedroom', 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery for uncategorized images + const propertyGallery = this.generatePropertyGalleryHTML(); + + return `Saint Barts Villa - Caribbean Paradise
Property Exterior

${propertyName}

${location}

${price}

About Saint Barts Villa

Discover paradise at Saint Barts Villa, where tropical luxury meets Caribbean charm in an idyllic setting.

Interior View

Tropical Features

Beach Access
Tropical Gardens
Ocean Views
Luxury Amenities
Bedroom View

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Caribbean Specialist"}

Phone: ${data.agentPhone || "(555) 456-7890"}

${propertyGallery}
`; } createLearnoyTemplate() { const data = this.propertyData || {}; - const propertyName = data.propertyName || "LEARNOY ESTATE"; - const location = data.location || "Heritage Collection"; - const price = data.price || "Starting from $2,800,000"; - const referenceId = data.referenceNumber || "LE-2025-001"; + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const price = data.Price__c || data.price || "Price"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const description = data.Description_English__c || data.descriptionEnglish || "Property description not available."; + const referenceId = data.Reference_Number__c || data.referenceNumber || "N/A"; - return `Learnoy Estate - Heritage Collection

${propertyName}

${location}

${price}

About Learnoy Estate

Experience timeless elegance at Learnoy Estate, where heritage meets modern luxury in perfect harmony.

Heritage Features

Royal Heritage
Luxury Finishes
Historic Gardens
Historic Architecture

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Heritage Specialist"}

Phone: ${data.agentPhone || "(555) 321-6540"}

`; + // Get smart images - use direct method for exterior + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const livingImage = this.getSmartImageForSection('living', 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery for uncategorized images + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); + + return `Learnoy Estate - Heritage Collection
Property Exterior

${propertyName}

${location}

${price}

About ${propertyName}

${description}

Property Type: ${propertyType}
Bedrooms: ${bedrooms}
Bathrooms: ${bathrooms}
Size: ${size} sq ft
Status: ${data.Status__c || data.status || "N/A"}
Year Built: ${data.Build_Year__c || data.yearBuilt || "N/A"}
Furnished: ${data.Furnished__c || data.furnished || "N/A"}
Parking: ${data.Parking_Spaces__c || data.parking || "N/A"}
Interior View

Property Features

${amenitiesHTML}
Living Area View

Contact Information

Reference ID: ${data.Reference_Number__c || data.referenceNumber || "N/A"}

Agent: ${data.Agent_Name__c || data.agentName || "N/A"}

Phone: ${data.Agent_Phone__c || data.agentPhone || "N/A"}

Email: ${data.Agent_Email__c || data.agentEmail || "N/A"}

Property Gallery

`; } createLeafampTemplate() { - return `Urban Properties

Urban Properties Template

The City Living Experience

`; + const data = this.propertyData || {}; + const propertyName = data.Name || data.propertyName || "LEAFAMP URBAN"; + const location = data.Address__c || data.location || "City Living Experience"; + const price = data.Price__c || data.price || "Starting from $1,200,000"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "LF-2025-001"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; + const description = data.Description_English__c || data.descriptionEnglish || "City living experience."; + + // Get smart images - use direct method for exterior + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const bathroomImage = this.getSmartImageForSection('bathroom', 'https://images.unsplash.com/photo-1584622650111-993a426fbf0a?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const mapsImage = this.getMapsImageUrl(); + + // Debug logging for maps image + console.log('=== LEAFAMP TEMPLATE MAPS DEBUG ==='); + console.log('Maps image URL:', mapsImage); + + // Generate property gallery for uncategorized images + const propertyGallery = this.generatePropertyGalleryHTML(); + + return `Leafamp Urban - City Living Experience
Property Exterior

${propertyName}

${location}

${price}

About Leafamp Urban

Experience the pulse of city life at Leafamp Urban, where modern design meets urban convenience in the heart of the metropolis.

Interior View

Urban Features

City Views
Transit Access
Urban Amenities
Smart Living
Bathroom View

Floor Plan & Location

Discover the strategic location and layout of Leafamp Urban, perfectly positioned for modern urban living with easy access to all city amenities.

MAPS IMAGE TEST: ${mapsImage}
Floor Plan & Location Map

Contact Information

Reference ID: ${referenceId}

Agent: ${data.agentName || "Urban Specialist"}

Phone: ${data.agentPhone || "(555) 987-6543"}

${propertyGallery}
`; } createCoreshiftTemplate() { @@ -1572,100 +1926,129 @@ export default class PropertyTemplateSelector extends LightningElement { console.log("=== CREATING REAL ESTATE MODERN HOME TEMPLATE ==="); const data = this.propertyData || {}; - const propertyName = data.propertyName || "Property Name"; - const propertyType = data.propertyType || "Property Type"; - const location = data.location || "Location"; - const price = data.price || "Price on Request"; - const bedrooms = data.bedrooms || "N/A"; - const bathrooms = data.bathrooms || "N/A"; - const area = data.area || "N/A"; - const description = data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."; + const propertyName = data.Name || data.propertyName || "Property Name"; + const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; + const location = data.Address__c || data.location || "Location"; + const price = data.Price__c || data.price || "Price on Request"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const area = data.Square_Feet__c || data.area || "N/A"; + const description = data.Description_English__c || data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."; + const referenceId = data.Reference_Number__c || data.referenceNumber || "N/A"; - return `Property Brochure

${propertyName}

${location}

${price}
${bedrooms} Beds ${bathrooms} Baths ${area} sq. ft.

Description

${data.descriptionEnglish || data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."}

Reference ID: ${data.referenceNumber || "REF-001"}
Owner Info: ${data.ownerName || "Owner Name"}, ${data.ownerPhone || "Owner Phone"}

In-depth Details

A closer look at the property's features and specifications.

Specifications

Status: ${data.status || "Available"}
Type: ${propertyType}
Floor: ${data.floor || "2 Levels"}
Parking: ${data.parking || "3-Car Garage"}
Year Built: ${data.yearBuilt || "2023"}
Furnishing: ${data.furnishing || "Partially Furnished"}
Maintenance Fee: ${data.maintenanceFee || "$1,200 / month"}
Service Charge: ${data.serviceCharge || "Included"}

Amenities & Features

Infinity Pool
Private Home Theater
Gourmet Chef's Kitchen
Wine Cellar
Smart Home Automation
Spa & Sauna Room
Landscaped Gardens
Outdoor Fire Pit
Reference ID: ${data.referenceNumber || "REF-001"}
Owner Info: ${data.ownerName || "Owner Name"}, ${data.ownerPhone || "Owner Phone"}

Location & Nearby

Landmarks: ${data.nearbyLandmarks || "Nearby Landmarks"}
Transportation: ${data.transportation || "Public Transport"}
Schools: ${data.schools || "5 min drive"}
Hospitals: ${data.hospitals || "12 min drive"}
Shopping: ${data.shoppingCenters || "10 min drive"}
Airport: ${data.airportDistance || "20 min drive"}
City: ${data.cityBayut || data.cityPropertyfinder || "City"}
Community: ${data.communityBayut || "Community"}
Sub Community: ${data.subCommunityBayut || "Sub Community"}
Locality: ${data.localityBayut || "Locality"}
Sub Locality: ${data.subLocalityBayut || "Sub Locality"}
Tower: ${data.towerBayut || "Tower"}
Beach Distance: ${data.beachDistance || "Beach Distance"}
Metro Distance: ${data.metroDistance || "Metro Distance"}
Country Club: ${data.countryClub || "Country Club"}

Additional Information

Pet Friendly: ${data.petFriendly || "Pet Friendly Status"}
Smoking: ${data.smokingAllowed || "Smoking Allowed"}
Available From: ${data.availableFrom || "Available From Date"}
Minimum Contract: ${data.minimumContract || "Minimum Contract Duration"}
Security Deposit: ${data.securityDeposit || "Security Deposit"}
Reference ID: ${data.referenceNumber || "REF-001"}
Owner Info: ${data.ownerName || "Owner Name"}, ${data.ownerPhone || "Owner Phone"}
`; + // Contact information + const agentName = data.contactName || data.Agent_Name__c || data.agentName || "N/A"; + const agentPhone = data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A"; + const agentEmail = data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A"; + + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const kitchenImage = this.getSmartImageForSection('kitchen', 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); + + return `Property Brochure

${propertyName}

${location}

${price}
${bedrooms} Beds ${bathrooms} Baths ${area} sq. ft.

Description

${data.descriptionEnglish || data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."}

Reference ID: ${data.Reference_Number__c || data.referenceNumber || "REF-001"}

In-depth Details

A closer look at the property's features and specifications.

Specifications

Status: ${data.status || "Available"}
Type: ${propertyType}
Floor: ${data.Floor__c || data.floor || "N/A"}
Parking: ${data.Parking_Spaces__c || data.parking || "N/A"}
Year Built: ${data.Build_Year__c || data.yearBuilt || "N/A"}
Furnishing: ${data.Furnished__c || data.furnishing || "N/A"}
Maintenance Fee: ${data.Maintenance_Fee__c || data.maintenanceFee || "N/A"}
Service Charge: ${data.Service_Charge__c || data.serviceCharge || "N/A"}

Amenities & Features

${amenitiesHTML}
Reference ID: ${data.Reference_Number__c || data.referenceNumber || "REF-001"}

Location & Nearby

Landmarks: ${data.Landmarks__c || data.nearbyLandmarks || "N/A"}
Transportation: ${data.Transportation__c || data.transportation || "N/A"}
Schools: ${data.Schools__c || data.schools || "N/A"}
Hospitals: ${data.Hospitals__c || data.hospitals || "N/A"}
Shopping: ${data.Shopping_Centers__c || data.shoppingCenters || "N/A"}
Airport: ${data.Airport_Distance__c || data.airportDistance || "N/A"}
City: ${data.City__c || data.cityBayut || data.cityPropertyfinder || "N/A"}
Community: ${data.Community__c || data.communityBayut || "N/A"}
Sub Community: ${data.Sub_Community__c || data.subCommunityBayut || "N/A"}
Locality: ${data.Locality__c || data.localityBayut || "N/A"}
Sub Locality: ${data.Sub_Locality__c || data.subLocalityBayut || "N/A"}
Tower: ${data.Tower__c || data.towerBayut || "N/A"}
Beach Distance: ${data.Beach_Distance__c || data.beachDistance || "N/A"}
Metro Distance: ${data.Metro_Distance__c || data.metroDistance || "N/A"}
Country Club: ${data.Country_Club__c || data.countryClub || "N/A"}
Property Location Map

Additional Information

Pet Friendly: ${data.petFriendly || "Pet Friendly Status"}
Smoking: ${data.smokingAllowed || "Smoking Allowed"}
Available From: ${data.availableFrom || "Available From Date"}
Minimum Contract: ${data.minimumContract || "Minimum Contract Duration"}
Security Deposit: ${data.securityDeposit || "Security Deposit"}
Reference ID: ${data.Reference_Number__c || data.referenceNumber || "REF-001"}
`; } createAsgar1Template() { const data = this.propertyData || {}; // Basic property information - const propertyName = data.propertyName || "The Grand Oak Villa"; - const location = data.location || "123 Luxury Lane, Prestige City, PC 45678"; - const price = data.price || "$4,500,000"; - const referenceId = data.referenceNumber || "GV-2025-001"; - const bedrooms = data.bedrooms || "5"; - const bathrooms = data.bathrooms || "6"; - const area = data.area || "6,200"; - const squareFeet = data.area || "6,200"; + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const price = data.Price__c || data.price || "Price"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "N/A"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const area = data.Square_Feet__c || data.area || "N/A"; + const squareFeet = data.Square_Feet__c || data.area || "N/A"; const sizeUnit = data.sizeUnit || "sq ft"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const description = data.Description_English__c || data.description || "Property description not available."; + + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const bedroomImage = this.getSmartImageForSection('bedroom', 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); // Contact information - const contactName = data.contactName || "Contact Name"; - const contactPhone = data.contactPhone || "Contact Phone"; - const contactEmail = data.contactEmail || "contact@email.com"; - const ownerName = data.ownerName || "Owner Name"; - const ownerPhone = data.ownerPhone || "Owner Phone"; - const ownerEmail = data.ownerEmail || "owner@email.com"; + const contactName = data.Agent_Name__c || data.contactName || "N/A"; + const contactPhone = data.Agent_Phone__c || data.contactPhone || "N/A"; + const contactEmail = data.Agent_Email__c || data.contactEmail || "N/A"; + const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; + const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; + const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Property specifications - const status = data.status || "For Sale"; - const propertyType = data.propertyType || "Single-Family Home"; - const yearBuilt = data.yearBuilt || "2023"; - const floor = data.floor || "2 Levels"; - const parking = data.parking || "3-Car Garage"; - const furnishing = data.furnishing || "Partially Furnished"; - const maintenanceFee = data.maintenanceFee || "$1,200/month"; - const serviceCharge = data.serviceCharge || "Included"; + const status = data.Status__c || data.status || "N/A"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; + const floor = data.Floor__c || data.floor || "N/A"; + const parking = data.Parking_Spaces__c || data.parking || "N/A"; + const furnishing = data.Furnished__c || data.furnishing || "N/A"; + const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; // Property details const acres = data.acres || "0.75"; // Location details - const cityBayut = data.cityBayut || "City"; - const cityPropertyfinder = data.cityPropertyfinder || "City"; - const communityBayut = data.communityBayut || "Community"; - const subCommunityBayut = data.subCommunityBayut || "Sub Community"; - const localityBayut = data.localityBayut || "Locality"; - const subLocalityBayut = data.subLocalityBayut || "Sub Locality"; - const towerBayut = data.towerBayut || "Tower"; + const cityBayut = data.City__c || data.cityBayut || "N/A"; + const cityPropertyfinder = data.City__c || data.cityPropertyfinder || "N/A"; + const communityBayut = data.Community__c || data.communityBayut || "N/A"; + const subCommunityBayut = data.Sub_Community__c || data.subCommunityBayut || "N/A"; + const localityBayut = data.Locality__c || data.localityBayut || "N/A"; + const subLocalityBayut = data.Sub_Locality__c || data.subLocalityBayut || "N/A"; + const towerBayut = data.Tower__c || data.towerBayut || "N/A"; // Nearby amenities - const schools = data.schools || "5 min drive"; - const shoppingCenters = data.shoppingCenters || "10 min drive"; - const hospitals = data.hospitals || "12 min drive"; - const countryClub = data.countryClub || "8 min drive"; - const airportDistance = data.airportDistance || "20 min drive"; - const nearbyLandmarks = data.nearbyLandmarks || "Nearby Landmarks"; - const transportation = data.transportation || "Public Transport"; - const beachDistance = data.beachDistance || "15 min drive"; - const metroDistance = data.metroDistance || "8 min walk"; + const schools = data.Schools__c || data.schools || "N/A"; + const shoppingCenters = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const countryClub = data.Country_Club__c || data.countryClub || "N/A"; + const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; + const nearbyLandmarks = data.Landmarks__c || data.nearbyLandmarks || "N/A"; + const transportation = data.Transportation__c || data.transportation || "N/A"; + const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A"; + const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A"; // Additional information - const petFriendly = data.petFriendly || "By Approval"; - const smokingAllowed = data.smokingAllowed || "Not Permitted"; - const availableFrom = data.availableFrom || "Immediate"; - const utilitiesIncluded = data.utilitiesIncluded || "Not Included"; - const internetIncluded = data.internetIncluded || "Not Included"; - const cableIncluded = data.cableIncluded || "Not Included"; + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smokingAllowed = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availableFrom = data.Available_From__c || data.availableFrom || "N/A"; + const utilitiesIncluded = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; + const internetIncluded = data.Internet_Included__c || data.internetIncluded || "N/A"; + const cableIncluded = data.Cable_Included__c || data.cableIncluded || "N/A"; // Additional property fields - const titleEnglish = data.titleEnglish || "Property Title"; - const descriptionEnglish = data.descriptionEnglish || "Property Description"; - const amenities = data.amenities || "No specific amenities listed."; - const features = data.features || "No specific features listed."; - const size = data.size || "N/A"; - const parkingSpaces = data.parkingSpaces || "N/A"; - const buildYear = data.buildYear || "N/A"; - const offeringType = data.offeringType || "N/A"; + const titleEnglish = data.Title_English__c || data.titleEnglish || "N/A"; + const descriptionEnglish = data.Description_English__c || data.descriptionEnglish || "N/A"; + const amenities = data.Amenities__c || data.amenities || "N/A"; + const features = data.Features__c || data.features || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A"; + const buildYear = data.Build_Year__c || data.buildYear || "N/A"; + const offeringType = data.Offering_Type__c || data.offeringType || "N/A"; // Financial and availability fields - const rentPriceMin = data.rentPriceMin || "N/A"; - const salePriceMin = data.salePriceMin || "N/A"; - const rentAvailableFrom = data.rentAvailableFrom || "N/A"; - const rentAvailableTo = data.rentAvailableTo || "N/A"; - const minimumContract = data.minimumContract || "N/A"; - const securityDeposit = data.securityDeposit || "N/A"; + const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; + const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; + const rentAvailableFrom = data.Rent_Available_From__c || data.rentAvailableFrom || "N/A"; + const rentAvailableTo = data.Rent_Available_To__c || data.rentAvailableTo || "N/A"; + const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A"; + const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; - return `Prestige Real Estate Brochure - 4 Page
${status.toUpperCase()}

${propertyName}

${location}

${bedrooms}
Bedrooms
${bathrooms}
Bathrooms
${squareFeet}
Sq. Ft.
${price}
Price

Description

${titleEnglish ? `Title: ${titleEnglish}` : ''}

${descriptionEnglish ? `Description: ${descriptionEnglish}` : ''}

${amenities ? `Amenities: ${amenities}` : ''}

${features ? `Features: ${features}` : ''}

Specifications

Reference ID: ${referenceId}
Status: ${status}
Type: ${propertyType}
Year Built: ${yearBuilt}
Floor: ${floor}
Parking: ${parking}
Furnishing: ${furnishing}
Maintenance Fee: ${maintenanceFee}
Service Charge: ${serviceCharge}
Size: ${size}
Parking Spaces: ${parkingSpaces}
Build Year: ${buildYear}
Offering Type: ${offeringType}
Bedrooms: ${bedrooms}
Bathrooms: ${bathrooms}
Area: ${area} ${sizeUnit}
Price: ${price}
Rent Price (Min): ${rentPriceMin}
Sale Price (Min): ${salePriceMin}
Rent Available From: ${rentAvailableFrom}
Rent Available To: ${rentAvailableTo}
Available From: ${availableFrom}
Minimum Contract: ${minimumContract}
Security Deposit: ${securityDeposit}
Pet Friendly: ${petFriendly}
Smoking Allowed: ${smokingAllowed}

Amenities & Features

  • Infinity Pool
  • Private Home Theater
  • Gourmet Chef's Kitchen
  • Wine Cellar
  • Smart Home Automation
  • Spa & Sauna Room
  • Landscaped Gardens
  • Outdoor Fire Pit
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}
Schools
${schools}
Shopping
${shoppingCenters}
Airport
${airportDistance}
Landmarks
${nearbyLandmarks}
Transportation
${transportation}
Hospitals
${hospitals}
Beach
${beachDistance}
Metro
${metroDistance}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}

Additional Information

Pet Friendly: ${petFriendly}
Smoking: ${smokingAllowed}
Available From: ${availableFrom}
Minimum Contract: ${minimumContract}
Security Deposit: ${securityDeposit}
Utilities Included: ${utilitiesIncluded}
Internet Included: ${internetIncluded}
Cable Included: ${cableIncluded}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}
`; + return `Prestige Real Estate Brochure - 4 Page
${status.toUpperCase()}

${propertyName}

${location}

${bedrooms}
Bedrooms
${bathrooms}
Bathrooms
${squareFeet}
Sq. Ft.
${price}
Price

Description

${titleEnglish ? `Title: ${titleEnglish}` : ''}

${descriptionEnglish ? `Description: ${descriptionEnglish}` : ''}

${amenities ? `Amenities: ${amenities}` : ''}

${features ? `Features: ${features}` : ''}

Specifications

Reference ID: ${referenceId}
Status: ${status}
Type: ${propertyType}
Year Built: ${yearBuilt}
Floor: ${floor}
Parking: ${parking}
Furnishing: ${furnishing}
Maintenance Fee: ${maintenanceFee}
Service Charge: ${serviceCharge}
Size: ${size}
Parking Spaces: ${parkingSpaces}
Build Year: ${buildYear}
Offering Type: ${offeringType}
Bedrooms: ${bedrooms}
Bathrooms: ${bathrooms}
Area: ${area} ${sizeUnit}
Price: ${price}
Rent Price (Min): ${rentPriceMin}
Sale Price (Min): ${salePriceMin}
Rent Available From: ${rentAvailableFrom}
Rent Available To: ${rentAvailableTo}
Available From: ${availableFrom}
Minimum Contract: ${minimumContract}
Security Deposit: ${securityDeposit}
Pet Friendly: ${petFriendly}
Smoking Allowed: ${smokingAllowed}

Amenities & Features

${amenitiesHTML}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}
Schools
${schools}
Shopping
${shoppingCenters}
Airport
${airportDistance}
Landmarks
${nearbyLandmarks}
Transportation
${transportation}
Hospitals
${hospitals}
Beach
${beachDistance}
Metro
${metroDistance}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}

Additional Information

Pet Friendly: ${petFriendly}
Smoking: ${smokingAllowed}
Available From: ${availableFrom}
Minimum Contract: ${minimumContract}
Security Deposit: ${securityDeposit}
Utilities Included: ${utilitiesIncluded}
Internet Included: ${internetIncluded}
Cable Included: ${cableIncluded}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}
Agent: ${data.agentName || "Agent Name"} | ${data.agentPhone || "Agent Phone"} | ${data.agentEmail || "Agent Email"}
Owner: ${ownerName} | ${ownerPhone} | ${ownerEmail}
`; } createSampleTemplate() { @@ -1688,96 +2071,113 @@ export default class PropertyTemplateSelector extends LightningElement { const data = this.propertyData || {}; // Extract all available property data with fallbacks - const propertyName = data.propertyName || "THE VERTICE"; - const location = data.location || "18 Skyline Avenue, Metropolis Centre, MC 90210"; - const referenceId = data.referenceNumber || "VP-2025-001"; - const agentName = data.agentName || "Alexander Valentine"; - const agentPhone = data.agentPhone || "(555) 123-9876"; - const agentEmail = data.agentEmail || "alex.v@elysian.com"; + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "N/A"; + const agentName = data.contactName || data.Agent_Name__c || data.agentName || "N/A"; + const agentPhone = data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A"; + const agentEmail = data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A"; // Dynamic pricing with fallbacks - const price = data.salePriceMin || data.rentPriceMin || data.price || "Starting from $1,200,000"; + const price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; // Dynamic property details - const bedrooms = data.bedrooms || "2-3"; - const bathrooms = data.bathrooms || "2-3.5"; - const squareFeet = data.squareFeet || data.area || "1,450 - 3,200"; - const propertyType = data.propertyType || "Condominium"; - const status = data.status || data.offeringType || "New Development"; - const yearBuilt = data.yearBuilt || data.buildYear || "2025"; - const furnishing = data.furnishing || "Unfurnished"; - const parking = data.parking || "2 Spaces per Unit"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const status = data.Status__c || data.status || data.offeringType || "N/A"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || data.buildYear || "N/A"; + const furnishing = data.Furnished__c || data.furnishing || "N/A"; + const parking = data.Parking_Spaces__c || data.parking || "N/A"; // Dynamic description - const description = data.descriptionEnglish || data.description || "A bold statement on modern urban living."; + const description = data.Description_English__c || data.descriptionEnglish || data.description || "Property description not available."; + + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const kitchenImage = this.getSmartImageForSection('kitchen', 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); // Dynamic location details - const schools = data.schools || "Metropolis Intl. (10 min)"; - const shopping = data.shoppingCenters || "The Galleria Mall (8 min)"; - const hospitals = data.hospitals || "City Medical Center (15 min)"; - const landmarks = data.landmarks || "Central Park (5 min)"; - const transportation = data.transportation || "Metro Line A (2 min walk)"; - const airport = data.airportDistance || "25 min drive"; + const schools = data.Schools__c || data.schools || "N/A"; + const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const landmarks = data.Landmarks__c || data.landmarks || "N/A"; + const transportation = data.Transportation__c || data.transportation || "N/A"; + const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; // Dynamic additional info - const petFriendly = data.petFriendly || "Allowed (w/ restrictions)"; - const smoking = data.smokingAllowed || "In designated areas"; - const availability = data.availableFrom || "Q4 2025"; - const utilities = data.utilitiesIncluded || "Sub-metered"; + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availability = data.Available_From__c || data.availableFrom || "N/A"; + const utilities = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; - // Dynamic amenities - const amenities = data.amenities || [ - "Rooftop Infinity Pool", "Fitness Center", "Residents' Sky Lounge", - "Private Cinema Room", "Wellness Spa & Sauna", "Business Center", - "24/7 Concierge", "Secure Parking" - ]; + - return `Modern Urban Residences Brochure - ${propertyName}
An Urban Oasis

${propertyName}

${location}

Where Design Meets Desire.

${propertyName} is not just a building; it's a bold statement on modern urban living. ${description}

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.

${propertyName}Page 02 / 06
${propertyName}Page 03 / 06

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.

Lifestyle Amenities

    ${this.buildAmenitiesListForVertice(data)}

Key Specifications

Status ${status}
Property Type ${propertyType}
Year Built ${yearBuilt}
Technology Integrated Smart Home
Design Sustainable & Eco-Friendly
Bedrooms ${bedrooms}
Bathrooms ${bathrooms}
Square Feet ${squareFeet}
${propertyName}Page 04 / 06

${bedrooms}-Bedroom Residence

${squareFeet}
SQ. FT.
${bedrooms}
BEDROOMS
${bathrooms}
BATHROOMS
${parking}
PARKING

A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.

Premium ${propertyType}

${squareFeet}
SQ. FT.
${bedrooms}
BEDROOMS
${bathrooms}
BATHROOMS
${furnishing}
FURNISHING

The pinnacle of luxury living, this residence offers expansive spaces, premium finishes, and exclusive access to premium amenities.

Additional Information

Pets
${petFriendly}
Smoking
${smoking}
Availability
${availability}
Parking
${parking}
Furnishing
${furnishing}
Utilities
${utilities}
${propertyName}Page 05 / 06

Schedule a Private Viewing

Experience ${propertyName} firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.

${agentName}
Sales Executive, Elysian Properties
${agentPhone}
${agentEmail}

Neighborhood Highlights

  • Landmarks: ${landmarks}
  • Transportation: ${transportation}
  • Schools: ${schools}
  • Shopping: ${shopping}
  • Airport: ${airport}
${propertyName}Page 06 / 06
`; + return `Modern Urban Residences Brochure - ${propertyName}
An Urban Oasis

${propertyName}

${location}

Where Design Meets Desire.

${propertyName} is not just a building; it's a bold statement on modern urban living. ${description}

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.

${propertyName}Page 02 / 07
${propertyName}Page 03 / 07

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.

Lifestyle Amenities

    ${amenitiesHTML}

Key Specifications

Status ${status}
Property Type ${propertyType}
Year Built ${yearBuilt}
Floor ${data.Floor__c || "N/A"}
Maintenance Fee ${data.Maintenance_Fee__c || "N/A"}
Bedrooms ${bedrooms}
Bathrooms ${bathrooms}
Square Feet ${squareFeet}
${propertyName}Page 04 / 07

${bedrooms}-Bedroom Residence

${squareFeet}
SQ. FT.
${bedrooms}
BEDROOMS
${bathrooms}
BATHROOMS
${parking}
PARKING

A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.

Premium ${propertyType}

${squareFeet}
SQ. FT.
${bedrooms}
BEDROOMS
${bathrooms}
BATHROOMS
${furnishing}
FURNISHING

The pinnacle of luxury living, this residence offers expansive spaces, premium finishes, and exclusive access to premium amenities.

Additional Information

Pets
${petFriendly}
Smoking
${smoking}
Availability
${availability}
Parking
${parking}
Furnishing
${furnishing}
Utilities
${utilities}
${propertyName}Page 05 / 07

Schedule a Private Viewing

Experience ${propertyName} firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.

${agentName}
Sales Executive, Elysian Properties
${agentPhone}
${agentEmail}

Neighborhood Highlights

  • Landmarks: ${landmarks}
  • Transportation: ${transportation}
  • Schools: ${schools}
  • Shopping: ${shopping}
  • Airport: ${airport}
${propertyName}Page 06 / 07
${propertyGallery}
${propertyName}Page 07 / 07
`; } createSerenityHouseTemplate() { const data = this.propertyData || {}; // Extract all available property data with fallbacks - const propertyName = data.propertyName || "The Serenity House"; - const location = data.location || "123 Luxury Lane, Prestige City, PC 45678"; - const referenceId = data.referenceNumber || "ES-8821"; - const agentName = data.agentName || "Olivia Sterling"; - const agentPhone = data.agentPhone || "(555) 987-6543"; - const agentEmail = data.agentEmail || "olivia@elysianestates.com"; - const ownerName = data.ownerName || "John & Jane Doe"; - const ownerPhone = data.ownerPhone || "(555) 111-2222"; - const ownerEmail = data.ownerEmail || "owner.serenity@email.com"; + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const referenceId = data.Reference_Number__c || data.referenceNumber || "N/A"; + const agentName = data.contactName || data.Agent_Name__c || data.agentName || "N/A"; + const agentPhone = data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A"; + const agentEmail = data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A"; + const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; + const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; + const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Dynamic pricing with fallbacks - const price = data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; + const price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; const priceDisplay = price !== "Price on Request" ? `Offered at ${price}` : "Price on Request"; // Dynamic property details - const bedrooms = data.bedrooms || "5"; - const bathrooms = data.bathrooms || "6"; - const squareFeet = data.squareFeet || data.area || "6,200"; - const propertyType = data.propertyType || "Single-Family Home"; - const status = data.status || data.offeringType || "For Sale"; - const yearBuilt = data.yearBuilt || data.buildYear || "2023"; - const furnishing = data.furnishing || "Partially Furnished"; - const parking = data.parking || "3-Car Garage"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const status = data.Status__c || data.status || data.offeringType || "N/A"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || data.buildYear || "N/A"; + const furnishing = data.Furnished__c || data.furnishing || "N/A"; + const parking = data.Parking_Spaces__c || data.parking || "N/A"; // Dynamic description - const description = data.descriptionEnglish || data.description || "An architectural marvel of curated living space."; + const description = data.Description_English__c || data.descriptionEnglish || data.description || "Property description not available."; + + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection('interior', 'https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + const bedroomImage = this.getSmartImageForSection('bedroom', 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'); + + // Generate property gallery + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); // Dynamic location details - const schools = data.schools || "5 min drive"; - const shopping = data.shoppingCenters || "10 min drive"; - const hospitals = data.hospitals || "12 min drive"; - const countryClub = data.countryClub || "8 min drive"; - const airport = data.airportDistance || "20 min drive"; + const schools = data.Schools__c || data.schools || "N/A"; + const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const countryClub = data.Country_Club__c || data.countryClub || "N/A"; + const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; // Dynamic additional info - const petFriendly = data.petFriendly || "By Approval"; - const smoking = data.smokingAllowed || "Not Permitted"; - const availability = data.availableFrom || "Immediate"; - const utilities = data.utilitiesIncluded || "Not Included"; + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availability = data.Available_From__c || data.availableFrom || "N/A"; + const utilities = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; return ` @@ -2068,8 +2468,8 @@ export default class PropertyTemplateSelector extends LightningElement { height: 280px; 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-image: url('${this.getMapsImageUrl()}'); + background-size: cover; background-position: center; background-repeat: no-repeat; margin-bottom: 40px; @@ -2224,11 +2624,11 @@ export default class PropertyTemplateSelector extends LightningElement {
Floor - ${data.floor || "2 Levels"} + ${data.Floor__c || data.floor || "N/A"}
Maintenance Fee - ${data.maintenanceFee || "$1,200 / month"} + ${data.Maintenance_Fee__c || data.maintenanceFee || "N/A"}
Parking @@ -2236,16 +2636,16 @@ export default class PropertyTemplateSelector extends LightningElement {
Service Charge - ${data.serviceCharge || "Included"} + ${data.Service_Charge__c || data.serviceCharge || "N/A"}

Amenities & Features

-
    - ${this.buildAmenitiesList(data)} -
+
+ ${amenitiesHTML} +
@@ -2313,6 +2713,18 @@ export default class PropertyTemplateSelector extends LightningElement { + + +
+
+ 05 +

${propertyName} - Property Gallery

+ + +
+
`; } @@ -2433,6 +2845,748 @@ export default class PropertyTemplateSelector extends LightningElement { } } + // Handle bullet list + handleBulletList() { + const selection = window.getSelection(); + if (selection.rangeCount > 0 && selection.toString().trim()) { + // Apply bullet formatting to selected text + const range = selection.getRangeAt(0); + const selectedText = selection.toString(); + + // Create a list item with the selected text + const listItem = document.createElement('li'); + listItem.textContent = selectedText; + listItem.style.listStyleType = 'disc'; + listItem.style.marginLeft = '20px'; + + // Check if we're already in a list + const parentList = range.commonAncestorContainer.parentElement?.closest('ul, ol'); + if (parentList) { + // Add to existing list + parentList.appendChild(listItem); + } else { + // Create new list + const list = document.createElement('ul'); + list.appendChild(listItem); + range.deleteContents(); + range.insertNode(list); + } + } else { + this.showError('Please select text first to apply bullet formatting'); + } + } + + // Handle numbered list + handleNumberedList() { + const selection = window.getSelection(); + if (selection.rangeCount > 0 && selection.toString().trim()) { + // Apply number formatting to selected text + const range = selection.getRangeAt(0); + const selectedText = selection.toString(); + + // Create a list item with the selected text + const listItem = document.createElement('li'); + listItem.textContent = selectedText; + listItem.style.listStyleType = 'decimal'; + listItem.style.marginLeft = '20px'; + + // Check if we're already in a list + const parentList = range.commonAncestorContainer.parentElement?.closest('ul, ol'); + if (parentList) { + // Add to existing list + parentList.appendChild(listItem); + } else { + // Create new list + const list = document.createElement('ol'); + list.appendChild(listItem); + range.deleteContents(); + range.insertNode(list); + } + } else { + this.showError('Please select text first to apply number formatting'); + } + } + + // Toggle selector mode + toggleSelectorMode() { + this.selectorMode = !this.selectorMode; + const button = this.template.querySelector('.selector-mode-text'); + const controls = this.template.querySelector('.selector-controls'); + + if (button) { + button.textContent = this.selectorMode ? 'Exit Selector' : 'Selector Mode'; + } + + if (controls) { + controls.style.display = this.selectorMode ? 'flex' : 'none'; + } + + if (this.selectorMode) { + this.addSelectorModeListeners(); + } else { + this.removeSelectorModeListeners(); + this.clearSelection(); + } + } + + // Add selector mode event listeners + addSelectorModeListeners() { + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + editor.addEventListener('click', this.handleSelectorClick.bind(this)); + editor.style.cursor = 'crosshair'; + } + } + + // Remove selector mode event listeners + removeSelectorModeListeners() { + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + editor.removeEventListener('click', this.handleSelectorClick.bind(this)); + editor.style.cursor = 'default'; + } + } + + // Handle selector click + handleSelectorClick(event) { + if (!this.selectorMode) return; + + event.preventDefault(); + event.stopPropagation(); + + this.clearSelection(); + + const element = event.target; + if (element && element !== this.template.querySelector('.enhanced-editor-content')) { + this.selectedElement = element; + this.highlightSelectedElement(element); + // Don't show floating panel - controls are now in toolbar + } + } + + // Highlight selected element + highlightSelectedElement(element) { + element.style.outline = '2px solid #007bff'; + element.style.outlineOffset = '2px'; + } + + // Clear selection + clearSelection() { + if (this.selectedElement) { + this.selectedElement.style.outline = ''; + this.selectedElement.style.outlineOffset = ''; + this.selectedElement = null; + } + // Don't hide floating panel since we're not using it + } + + // Show selector options + showSelectorOptions(element) { + // Create or update selector options panel + let optionsPanel = this.template.querySelector('.selector-options-panel'); + if (!optionsPanel) { + optionsPanel = document.createElement('div'); + optionsPanel.className = 'selector-options-panel'; + optionsPanel.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: white; + border: 2px solid #007bff; + border-radius: 8px; + padding: 15px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10000; + min-width: 200px; + max-width: 250px; + `; + document.body.appendChild(optionsPanel); + } + + optionsPanel.innerHTML = ` +
+ Element Options +
+
+ + +
+
+ + +
+
+ + +
+ + `; + } + + // Hide selector options + hideSelectorOptions() { + const optionsPanel = this.template.querySelector('.selector-options-panel'); + if (optionsPanel) { + optionsPanel.remove(); + } + } + + // Insert content at selected position + insertAtSelection(type) { + if (!this.selectedElement) return; + + let content; + switch (type) { + case 'text': + content = document.createElement('p'); + content.textContent = 'New Text'; + content.contentEditable = true; + break; + case 'image': + content = document.createElement('img'); + content.src = 'https://via.placeholder.com/200x150'; + content.style.maxWidth = '200px'; + content.style.height = 'auto'; + content.draggable = true; + content.addEventListener('dragstart', this.handleImageDragStart.bind(this)); + break; + case 'table': + content = this.createTableElement(); + // Make table draggable + content.draggable = true; + content.addEventListener('dragstart', this.handleTableDragStart.bind(this)); + break; + } + + if (content) { + this.selectedElement.parentNode.insertBefore(content, this.selectedElement.nextSibling); + this.clearSelection(); + } + } + + // Remove selected element + removeSelectedElement() { + if (this.selectedElement) { + this.selectedElement.remove(); + this.clearSelection(); + } + } + + // Move element up + moveElementUp() { + if (this.selectedElement && this.selectedElement.previousElementSibling) { + this.selectedElement.parentNode.insertBefore(this.selectedElement, this.selectedElement.previousElementSibling); + } + } + + // Move element down + moveElementDown() { + if (this.selectedElement && this.selectedElement.nextElementSibling) { + this.selectedElement.parentNode.insertBefore(this.selectedElement.nextElementSibling, this.selectedElement); + } + } + + // Insert property image + insertPropertyImage() { + if (!this.selectedElement) return; + + // Show property image selection popup + this.showPropertyImagePopup(); + } + + // Insert local image + insertLocalImage() { + if (!this.selectedElement) return; + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const img = document.createElement('img'); + img.src = e.target.result; + img.style.maxWidth = '200px'; + img.style.height = 'auto'; + img.draggable = true; + img.addEventListener('dragstart', this.handleImageDragStart.bind(this)); + + this.selectedElement.parentNode.insertBefore(img, this.selectedElement.nextSibling); + this.clearSelection(); + }; + reader.readAsDataURL(file); + } + }; + input.click(); + } + + // Show property image popup + showPropertyImagePopup() { + // Create property image selection popup + let popup = this.template.querySelector('.property-image-popup'); + if (!popup) { + popup = document.createElement('div'); + popup.className = 'property-image-popup'; + popup.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border: 2px solid #007bff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + max-width: 400px; + max-height: 500px; + overflow-y: auto; + `; + document.body.appendChild(popup); + } + + // Get property images + const images = this.realPropertyImages || []; + const imageGrid = images.map(img => ` +
+ +
${img.category || 'Uncategorized'}
+
+ `).join(''); + + popup.innerHTML = ` +
+ Select Property Image +
+
+ ${imageGrid} +
+ + `; + } + + // Select property image + selectPropertyImage(imageUrl) { + if (this.selectedElement) { + const img = document.createElement('img'); + img.src = imageUrl; + img.style.maxWidth = '200px'; + img.style.height = 'auto'; + img.draggable = true; + img.addEventListener('dragstart', this.handleImageDragStart.bind(this)); + + this.selectedElement.parentNode.insertBefore(img, this.selectedElement.nextSibling); + this.clearSelection(); + } + this.closePropertyImagePopup(); + } + + // Close property image popup + closePropertyImagePopup() { + const popup = this.template.querySelector('.property-image-popup'); + if (popup) { + popup.remove(); + } + } + + // Create table element with enhanced drag and resize functionality + createTableElement() { + // Create the main table container with absolute positioning for drag/resize + const tableContainer = document.createElement('div'); + tableContainer.className = 'draggable-table-container'; + tableContainer.style.cssText = ` + position: absolute; + left: 50px; + top: 50px; + width: 400px; + min-width: 200px; + min-height: 150px; + z-index: 1000; + border: 2px solid transparent; + cursor: move; + user-select: none; + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; + `; + + // Create the actual table + const table = document.createElement('table'); + table.style.cssText = ` + width: 100%; + height: 100%; + border-collapse: collapse; + margin: 0; + background: white; + `; + + // Create header row + const headerRow = document.createElement('tr'); + for (let i = 0; i < this.tableCols; i++) { + const th = document.createElement('th'); + th.textContent = `Header ${i + 1}`; + th.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: #f8f9fa; + font-weight: 600; + text-align: left; + `; + headerRow.appendChild(th); + } + table.appendChild(headerRow); + + // Create data rows + const startRow = this.includeHeader ? 1 : 0; + for (let i = startRow; i < this.tableRows; i++) { + const row = document.createElement('tr'); + for (let j = 0; j < this.tableCols; j++) { + const td = document.createElement('td'); + td.textContent = `Cell ${i + 1},${j + 1}`; + td.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: white; + `; + // Make cells editable + td.contentEditable = true; + td.addEventListener('blur', () => { + // Save changes when cell loses focus + }); + row.appendChild(td); + } + table.appendChild(row); + } + + tableContainer.appendChild(table); + + // Add resize handles (same as images) + this.addResizeHandles(tableContainer); + + // Add delete handle (same as images) + this.addDeleteHandle(tableContainer); + + // Add drag functionality (same as images) + this.makeDraggable(tableContainer); + + // Add click to select functionality + tableContainer.addEventListener('click', (e) => { + e.stopPropagation(); + this.selectDraggableElement(tableContainer); + }); + + // Add table controls overlay + this.addTableControls(tableContainer, table); + + // Select the table after a short delay + setTimeout(() => { + this.selectDraggableElement(tableContainer); + }, 100); + + return tableContainer; + } + + // Add table controls overlay + addTableControls(container, table) { + const controls = document.createElement('div'); + controls.className = 'table-controls-overlay'; + controls.style.cssText = ` + position: absolute; + top: -40px; + left: 0; + background: white; + padding: 8px; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.2s ease; + display: flex; + gap: 4px; + z-index: 1002; + `; + + // Add Row button + const addRowBtn = document.createElement('button'); + addRowBtn.innerHTML = '+ Row'; + addRowBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + addRowBtn.onclick = (e) => { + e.stopPropagation(); + this.addTableRow(table); + }; + + // Add Column button + const addColBtn = document.createElement('button'); + addColBtn.innerHTML = '+ Col'; + addColBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #17a2b8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + addColBtn.onclick = (e) => { + e.stopPropagation(); + this.addTableColumn(table); + }; + + // Delete Row button + const delRowBtn = document.createElement('button'); + delRowBtn.innerHTML = '- Row'; + delRowBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #ffc107; + color: black; + border: none; + border-radius: 4px; + cursor: pointer; + `; + delRowBtn.onclick = (e) => { + e.stopPropagation(); + this.deleteTableRow(table); + }; + + // Delete Column button + const delColBtn = document.createElement('button'); + delColBtn.innerHTML = '- Col'; + delColBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #fd7e14; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + delColBtn.onclick = (e) => { + e.stopPropagation(); + this.deleteTableColumn(table); + }; + + controls.appendChild(addRowBtn); + controls.appendChild(addColBtn); + controls.appendChild(delRowBtn); + controls.appendChild(delColBtn); + + container.appendChild(controls); + + // Show/hide controls on hover + container.addEventListener('mouseenter', () => { + controls.style.opacity = '1'; + }); + + container.addEventListener('mouseleave', () => { + controls.style.opacity = '0'; + }); + } + + // Table manipulation methods (updated for new structure) + addTableRow(table) { + const newRow = document.createElement('tr'); + const colCount = table.rows[0].cells.length; + + for (let i = 0; i < colCount; i++) { + const td = document.createElement('td'); + td.textContent = `New Cell`; + td.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: white; + `; + td.contentEditable = true; + newRow.appendChild(td); + } + + table.appendChild(newRow); + } + + addTableColumn(table) { + const rows = table.rows; + + for (let i = 0; i < rows.length; i++) { + const cell = document.createElement(i === 0 ? 'th' : 'td'); + cell.textContent = i === 0 ? `Header ${rows[i].cells.length + 1}` : `New Cell`; + cell.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: ${i === 0 ? '#f8f9fa' : 'white'}; + font-weight: ${i === 0 ? '600' : 'normal'}; + `; + if (i > 0) { + cell.contentEditable = true; + } + rows[i].appendChild(cell); + } + } + + deleteTableRow(table) { + if (table.rows.length > 1) { + table.deleteRow(-1); + } + } + + deleteTableColumn(table) { + const rows = table.rows; + if (rows[0].cells.length > 1) { + for (let i = 0; i < rows.length; i++) { + rows[i].deleteCell(-1); + } + } + } + + deleteTable(event) { + const tableContainer = event.target.closest('div'); + tableContainer.remove(); + } + + // Make images draggable and resizable + makeImagesDraggableAndResizable() { + const editor = this.template.querySelector('.enhanced-editor-content'); + if (!editor) return; + + const images = editor.querySelectorAll('img'); + images.forEach(img => { + // Make draggable + img.draggable = true; + img.style.position = 'relative'; + img.style.zIndex = '1000'; + + // Add resize handles + this.addResizeHandles(img); + + // Add drag event listeners + img.addEventListener('dragstart', this.handleImageDragStart.bind(this)); + img.addEventListener('dragend', this.handleImageDragEnd.bind(this)); + }); + } + + // Add resize handles to image + addResizeHandles(img) { + const handles = ['nw', 'ne', 'sw', 'se']; + handles.forEach(handle => { + const resizeHandle = document.createElement('div'); + resizeHandle.className = `resize-handle resize-${handle}`; + resizeHandle.style.cssText = ` + position: absolute; + width: 8px; + height: 8px; + background: #007bff; + border: 1px solid white; + cursor: ${handle}-resize; + z-index: 1001; + `; + + // Position handles + switch (handle) { + case 'nw': + resizeHandle.style.top = '-4px'; + resizeHandle.style.left = '-4px'; + break; + case 'ne': + resizeHandle.style.top = '-4px'; + resizeHandle.style.right = '-4px'; + break; + case 'sw': + resizeHandle.style.bottom = '-4px'; + resizeHandle.style.left = '-4px'; + break; + case 'se': + resizeHandle.style.bottom = '-4px'; + resizeHandle.style.right = '-4px'; + break; + } + + img.appendChild(resizeHandle); + + // Add resize functionality + resizeHandle.addEventListener('mousedown', (e) => { + e.preventDefault(); + this.startResize(e, img, handle); + }); + }); + } + + // Handle image drag start + handleImageDragStart(event) { + event.dataTransfer.setData('text/plain', 'image'); + event.dataTransfer.effectAllowed = 'move'; + } + + // Handle image drag end + handleImageDragEnd(event) { + // Remove any drag feedback + } + + // Start resize operation + startResize(event, img, handle) { + const startX = event.clientX; + const startY = event.clientY; + const startWidth = img.offsetWidth; + const startHeight = img.offsetHeight; + const startLeft = img.offsetLeft; + const startTop = img.offsetTop; + + const handleMouseMove = (e) => { + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newWidth = startWidth; + let newHeight = startHeight; + let newLeft = startLeft; + let newTop = startTop; + + switch (handle) { + case 'se': + newWidth = startWidth + deltaX; + newHeight = startHeight + deltaY; + break; + case 'sw': + newWidth = startWidth - deltaX; + newHeight = startHeight + deltaY; + newLeft = startLeft + deltaX; + break; + case 'ne': + newWidth = startWidth + deltaX; + newHeight = startHeight - deltaY; + newTop = startTop + deltaY; + break; + case 'nw': + newWidth = startWidth - deltaX; + newHeight = startHeight - deltaY; + newLeft = startLeft + deltaX; + newTop = startTop + deltaY; + break; + } + + img.style.width = Math.max(50, newWidth) + 'px'; + img.style.height = Math.max(50, newHeight) + 'px'; + img.style.left = newLeft + 'px'; + img.style.top = newTop + 'px'; + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + } + handleAlignLeft() { const selection = window.getSelection(); if (selection.rangeCount > 0) { @@ -2611,26 +3765,110 @@ export default class PropertyTemplateSelector extends LightningElement { let clickedImage = null; // Method 1: Direct image click - if (e.target.tagName === 'IMG') { + if (e.target.tagName === 'IMG' && e.target.src && e.target.src.trim() !== '') { clickedImage = e.target; } - // Method 2: Click on element containing an image + // Method 2: Click on element containing an image (children) if (!clickedImage && e.target.querySelector) { const containedImg = e.target.querySelector('img'); - if (containedImg) { + if (containedImg && containedImg.src && containedImg.src.trim() !== '') { clickedImage = containedImg; } } + // Method 3: Click on element that is inside a container with an image (parent traversal) + if (!clickedImage) { + let currentElement = e.target; + while (currentElement && currentElement !== editor) { + // Check if current element is an IMG + if (currentElement.tagName === 'IMG' && currentElement.src && currentElement.src.trim() !== '') { + clickedImage = currentElement; + break; + } + // Check if current element contains an IMG + if (currentElement.querySelector && currentElement.querySelector('img')) { + const img = currentElement.querySelector('img'); + if (img && img.src && img.src.trim() !== '') { + clickedImage = img; + break; + } + } + // Check siblings for IMG elements only if current element is positioned + if (currentElement.parentElement && + (currentElement.style.position === 'absolute' || + currentElement.style.position === 'relative' || + currentElement.classList.contains('draggable-element'))) { + const siblingImg = currentElement.parentElement.querySelector('img'); + if (siblingImg && siblingImg.src && siblingImg.src.trim() !== '') { + clickedImage = siblingImg; + break; + } + } + currentElement = currentElement.parentElement; + } + } + + // Method 4: Check for background images in the element hierarchy (enhanced for property cards) + if (!clickedImage) { + let currentElement = e.target; + while (currentElement && currentElement !== editor) { + // Check for background images on any element (not just positioned ones) + const computedStyle = window.getComputedStyle(currentElement); + const backgroundImage = computedStyle.backgroundImage; + + if (backgroundImage && backgroundImage !== 'none' && backgroundImage !== 'initial') { + // Create a virtual IMG element for background images + const virtualImg = document.createElement('img'); + virtualImg.src = backgroundImage.replace(/url\(['"]?(.+?)['"]?\)/, '$1'); + virtualImg.isBackgroundImage = true; + virtualImg.style.backgroundImage = backgroundImage; + virtualImg.originalElement = currentElement; // Store reference to original element + clickedImage = virtualImg; + break; + } + + // Also check if this element has a background image set via CSS classes + if (currentElement.className) { + const classList = currentElement.className.split(' '); + for (let className of classList) { + // Look for common background image class patterns + if (className.includes('bg-') || className.includes('background') || + className.includes('hero') || className.includes('banner') || + className.includes('card') || className.includes('property')) { + + const classStyle = window.getComputedStyle(currentElement); + const classBgImage = classStyle.backgroundImage; + if (classBgImage && classBgImage !== 'none' && classBgImage !== 'initial') { + const virtualImg = document.createElement('img'); + virtualImg.src = classBgImage.replace(/url\(['"]?(.+?)['"]?\)/, '$1'); + virtualImg.isBackgroundImage = true; + virtualImg.style.backgroundImage = classBgImage; + virtualImg.originalElement = currentElement; + clickedImage = virtualImg; + break; + } + } + } + } + + currentElement = currentElement.parentElement; + } + } if (clickedImage) { - e.preventDefault(); - e.stopPropagation(); - this.openImageReplacement(clickedImage); + // Additional validation to ensure we have a valid image + if (clickedImage.tagName === 'IMG' || clickedImage.isBackgroundImage) { + this.handleImageClick(clickedImage, e); return; + } else { + console.log('Invalid image element detected, ignoring:', clickedImage); + } } + // Reset image click tracking when clicking on non-image areas + this.resetImageClickTracking(); + // Only deselect if clicking on the editor background or non-editable content if (e.target === editor || (!e.target.classList.contains('draggable-element') && !e.target.closest('.draggable-element'))) { @@ -2655,6 +3893,19 @@ export default class PropertyTemplateSelector extends LightningElement { e.preventDefault(); }, true); + // Add keyboard event handling for undo/redo + editor.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + if (e.key === 'z' && !e.shiftKey) { + e.preventDefault(); + this.undo(); + } else if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) { + e.preventDefault(); + this.redo(); + } + } + }); + editor.hasClickHandler = true; } } @@ -2691,6 +3942,353 @@ export default class PropertyTemplateSelector extends LightningElement { } } + // Show image insertion modal + showImageInsertModal() { + this.showImageModal = true; + this.selectedImageUrl = ''; + this.selectedImageName = ''; + this.uploadedImageData = ''; + this.selectedImageCategory = 'all'; + this.insertButtonDisabled = true; + + // Populate property images from the existing data + this.populatePropertyImages(); + } + + // Populate property images array + populatePropertyImages() { + this.propertyImages = []; + + // Add images from imagesByCategory + Object.keys(this.imagesByCategory).forEach(category => { + this.imagesByCategory[category].forEach(image => { + this.propertyImages.push({ + url: image.url, + name: image.title || image.name || `${category} Image`, + category: category.toLowerCase() + }); + }); + }); + + // Add real property images if available + if (this.realPropertyImages && this.realPropertyImages.length > 0) { + this.realPropertyImages.forEach(image => { + this.propertyImages.push({ + url: image.url || image.Url__c, + name: image.name || image.Name || 'Property Image', + category: (image.category || image.Category__c || 'none').toLowerCase() + }); + }); + } + + console.log('Property images populated:', this.propertyImages); + } + + // Close image insertion modal + closeImageModal() { + this.showImageModal = false; + this.selectedImageUrl = ''; + this.selectedImageName = ''; + this.uploadedImageData = ''; + this.insertButtonDisabled = true; + + // Clear any selections + document.querySelectorAll('.property-image-item').forEach(item => { + item.classList.remove('selected'); + }); + + // Reset upload area + this.resetUploadArea(); + } + + // Set image source (property or local) + setImageSource(event) { + const source = event.target.dataset.source; + this.imageSource = source; + this.selectedImageUrl = ''; + this.selectedImageName = ''; + this.uploadedImageData = ''; + this.insertButtonDisabled = true; + + // Clear any selections + document.querySelectorAll('.property-image-item').forEach(item => { + item.classList.remove('selected'); + }); + + // Reset upload area + this.resetUploadArea(); + } + + // Select image category + selectImageCategory(event) { + const category = event.target.dataset.category; + this.selectedImageCategory = category; + + // Update button states + document.querySelectorAll('.category-btn').forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.category === category) { + btn.classList.add('active'); + } + }); + } + + // Select property image + selectPropertyImage(event) { + // Get the image URL from the closest element with data-image-url + const imageItem = event.target.closest('[data-image-url]'); + const imageUrl = imageItem ? imageItem.dataset.imageUrl : null; + const imageName = event.target.alt || event.target.textContent || 'Property Image'; + + console.log('Property image selected:', imageUrl, imageName); + + if (!imageUrl) { + console.error('No image URL found in selected item'); + return; + } + + // Remove previous selection + document.querySelectorAll('.property-image-item').forEach(item => { + item.classList.remove('selected'); + }); + + // Add selection to clicked item + const targetItem = event.target.closest('.property-image-item'); + if (targetItem) { + targetItem.classList.add('selected'); + } + + // Force reactivity by creating new objects + this.selectedImageUrl = imageUrl; + this.selectedImageName = imageName; + this.uploadedImageData = ''; + this.insertButtonDisabled = false; + + console.log('After selection - selectedImageUrl:', this.selectedImageUrl); + console.log('Button disabled state:', this.insertButtonDisabled); + + // Log current state for debugging + this.logCurrentState(); + + // Reset upload area if we're on local tab + if (this.imageSource === 'local') { + this.resetUploadArea(); + } + + // Force a re-render by updating a tracked property + this.forceRerender(); + } + + // Reset upload area to default state + resetUploadArea() { + const uploadArea = this.template.querySelector('.upload-area'); + if (uploadArea) { + // Remove existing preview if any + const existingPreview = uploadArea.querySelector('.uploaded-image-preview'); + if (existingPreview) { + existingPreview.remove(); + } + + // Show upload content again + const uploadContent = uploadArea.querySelector('.upload-content'); + if (uploadContent) { + uploadContent.style.display = 'flex'; + } + } + } + + // Trigger file upload + triggerFileUpload() { + const fileInput = this.template.querySelector('.file-input'); + if (fileInput) { + fileInput.click(); + } + } + + // Handle file upload + handleFileUpload(event) { + const file = event.target.files[0]; + if (file) { + console.log('File selected:', file.name); + const reader = new FileReader(); + reader.onload = (e) => { + this.uploadedImageData = e.target.result; + this.selectedImageUrl = e.target.result; + this.selectedImageName = file.name; + this.insertButtonDisabled = false; + + console.log('Image loaded, selectedImageUrl:', this.selectedImageUrl); + console.log('Button disabled state:', this.insertButtonDisabled); + + // Log current state for debugging + this.logCurrentState(); + + // Update the upload area to show selected image + this.updateUploadAreaWithSelectedImage(e.target.result, file.name); + + // Force a re-render by updating a tracked property + this.forceRerender(); + }; + reader.readAsDataURL(file); + } + } + + // Update upload area to show selected image + updateUploadAreaWithSelectedImage(imageUrl, fileName) { + console.log('updateUploadAreaWithSelectedImage called with:', imageUrl, fileName); + const uploadArea = this.template.querySelector('.upload-area'); + console.log('Upload area found:', uploadArea); + if (uploadArea) { + // Remove existing preview if any + const existingPreview = uploadArea.querySelector('.uploaded-image-preview'); + if (existingPreview) { + existingPreview.remove(); + } + + // Create preview container + const previewContainer = document.createElement('div'); + previewContainer.className = 'uploaded-image-preview'; + previewContainer.style.cssText = ` + position: relative; + width: 100%; + max-width: 200px; + margin: 0 auto; + border-radius: 8px; + overflow: hidden; + border: 2px solid #4f46e5; + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2); + `; + + // Create image element + const img = document.createElement('img'); + img.src = imageUrl; + img.alt = fileName; + img.style.cssText = ` + width: 100%; + height: auto; + display: block; + max-height: 150px; + object-fit: cover; + `; + + // Create file name overlay + const fileNameOverlay = document.createElement('div'); + fileNameOverlay.style.cssText = ` + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); + color: white; + padding: 8px; + font-size: 12px; + font-weight: 500; + `; + fileNameOverlay.textContent = fileName; + + previewContainer.appendChild(img); + previewContainer.appendChild(fileNameOverlay); + + // Replace upload content with preview + const uploadContent = uploadArea.querySelector('.upload-content'); + if (uploadContent) { + uploadContent.style.display = 'none'; + } + + uploadArea.appendChild(previewContainer); + + // Add click handler to change image + uploadArea.onclick = () => { + this.triggerFileUpload(); + }; + } + } + + // Handle insert button click with debugging + handleInsertButtonClick() { + console.log('=== INSERT BUTTON CLICKED ==='); + this.logCurrentState(); + console.log('============================='); + this.insertSelectedImage(); + } + + // Insert selected image + insertSelectedImage() { + console.log('insertSelectedImage called'); + console.log('selectedImageUrl:', this.selectedImageUrl); + console.log('insertButtonDisabled:', this.insertButtonDisabled); + + // Check if we have a valid image URL + const imageUrl = this.selectedImageUrl || this.uploadedImageData; + const imageName = this.selectedImageName || 'Uploaded Image'; + + if (this.insertButtonDisabled || !imageUrl) { + console.error('Please select an image first'); + console.error('selectedImageUrl:', this.selectedImageUrl); + console.error('uploadedImageData:', this.uploadedImageData); + console.error('insertButtonDisabled:', this.insertButtonDisabled); + alert('Please select an image first'); + return; + } + + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + // Save undo state before making changes + this.saveUndoState(); + this.setupEditorClickHandler(); + + // Create draggable image container + const imageContainer = document.createElement('div'); + imageContainer.className = 'draggable-image-container'; + imageContainer.style.left = '50px'; + imageContainer.style.top = '50px'; + imageContainer.style.width = '200px'; + imageContainer.style.height = '150px'; + imageContainer.style.zIndex = '1000'; + imageContainer.style.position = 'absolute'; + imageContainer.style.overflow = 'hidden'; + imageContainer.style.border = '2px solid transparent'; + imageContainer.style.cursor = 'move'; + imageContainer.style.userSelect = 'none'; + + // Create image element + const img = document.createElement('img'); + img.src = imageUrl; + img.alt = imageName; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + img.style.display = 'block'; + + imageContainer.appendChild(img); + + // Add resize handles + this.addResizeHandles(imageContainer); + + // Add delete handle + this.addDeleteHandle(imageContainer); + + // Add drag functionality + this.makeDraggable(imageContainer); + + // Add click to select functionality + imageContainer.addEventListener('click', (e) => { + e.stopPropagation(); + this.selectDraggableElement(imageContainer); + }); + + // Select the image after a short delay + setTimeout(() => { + this.selectDraggableElement(imageContainer); + }, 100); + + editor.appendChild(imageContainer); + + // Close modal + this.closeImageModal(); + } + } + // Insert draggable image element insertDraggableImage() { const input = document.createElement('input'); @@ -3483,16 +5081,78 @@ export default class PropertyTemplateSelector extends LightningElement { this.currentImage = null; } - selectCategory(category) { - console.log('=== SELECTING CATEGORY ==='); - console.log('Category:', category); - this.selectedCategory = category; - this.currentImageIndex = 0; + selectCategory(event) { + let category; - // Get images for the selected category based on property data - const categoryImages = this.getImagesForCategory(category); - this.totalImages = categoryImages.length; - this.currentImage = this.totalImages > 0 ? categoryImages[0] : null; + // Handle both event and direct category parameter + if (typeof event === 'string') { + category = event; + } else if (event && event.currentTarget && event.currentTarget.dataset) { + category = event.currentTarget.dataset.category; + + // Update active category button + this.template.querySelectorAll('.category-btn-step2').forEach(btn => { + btn.classList.remove('active'); + }); + event.currentTarget.classList.add('active'); + } else { + console.error('Invalid category selection event'); + return; + } + + console.log('=== MANUAL CATEGORY SELECTION ==='); + console.log('Selected category:', category); + console.log('Previous category:', this.selectedCategory); + + this.selectedCategory = category; + console.log('Updated selectedCategory:', this.selectedCategory); + + // Filter real property images by category + this.filterImagesByCategory(category); + } + + // Add new method to filter images by category + filterImagesByCategory(category) { + console.log('=== FILTERING IMAGES BY CATEGORY ==='); + console.log('Category:', category); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No real property images available'); + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + return; + } + + // Filter images by category + const filteredImages = this.realPropertyImages.filter(img => { + const imgCategory = img.category || img.pcrm__Category__c; + console.log('Image category:', imgCategory, 'Selected category:', category); + + // Handle "None" category - show images with no category or empty category + if (category === 'None') { + return !imgCategory || imgCategory === '' || imgCategory === null || imgCategory === undefined || imgCategory === 'None'; + } + + return imgCategory === category; + }); + + console.log('Filtered images for category', category, ':', filteredImages); + + if (filteredImages.length > 0) { + this.currentImage = filteredImages[0]; + this.totalImages = filteredImages.length; + this.currentImageIndex = 0; + console.log('Set current image:', this.currentImage); + console.log('Total images:', this.totalImages); + console.log('Current index reset to 0 for category:', category); + } else { + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + console.log('No images found for category:', category); + } } getImagesForCategory(category) { @@ -3638,10 +5298,12 @@ export default class PropertyTemplateSelector extends LightningElement { console.log('=== NEXT IMAGE CLICKED ==='); console.log('Current index:', this.currentImageIndex); console.log('Total images:', this.totalImages); + console.log('Selected category:', this.selectedCategory); if (this.currentImageIndex < this.totalImages - 1) { this.currentImageIndex++; - const categoryImages = this.getImagesForCategory(this.selectedCategory); - this.currentImage = categoryImages[this.currentImageIndex]; + this.updateCurrentImage(); + } else { + console.log('Already at last image'); } } @@ -3649,10 +5311,37 @@ export default class PropertyTemplateSelector extends LightningElement { console.log('=== PREVIOUS IMAGE CLICKED ==='); console.log('Current index:', this.currentImageIndex); console.log('Total images:', this.totalImages); + console.log('Selected category:', this.selectedCategory); if (this.currentImageIndex > 0) { this.currentImageIndex--; - const categoryImages = this.getImagesForCategory(this.selectedCategory); - this.currentImage = categoryImages[this.currentImageIndex]; + this.updateCurrentImage(); + } else { + console.log('Already at first image'); + } + } + + // Add new method to update current image + updateCurrentImage() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return; + } + + // Use the same filtering logic as filterImagesByCategory + const filteredImages = this.realPropertyImages.filter(img => { + const imgCategory = img.category || img.pcrm__Category__c; + + // Handle "None" category - show images with no category or empty category + if (this.selectedCategory === 'None') { + return !imgCategory || imgCategory === '' || imgCategory === null || imgCategory === undefined || imgCategory === 'None'; + } + + return imgCategory === this.selectedCategory; + }); + + if (filteredImages.length > 0 && this.currentImageIndex < filteredImages.length) { + this.currentImage = filteredImages[this.currentImageIndex]; + console.log('Updated current image:', this.currentImage); + console.log('Current index:', this.currentImageIndex, 'of', filteredImages.length); } } @@ -3701,6 +5390,13 @@ export default class PropertyTemplateSelector extends LightningElement { renderedCallback() { this.ensureEditorEditable(); this.setupEditorClickHandler(); + + // Save initial state for undo functionality + setTimeout(() => { + this.saveUndoState(); + }, 100); + + } // Test editor functionality - can be called from toolbar @@ -3858,6 +5554,74 @@ export default class PropertyTemplateSelector extends LightningElement { return null; } + // Triple click handler for image replacement + handleImageClick(clickedImage, event) { + // Clear any existing timeout + if (this.clickTimeout) { + clearTimeout(this.clickTimeout); + } + + // Debug logging for image detection + console.log('Image detected:', { + tagName: clickedImage.tagName, + isBackgroundImage: clickedImage.isBackgroundImage, + src: clickedImage.src, + backgroundImage: clickedImage.style.backgroundImage, + originalElement: clickedImage.originalElement + }); + + // Check if this is the same image as the last click + const isSameImage = this.lastClickedImage && + ((this.lastClickedImage.src && clickedImage.src && this.lastClickedImage.src === clickedImage.src) || + (this.lastClickedImage.isBackgroundImage && clickedImage.isBackgroundImage && + this.lastClickedImage.style.backgroundImage === clickedImage.style.backgroundImage)); + + if (isSameImage) { + // Same image clicked, increment counter + this.imageClickCount++; + console.log(`Image clicked ${this.imageClickCount} times`); + } else { + // Different image clicked, reset counter + this.imageClickCount = 1; + this.lastClickedImage = clickedImage; + console.log('New image clicked, starting count'); + } + + // Set timeout to reset counter after 1 second + this.clickTimeout = setTimeout(() => { + this.imageClickCount = 0; + this.lastClickedImage = null; + console.log('Click counter reset due to timeout'); + }, 1000); + + // Check if we've reached exactly 3 clicks + if (this.imageClickCount === 3) { + console.log('Triple click detected! Opening image replacement popup'); + event.preventDefault(); + event.stopPropagation(); + this.openImageReplacement(clickedImage); + + // Reset counter after opening popup + this.imageClickCount = 0; + this.lastClickedImage = null; + if (this.clickTimeout) { + clearTimeout(this.clickTimeout); + this.clickTimeout = null; + } + } else { + // Show feedback for clicks 1 and 2, but don't open popup + if (this.imageClickCount === 1) { + this.showSuccess('Click 2 more times on the same image to replace it'); + } else if (this.imageClickCount === 2) { + this.showSuccess('Click 1 more time on the same image to replace it'); + } + + // Prevent any default behavior for clicks 1 and 2 + event.preventDefault(); + event.stopPropagation(); + } + } + // Image Replacement Methods openImageReplacement(imageElement) { if (!imageElement) { @@ -3870,18 +5634,35 @@ export default class PropertyTemplateSelector extends LightningElement { this.selectedImageElement = imageElement; this.showImageReplacement = true; this.replacementActiveTab = 'property'; - this.replacementSelectedCategory = 'Interior'; + + // Use smart category selection like Step 2 + this.replacementSelectedCategory = this.findFirstAvailableCategory(); + console.log('Opening image replacement with smart category selection:', this.replacementSelectedCategory); + this.uploadedImagePreview = null; this.filterReplacementImages(); + // Update category button states after filtering + setTimeout(() => { + const categoryButtons = this.template.querySelectorAll('.category-btn-step2'); + categoryButtons.forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.category === this.replacementSelectedCategory) { + btn.classList.add('active'); + } + }); + }, 100); + // Prevent body scrolling document.body.style.overflow = 'hidden'; // Log the selected image details for debugging if (imageElement.isBackgroundImage) { console.log('Background image selected for replacement:', imageElement.src); + console.log('Background image style:', imageElement.style.backgroundImage); } else if (imageElement.tagName === 'IMG') { console.log('IMG element selected for replacement:', imageElement.src); + console.log('IMG element classes:', imageElement.className); } else { console.log('Unknown image type selected:', imageElement); } @@ -3892,10 +5673,22 @@ export default class PropertyTemplateSelector extends LightningElement { this.selectedImageElement = null; this.uploadedImagePreview = null; + // Clear click tracking + this.resetImageClickTracking(); + // Restore body scrolling document.body.style.overflow = ''; } + resetImageClickTracking() { + this.imageClickCount = 0; + this.lastClickedImage = null; + if (this.clickTimeout) { + clearTimeout(this.clickTimeout); + this.clickTimeout = null; + } + } + selectPropertyImagesTab() { this.replacementActiveTab = 'property'; this.filterReplacementImages(); @@ -3907,11 +5700,15 @@ export default class PropertyTemplateSelector extends LightningElement { } selectReplacementCategory(event) { - this.replacementSelectedCategory = event.target.dataset.category; + const category = event.target.dataset.category; + console.log('=== REPLACEMENT CATEGORY SELECTION ==='); + console.log('Selected category:', category); + + this.replacementSelectedCategory = category; this.filterReplacementImages(); // Update active state for category buttons - const categoryButtons = this.template.querySelectorAll('.category-btn-replacement'); + const categoryButtons = this.template.querySelectorAll('.category-btn-step2'); categoryButtons.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.category === this.replacementSelectedCategory) { @@ -3921,12 +5718,35 @@ export default class PropertyTemplateSelector extends LightningElement { } filterReplacementImages() { - const categoryImages = this.imagesByCategory[this.replacementSelectedCategory] || []; - this.filteredReplacementImages = categoryImages.map((img, index) => ({ + console.log('=== FILTERING REPLACEMENT IMAGES ==='); + console.log('Category:', this.replacementSelectedCategory); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No real property images available for replacement'); + this.filteredReplacementImages = []; + return; + } + + // Filter images by category using the same logic as Step 2 + const filteredImages = this.realPropertyImages.filter(img => { + const imgCategory = img.category || img.pcrm__Category__c; + + // Handle "None" category - show images with no category or empty category + if (this.replacementSelectedCategory === 'None') { + return !imgCategory || imgCategory === '' || imgCategory === null || imgCategory === undefined || imgCategory === 'None'; + } + + return imgCategory === this.replacementSelectedCategory; + }); + + console.log('Filtered replacement images:', filteredImages.length); + + this.filteredReplacementImages = filteredImages.map((img, index) => ({ id: `${this.replacementSelectedCategory}-${index}`, url: img.url, - title: img.title, - category: this.replacementSelectedCategory + title: img.title || img.name || `Image ${index + 1}`, + category: img.category || img.pcrm__Category__c || 'None' })); } @@ -4004,9 +5824,36 @@ export default class PropertyTemplateSelector extends LightningElement { // Handle background images if (this.selectedImageElement.isBackgroundImage) { - this.selectedImageElement.element.style.backgroundImage = `url("${newImageUrl}")`; - console.log('Background image replaced successfully:', newImageUrl); + // Use the stored original element reference if available + if (this.selectedImageElement.originalElement) { + this.selectedImageElement.originalElement.style.backgroundImage = `url("${newImageUrl}")`; + console.log('Background image replaced successfully using original element:', newImageUrl); this.showSuccess('Background image updated successfully!'); + return; + } + + // Fallback: Find the actual DOM element that has the background image + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + // Find all elements with background images and update the one that matches + const allElements = editor.querySelectorAll('*'); + for (let element of allElements) { + const computedStyle = window.getComputedStyle(element); + const currentBgImage = computedStyle.backgroundImage; + if (currentBgImage && currentBgImage !== 'none') { + // Check if this is the element we want to update + const currentBgUrl = currentBgImage.replace(/url\(['"]?(.+?)['"]?\)/, '$1'); + if (currentBgUrl === this.selectedImageElement.src) { + element.style.backgroundImage = `url("${newImageUrl}")`; + console.log('Background image replaced successfully via fallback:', newImageUrl); + this.showSuccess('Background image updated successfully!'); + return; + } + } + } + } + console.error('Could not find the background image element to replace'); + this.showError('Failed to update background image. Please try again.'); return; } @@ -4267,11 +6114,13 @@ export default class PropertyTemplateSelector extends LightningElement { // Table Dialog Methods openTableDialog() { + console.log('Opening table dialog'); this.showTableDialog = true; document.body.style.overflow = 'hidden'; } closeTableDialog() { + console.log('Closing table dialog'); this.showTableDialog = false; document.body.style.overflow = ''; } @@ -4295,52 +6144,734 @@ export default class PropertyTemplateSelector extends LightningElement { return; } - // Save undo state before making changes + // Save undo state this.saveUndoState(); - // Create table HTML - let tableHtml = ''; + // Create table element using our new method + const tableContainer = this.createTableElement(); - // Add header row if requested - if (this.includeHeader) { - tableHtml += ''; - for (let col = 0; col < this.tableCols; col++) { - tableHtml += ``; - } - tableHtml += ''; - } - - // Add body rows - tableHtml += ''; - const startRow = this.includeHeader ? 1 : 0; - const totalRows = this.includeHeader ? this.tableRows + 1 : this.tableRows; - - for (let row = startRow; row < totalRows; row++) { - tableHtml += ''; - for (let col = 0; col < this.tableCols; col++) { - tableHtml += ``; - } - tableHtml += ''; - } - tableHtml += '
Header ${col + 1}
Cell ${row + 1}-${col + 1}
'; - - // Insert table into editor + // Get current cursor position const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); + // Insert at cursor position range.deleteContents(); - const tableElement = document.createElement('div'); - tableElement.innerHTML = tableHtml; - range.insertNode(tableElement.firstChild); + range.insertNode(tableContainer); } else { - // If no selection, append to editor - editor.innerHTML += tableHtml; + // If no selection, insert at the end + editor.appendChild(tableContainer); } - + this.closeTableDialog(); this.showSuccess('Table inserted successfully!'); } + // ===== DYNAMIC IMAGE REPLACEMENT UTILITIES ===== + + // Get first image from a specific category + getFirstImageByCategory(category) { + console.log('=== GET FIRST IMAGE BY CATEGORY ==='); + console.log('Category:', category); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No real property images available'); + return null; + } + + const categoryImages = this.realPropertyImages.filter(img => { + const imgCategory = img.category || img.pcrm__Category__c; + return imgCategory && imgCategory.toLowerCase() === category.toLowerCase(); + }); + + console.log('Category images found:', categoryImages.length); + console.log('First category image:', categoryImages[0]); + + return categoryImages.length > 0 ? categoryImages[0] : null; + } + + // Direct method to get exterior image URL + getExteriorImageUrl() { + console.log('=== GET EXTERIOR IMAGE URL ==='); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No property images, using fallback'); + return 'https://images.unsplash.com/photo-1568605114967-8130f3a36994?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'; + } + + // Look for exterior images first + const exteriorImages = this.realPropertyImages.filter(img => { + const category = img.category || img.pcrm__Category__c; + return category && category.toLowerCase().includes('exterior'); + }); + + if (exteriorImages.length > 0) { + console.log('Found exterior image:', exteriorImages[0].url); + return exteriorImages[0].url; + } + + // If no exterior, use first available image + if (this.realPropertyImages.length > 0) { + console.log('No exterior image, using first available:', this.realPropertyImages[0].url); + return this.realPropertyImages[0].url; + } + + console.log('No images available, using fallback'); + return 'https://images.unsplash.com/photo-1568605114967-8130f3a36994?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'; + } + + // Direct method to get maps image URL + getMapsImageUrl() { + console.log('=== GET MAPS IMAGE URL ==='); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No property images, using fallback'); + return 'https://images.unsplash.com/photo-1524661135-423995f22d0b?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'; + } + + // Look for maps images first - check both exact match and contains + const mapsImages = this.realPropertyImages.filter(img => { + const category = img.category || img.pcrm__Category__c; + console.log('Checking image category:', category, 'for maps'); + return category && (category.toLowerCase() === 'maps' || category.toLowerCase().includes('maps')); + }); + + console.log('Maps images found:', mapsImages.length); + console.log('Maps images:', mapsImages); + + if (mapsImages.length > 0) { + console.log('Found maps image:', mapsImages[0].url); + return mapsImages[0].url; + } + + // Look for anchor images as fallback + const anchorImages = this.realPropertyImages.filter(img => { + const category = img.category || img.pcrm__Category__c; + console.log('Checking image category:', category, 'for anchor'); + return category && (category.toLowerCase() === 'anchor' || category.toLowerCase().includes('anchor')); + }); + + console.log('Anchor images found:', anchorImages.length); + + if (anchorImages.length > 0) { + console.log('No maps image, using anchor image:', anchorImages[0].url); + return anchorImages[0].url; + } + + console.log('No maps or anchor images available, using fallback'); + return 'https://images.unsplash.com/photo-1524661135-423995f22d0b?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'; + } + + // Method to replace background-image URLs in CSS at runtime + replaceBackgroundImagesInHTML(htmlContent) { + console.log('=== REPLACING BACKGROUND IMAGES IN HTML ==='); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No property images available for replacement'); + return htmlContent; + } + + const exteriorImageUrl = this.getExteriorImageUrl(); + console.log('Using exterior image URL for replacement:', exteriorImageUrl); + + // Replace any hardcoded background-image URLs with the property's exterior image + let updatedHTML = htmlContent; + + // Pattern to match background-image: url('...') or background-image: url("...") + const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; + + // Replace all background-image URLs with the property's exterior image + updatedHTML = updatedHTML.replace(backgroundImagePattern, `background-image: url('${exteriorImageUrl}')`); + + console.log('Background images replaced in HTML'); + return updatedHTML; + } + + // Method to dynamically update CSS background-image rules after template loads + updateCSSBackgroundImages() { + console.log('=== UPDATING CSS BACKGROUND IMAGES ==='); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No property images available for CSS update'); + return; + } + + const exteriorImageUrl = this.getExteriorImageUrl(); + console.log('Updating CSS with exterior image URL:', exteriorImageUrl); + + // Find all style elements in the document + const styleElements = document.querySelectorAll('style'); + styleElements.forEach(styleElement => { + let cssText = styleElement.textContent; + + // Replace background-image URLs in CSS + const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; + const updatedCSS = cssText.replace(backgroundImagePattern, `background-image: url('${exteriorImageUrl}')`); + + if (updatedCSS !== cssText) { + styleElement.textContent = updatedCSS; + console.log('Updated CSS background-image rules'); + } + }); + + // Also update any elements with inline background-image styles + const elementsWithBackground = document.querySelectorAll('[style*="background-image"]'); + elementsWithBackground.forEach(element => { + const currentStyle = element.getAttribute('style'); + const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; + const updatedStyle = currentStyle.replace(backgroundImagePattern, `background-image: url('${exteriorImageUrl}')`); + + if (updatedStyle !== currentStyle) { + element.setAttribute('style', updatedStyle); + console.log('Updated inline background-image style'); + } + }); + } + + // Get all images from a specific category + getAllImagesByCategory(category) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return []; + } + + return this.realPropertyImages.filter(img => + img.category && img.category.toLowerCase() === category.toLowerCase() + ); + } + + // Get uncategorized images (no category or category is null/empty) + getUncategorizedImages() { + console.log('=== GET UNCATEGORIZED IMAGES ==='); + console.log('Real property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No real property images available'); + return []; + } + + const uncategorized = this.realPropertyImages.filter(img => + !img.category || img.category.trim() === '' || img.category.toLowerCase() === 'none' + ); + + console.log('Uncategorized images found:', uncategorized.length); + console.log('Uncategorized images:', uncategorized); + + return uncategorized; + } + + // Smart image replacement - tries multiple categories in order of preference + getSmartImageForSection(sectionType, fallbackUrl) { + console.log('=== GET SMART IMAGE FOR SECTION ==='); + console.log('Section type:', sectionType); + console.log('Fallback URL:', fallbackUrl); + console.log('Real property images available:', this.realPropertyImages?.length || 0); + + const categoryPriority = { + 'exterior': ['Exterior', 'Anchor', 'None'], + 'interior': ['Interior', 'Living Area', 'Kitchen', 'Bedroom', 'None'], + 'kitchen': ['Kitchen', 'Interior', 'Living Area', 'None'], + 'bedroom': ['Bedroom', 'Interior', 'None'], + 'living': ['Living Area', 'Interior', 'Kitchen', 'None'], + 'bathroom': ['Bathroom', 'Interior', 'None'], + 'parking': ['Parking', 'Exterior', 'None'], + 'maps': ['Maps', 'Anchor', 'Exterior', 'None'], + 'gallery': ['Interior', 'Exterior', 'Kitchen', 'Bedroom', 'Living Area', 'None'] + }; + + const categories = categoryPriority[sectionType] || ['Interior', 'Exterior', 'None']; + console.log('Categories to try:', categories); + + for (const category of categories) { + const image = this.getFirstImageByCategory(category); + if (image && image.url) { + console.log('Found image for category:', category, 'URL:', image.url); + return image.url; + } + } + + console.log('No property image found, using fallback:', fallbackUrl); + return fallbackUrl; + } + + // Generate Property Gallery HTML for uncategorized images + generatePropertyGalleryHTML() { + console.log('=== GENERATING PROPERTY GALLERY HTML ==='); + console.log('All property images:', this.realPropertyImages); + + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + console.log('No property images available, returning empty gallery'); + return '
No images available
'; + } + + console.log('Property images found:', this.realPropertyImages.length); + + let galleryHTML = ''; + + this.realPropertyImages.forEach((image, index) => { + const category = image.category || image.pcrm__Category__c || 'Uncategorized'; + const title = image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; + + galleryHTML += ` +
+ ${title} +
+
${title}
+
${category}
+
+
+ `; + }); + + console.log('Generated gallery HTML length:', galleryHTML.length); + return galleryHTML; + } + + // ===== TABLE DRAG AND DROP FUNCTIONALITY ===== + + // Handle table drag start + handleTableDragStart(event) { + console.log('Starting table drag'); + this.isDraggingTable = true; + + // Store table configuration data + this.draggedTableData = { + rows: this.tableRows, + cols: this.tableCols, + includeHeader: this.includeHeader + }; + + // Set drag data + event.dataTransfer.setData('text/plain', 'table'); + event.dataTransfer.effectAllowed = 'copy'; + + // Add visual feedback + event.currentTarget.classList.add('dragging'); + + // Add drag over class to editor + setTimeout(() => { + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + editor.classList.add('drag-over'); + } + }, 100); + } + + // Handle editor drag over + handleEditorDragOver(event) { + if (this.isDraggingTable) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + } + } + + // Handle editor drop + handleEditorDrop(event) { + event.preventDefault(); + + if (!this.isDraggingTable || !this.draggedTableData) { + return; + } + + console.log('Dropping table at position:', event.clientX, event.clientY); + + // Remove visual feedback + this.removeTableDragFeedback(); + + // Get drop position + const editor = this.template.querySelector('.enhanced-editor-content'); + if (!editor) { + this.showError("Editor not found"); + return; + } + + // Save undo state before making changes + this.saveUndoState(); + + // Insert table at drop position + this.insertTableAtPosition(editor, this.draggedTableData, event); + + // Reset drag state + this.isDraggingTable = false; + this.draggedTableData = null; + + this.showSuccess('Table inserted via drag and drop!'); + } + + + + // Insert table at specific position + insertTableAtPosition(editor, tableData, event) { + // Get cursor position relative to editor + const rect = editor.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // Create table element directly using DOM methods (same as insertTable) + const tableId = `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // Create container div + const container = document.createElement('div'); + container.className = 'editable-table-container'; + container.setAttribute('data-table-id', tableId); + container.setAttribute('draggable', 'true'); + container.style.cssText = 'border: 2px solid #4f46e5; margin: 20px 0; padding: 10px; background-color: #f8f9fa; position: relative;'; + + // Create table controls + const controls = document.createElement('div'); + controls.className = 'table-controls'; + controls.style.cssText = 'position: absolute; top: -40px; left: 0; background: white; padding: 5px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); opacity: 0; transition: opacity 0.2s;'; + + // Add control buttons (same as insertTable) + const controlGroup1 = document.createElement('div'); + controlGroup1.className = 'table-control-group'; + controlGroup1.style.cssText = 'display: flex; gap: 5px;'; + + const addRowBtn = document.createElement('button'); + addRowBtn.className = 'table-control-btn'; + addRowBtn.setAttribute('data-table-id', tableId); + addRowBtn.textContent = '+ Row'; + addRowBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;'; + + const addColBtn = document.createElement('button'); + addColBtn.className = 'table-control-btn'; + addColBtn.setAttribute('data-table-id', tableId); + addColBtn.textContent = '+ Col'; + addColBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;'; + + const delRowBtn = document.createElement('button'); + delRowBtn.className = 'table-control-btn'; + delRowBtn.setAttribute('data-table-id', tableId); + delRowBtn.textContent = '- Row'; + delRowBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;'; + + const delColBtn = document.createElement('button'); + delColBtn.className = 'table-control-btn'; + delColBtn.setAttribute('data-table-id', tableId); + delColBtn.textContent = '- Col'; + delColBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;'; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'table-control-btn delete'; + deleteBtn.setAttribute('data-table-id', tableId); + deleteBtn.textContent = '🗑️'; + deleteBtn.style.cssText = 'padding: 4px 8px; border: 1px solid #ff4444; background: #ff4444; color: white; cursor: pointer;'; + + controlGroup1.appendChild(addRowBtn); + controlGroup1.appendChild(addColBtn); + controlGroup1.appendChild(delRowBtn); + controlGroup1.appendChild(delColBtn); + + const controlGroup2 = document.createElement('div'); + controlGroup2.className = 'table-control-group'; + controlGroup2.style.cssText = 'display: flex; gap: 5px; margin-left: 10px;'; + controlGroup2.appendChild(deleteBtn); + + controls.appendChild(controlGroup1); + controls.appendChild(controlGroup2); + + // Create table + const table = document.createElement('table'); + table.className = 'inserted-table'; + table.id = tableId; + table.style.cssText = 'border-collapse: collapse; width: 100%; margin: 1rem 0; border: 2px solid #333; background-color: white;'; + + // Create table body + const tbody = document.createElement('tbody'); + + // Add header row if requested + if (tableData.includeHeader) { + const thead = document.createElement('thead'); + const headerRow = document.createElement('tr'); + + for (let col = 0; col < tableData.cols; col++) { + const th = document.createElement('th'); + th.style.cssText = 'border: 1px solid #333; padding: 12px; background-color: #4f46e5; color: white; font-weight: bold; text-align: center;'; + th.setAttribute('contenteditable', 'true'); + th.textContent = `Header ${col + 1}`; + headerRow.appendChild(th); + } + + thead.appendChild(headerRow); + table.appendChild(thead); + } + + // Add body rows + for (let row = 0; row < tableData.rows; row++) { + const tr = document.createElement('tr'); + + for (let col = 0; col < tableData.cols; col++) { + const td = document.createElement('td'); + td.style.cssText = 'border: 1px solid #333; padding: 12px; background-color: #f8f9fa; min-width: 100px; min-height: 40px;'; + td.setAttribute('contenteditable', 'true'); + td.textContent = `Cell ${row + 1}-${col + 1}`; + tr.appendChild(td); + } + + tbody.appendChild(tr); + } + + table.appendChild(tbody); + + // Assemble the container + container.appendChild(controls); + container.appendChild(table); + + const tableElement = container; + + // Try to find the best insertion point + const range = document.createRange(); + const walker = document.createTreeWalker( + editor, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let bestNode = null; + let bestDistance = Infinity; + let node; + + // Find the closest text node to the drop position + while (node = walker.nextNode()) { + const nodeRect = node.getBoundingClientRect(); + const nodeX = nodeRect.left - rect.left; + const nodeY = nodeRect.top - rect.top; + const distance = Math.sqrt((x - nodeX) ** 2 + (y - nodeY) ** 2); + + if (distance < bestDistance) { + bestDistance = distance; + bestNode = node; + } + } + + if (bestNode) { + // Insert after the closest text node + range.setStartAfter(bestNode); + range.insertNode(tableElement); + } else { + // Fallback: append to editor + editor.appendChild(tableElement); + } + + // Add event listeners to the new table + this.setupTableEventListeners(tableElement); + } + + // Remove table drag feedback + removeTableDragFeedback() { + // Remove dragging class from button + const tableBtn = this.template.querySelector('.draggable-table-btn'); + if (tableBtn) { + tableBtn.classList.remove('dragging'); + } + + // Remove drag-over class from editor + const editor = this.template.querySelector('.enhanced-editor-content'); + if (editor) { + editor.classList.remove('drag-over'); + } + } + + // ===== TABLE EDITING FUNCTIONALITY ===== + + // Add row to table + addTableRow(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + this.saveUndoState(); + + const tbody = table.querySelector('tbody'); + const firstRow = tbody.querySelector('tr'); + if (!firstRow) return; + + const newRow = firstRow.cloneNode(true); + const cells = newRow.querySelectorAll('td, th'); + cells.forEach((cell, index) => { + cell.textContent = `Cell ${tbody.children.length + 1}-${index + 1}`; + }); + + tbody.appendChild(newRow); + this.showSuccess('Row added successfully!'); + } + + // Add column to table + addTableColumn(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + this.saveUndoState(); + + const rows = table.querySelectorAll('tr'); + rows.forEach((row, rowIndex) => { + const newCell = document.createElement(row.cells[0].tagName.toLowerCase()); + newCell.style.border = '1px solid #ddd'; + newCell.style.padding = '8px'; + newCell.contentEditable = 'true'; + + if (row.cells[0].tagName === 'TH') { + newCell.style.backgroundColor = '#f2f2f2'; + newCell.style.fontWeight = 'bold'; + newCell.textContent = `Header ${row.cells.length + 1}`; + } else { + newCell.textContent = `Cell ${rowIndex}-${row.cells.length + 1}`; + } + + row.appendChild(newCell); + }); + + this.showSuccess('Column added successfully!'); + } + + // Delete row from table + deleteTableRow(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + const tbody = table.querySelector('tbody'); + if (tbody.children.length <= 1) { + this.showError('Cannot delete the last row!'); + return; + } + + this.saveUndoState(); + tbody.removeChild(tbody.lastChild); + this.showSuccess('Row deleted successfully!'); + } + + // Delete column from table + deleteTableColumn(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + const firstRow = table.querySelector('tr'); + if (!firstRow || firstRow.cells.length <= 1) { + this.showError('Cannot delete the last column!'); + return; + } + + this.saveUndoState(); + + const rows = table.querySelectorAll('tr'); + rows.forEach(row => { + if (row.cells.length > 0) { + row.removeChild(row.lastChild); + } + }); + + this.showSuccess('Column deleted successfully!'); + } + + // Delete entire table + deleteTable(event) { + const tableId = event.currentTarget.dataset.tableId; + const tableContainer = document.querySelector(`[data-table-id="${tableId}"]`); + if (!tableContainer) return; + + this.saveUndoState(); + tableContainer.remove(); + this.showSuccess('Table deleted successfully!'); + } + + // Handle table drag start (for moving tables) + handleTableContainerDragStart(event) { + if (event.target.classList.contains('table-control-btn')) { + event.preventDefault(); + return; + } + + const tableId = event.currentTarget.dataset.tableId; + console.log('Starting table container drag:', tableId); + + event.dataTransfer.setData('text/plain', 'table-container'); + event.dataTransfer.setData('table-id', tableId); + event.dataTransfer.effectAllowed = 'move'; + + // Add visual feedback + event.currentTarget.style.opacity = '0.5'; + event.currentTarget.style.transform = 'rotate(2deg)'; + } + + // Handle table container drag end + handleTableContainerDragEnd(event) { + event.currentTarget.style.opacity = '1'; + event.currentTarget.style.transform = 'rotate(0deg)'; + } + + // Setup event listeners for table controls + setupTableEventListeners(tableContainer) { + const tableId = tableContainer.dataset.tableId; + + // Add row button + const addRowBtn = tableContainer.querySelector('.table-control-btn[title="Add Row"]'); + if (addRowBtn) { + addRowBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.addTableRow(e); + }); + } + + // Add column button + const addColBtn = tableContainer.querySelector('.table-control-btn[title="Add Column"]'); + if (addColBtn) { + addColBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.addTableColumn(e); + }); + } + + // Delete row button + const deleteRowBtn = tableContainer.querySelector('.table-control-btn[title="Delete Row"]'); + if (deleteRowBtn) { + deleteRowBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.deleteTableRow(e); + }); + } + + // Delete column button + const deleteColBtn = tableContainer.querySelector('.table-control-btn[title="Delete Column"]'); + if (deleteColBtn) { + deleteColBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.deleteTableColumn(e); + }); + } + + // Delete table button + const deleteTableBtn = tableContainer.querySelector('.table-control-btn[title="Delete Table"]'); + if (deleteTableBtn) { + deleteTableBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.deleteTable(e); + }); + } + + // Drag and drop for table container + tableContainer.addEventListener('dragstart', (e) => { + this.handleTableContainerDragStart(e); + }); + + tableContainer.addEventListener('dragend', (e) => { + this.handleTableContainerDragEnd(e); + }); + + // Prevent drag on control buttons + const controlButtons = tableContainer.querySelectorAll('.table-control-btn'); + controlButtons.forEach(btn => { + btn.addEventListener('dragstart', (e) => { + e.preventDefault(); + }); + }); + } + // Improved text insertion that's draggable anywhere insertDraggableText() { const editor = this.template.querySelector('.enhanced-editor-content'); @@ -4461,6 +6992,66 @@ export default class PropertyTemplateSelector extends LightningElement { console.log('Redo performed'); } + // Setup editor event handlers after undo/redo + setupEditorEventHandlers() { + this.setupEditorClickHandler(); + this.ensureEditorEditable(); + } + + // Find the first available category that has images + findFirstAvailableCategory() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return 'None'; + } + + // Define the order of categories to check + const categoryOrder = ['Interior', 'Exterior', 'Kitchen', 'Bedroom', 'Living Area', 'Parking', 'Anchor', 'Maps', 'None']; + + // Check each category in order + for (let category of categoryOrder) { + const hasImages = this.realPropertyImages.some(img => { + const imgCategory = img.category || img.pcrm__Category__c; + + if (category === 'None') { + return !imgCategory || imgCategory === '' || imgCategory === null || imgCategory === undefined || imgCategory === 'None'; + } + + return imgCategory === category; + }); + + if (hasImages) { + console.log(`Found images in category: ${category}`); + return category; + } + } + + // Fallback to None if no specific category has images + return 'None'; + } + + // Ensure smart category selection only on initial load + ensureSmartCategorySelection() { + // Only run if initial category selection hasn't been done yet + if (!this.initialCategorySelected && this.realPropertyImages && this.realPropertyImages.length > 0) { + const firstAvailableCategory = this.findFirstAvailableCategory(); + console.log('Initial smart category selection:', firstAvailableCategory); + this.selectedCategory = firstAvailableCategory; + this.filterImagesByCategory(firstAvailableCategory); + this.initialCategorySelected = true; + + // Update button states + const categoryButtons = this.template.querySelectorAll('.category-btn-step2'); + categoryButtons.forEach(btn => { + btn.classList.remove('active'); + if (btn.dataset.category === firstAvailableCategory) { + btn.classList.add('active'); + } + }); + } + } + + + // Enhanced keyboard event handler handleEditorKeydown(event) { // Check for Ctrl+Z (Undo) @@ -4488,6 +7079,240 @@ export default class PropertyTemplateSelector extends LightningElement { } } + // Enhanced image manipulation methods + addResizeHandles(container) { + const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'w', 'e']; + + handles.forEach(handle => { + const resizeHandle = document.createElement('div'); + resizeHandle.className = `resize-handle ${handle}`; + resizeHandle.style.position = 'absolute'; + resizeHandle.style.background = '#4f46e5'; + resizeHandle.style.border = '2px solid white'; + resizeHandle.style.borderRadius = '50%'; + resizeHandle.style.width = '12px'; + resizeHandle.style.height = '12px'; + resizeHandle.style.zIndex = '1001'; + + // Position handles + switch(handle) { + case 'nw': resizeHandle.style.top = '-6px'; resizeHandle.style.left = '-6px'; resizeHandle.style.cursor = 'nw-resize'; break; + case 'ne': resizeHandle.style.top = '-6px'; resizeHandle.style.right = '-6px'; resizeHandle.style.cursor = 'ne-resize'; break; + case 'sw': resizeHandle.style.bottom = '-6px'; resizeHandle.style.left = '-6px'; resizeHandle.style.cursor = 'sw-resize'; break; + case 'se': resizeHandle.style.bottom = '-6px'; resizeHandle.style.right = '-6px'; resizeHandle.style.cursor = 'se-resize'; break; + case 'n': resizeHandle.style.top = '-6px'; resizeHandle.style.left = '50%'; resizeHandle.style.transform = 'translateX(-50%)'; resizeHandle.style.cursor = 'n-resize'; break; + case 's': resizeHandle.style.bottom = '-6px'; resizeHandle.style.left = '50%'; resizeHandle.style.transform = 'translateX(-50%)'; resizeHandle.style.cursor = 's-resize'; break; + case 'w': resizeHandle.style.top = '50%'; resizeHandle.style.left = '-6px'; resizeHandle.style.transform = 'translateY(-50%)'; resizeHandle.style.cursor = 'w-resize'; break; + case 'e': resizeHandle.style.top = '50%'; resizeHandle.style.right = '-6px'; resizeHandle.style.transform = 'translateY(-50%)'; resizeHandle.style.cursor = 'e-resize'; break; + } + + // Add resize functionality + this.addResizeFunctionality(resizeHandle, container, handle); + + container.appendChild(resizeHandle); + }); + } + + // Add delete handle to image + addDeleteHandle(container) { + const deleteHandle = document.createElement('button'); + deleteHandle.className = 'delete-handle'; + deleteHandle.innerHTML = '×'; + deleteHandle.style.position = 'absolute'; + deleteHandle.style.top = '-8px'; + deleteHandle.style.right = '-8px'; + deleteHandle.style.background = '#ef4444'; + deleteHandle.style.color = 'white'; + deleteHandle.style.border = 'none'; + deleteHandle.style.borderRadius = '50%'; + deleteHandle.style.width = '20px'; + deleteHandle.style.height = '20px'; + deleteHandle.style.fontSize = '12px'; + deleteHandle.style.cursor = 'pointer'; + deleteHandle.style.zIndex = '1002'; + deleteHandle.style.display = 'flex'; + deleteHandle.style.alignItems = 'center'; + deleteHandle.style.justifyContent = 'center'; + deleteHandle.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.2)'; + + deleteHandle.addEventListener('click', (e) => { + e.stopPropagation(); + this.saveUndoState(); + container.remove(); + }); + + deleteHandle.addEventListener('mouseenter', () => { + deleteHandle.style.background = '#dc2626'; + deleteHandle.style.transform = 'scale(1.1)'; + }); + + deleteHandle.addEventListener('mouseleave', () => { + deleteHandle.style.background = '#ef4444'; + deleteHandle.style.transform = 'scale(1)'; + }); + + container.appendChild(deleteHandle); + } + + // Add resize functionality to handle + addResizeFunctionality(handle, container, direction) { + let isResizing = false; + let startX, startY, startWidth, startHeight, startLeft, startTop; + + handle.addEventListener('mousedown', (e) => { + e.stopPropagation(); + isResizing = true; + + startX = e.clientX; + startY = e.clientY; + startWidth = parseInt(window.getComputedStyle(container).width, 10); + startHeight = parseInt(window.getComputedStyle(container).height, 10); + startLeft = parseInt(window.getComputedStyle(container).left, 10); + startTop = parseInt(window.getComputedStyle(container).top, 10); + + document.addEventListener('mousemove', handleResize); + document.addEventListener('mouseup', stopResize); + }); + + const handleResize = (e) => { + if (!isResizing) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newWidth = startWidth; + let newHeight = startHeight; + let newLeft = startLeft; + let newTop = startTop; + + switch(direction) { + case 'se': + newWidth = Math.max(50, startWidth + deltaX); + newHeight = Math.max(50, startHeight + deltaY); + break; + case 'sw': + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max(50, startHeight + deltaY); + newLeft = startLeft + (startWidth - newWidth); + break; + case 'ne': + newWidth = Math.max(50, startWidth + deltaX); + newHeight = Math.max(50, startHeight - deltaY); + newTop = startTop + (startHeight - newHeight); + break; + case 'nw': + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max(50, startHeight - deltaY); + newLeft = startLeft + (startWidth - newWidth); + newTop = startTop + (startHeight - newHeight); + break; + case 'e': + newWidth = Math.max(50, startWidth + deltaX); + break; + case 'w': + newWidth = Math.max(50, startWidth - deltaX); + newLeft = startLeft + (startWidth - newWidth); + break; + case 's': + newHeight = Math.max(50, startHeight + deltaY); + break; + case 'n': + newHeight = Math.max(50, startHeight - deltaY); + newTop = startTop + (startHeight - newHeight); + break; + } + + container.style.width = newWidth + 'px'; + container.style.height = newHeight + 'px'; + container.style.left = newLeft + 'px'; + container.style.top = newTop + 'px'; + }; + + const stopResize = () => { + isResizing = false; + document.removeEventListener('mousemove', handleResize); + document.removeEventListener('mouseup', stopResize); + }; + } + + // Make element draggable (enhanced version) + makeDraggable(element) { + let isDragging = false; + let startX, startY, startLeft, startTop; + + element.addEventListener('mousedown', (e) => { + // Don't start drag if clicking on resize handles or delete button + if (e.target.classList.contains('resize-handle') || e.target.classList.contains('delete-handle')) { + return; + } + + isDragging = true; + startX = e.clientX; + startY = e.clientY; + startLeft = parseInt(window.getComputedStyle(element).left, 10); + startTop = parseInt(window.getComputedStyle(element).top, 10); + + element.style.cursor = 'grabbing'; + document.addEventListener('mousemove', handleDrag); + document.addEventListener('mouseup', stopDrag); + }); + + const handleDrag = (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + element.style.left = (startLeft + deltaX) + 'px'; + element.style.top = (startTop + deltaY) + 'px'; + }; + + const stopDrag = () => { + isDragging = false; + element.style.cursor = 'move'; + document.removeEventListener('mousemove', handleDrag); + document.removeEventListener('mouseup', stopDrag); + }; + } + + // Select draggable element + selectDraggableElement(element) { + // Remove selection from all draggable elements + document.querySelectorAll('.draggable-image-container, .draggable-table-container').forEach(el => { + el.classList.remove('selected'); + }); + + // Add selection to clicked element + element.classList.add('selected'); + } + + // Force re-render by updating a tracked property + forceRerender() { + // Update a dummy property to force reactivity + this.renderKey = this.renderKey ? this.renderKey + 1 : 1; + } + + // Debug method to log current state + logCurrentState() { + console.log('=== CURRENT STATE ==='); + console.log('selectedImageUrl:', this.selectedImageUrl); + console.log('selectedImageName:', this.selectedImageName); + console.log('uploadedImageData:', this.uploadedImageData); + console.log('insertButtonDisabled:', this.insertButtonDisabled); + console.log('imageSource:', this.imageSource); + console.log('propertyImages length:', this.propertyImages ? this.propertyImages.length : 0); + console.log('===================='); + } + + // Test method to manually set an image (for debugging) + testSetImage() { + console.log('Testing manual image set'); + this.selectedImageUrl = 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200'; + this.selectedImageName = 'Test Image'; + this.insertButtonDisabled = false; + this.logCurrentState(); + } + connectedCallback() { this.loadSavedTemplates(); } diff --git a/python-pdf-generator/app.py b/python-pdf-generator/app.py deleted file mode 100644 index 351217e..0000000 --- a/python-pdf-generator/app.py +++ /dev/null @@ -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 '' in html_content: - html_content = html_content.replace('', continuous_flow_css + '\n ') - - 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 '' in html_content: - html_content = html_content.replace('', pdf_optimizations + '\n ') - - 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) \ No newline at end of file diff --git a/python-pdf-generator/requirements.txt b/python-pdf-generator/requirements.txt deleted file mode 100644 index 858f4c6..0000000 --- a/python-pdf-generator/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ - - -Flask==2.3.3 -playwright==1.40.0 -requests==2.31.0 -Werkzeug==2.3.7 -gunicorn==21.2.0 \ No newline at end of file diff --git a/sample templates/real estate modern home.html b/sample templates/real estate modern home.html deleted file mode 100644 index b36870f..0000000 --- a/sample templates/real estate modern home.html +++ /dev/null @@ -1,477 +0,0 @@ - - - - - - Property Brochure - - - - - -
-
-
-

[Property Name]

-

[Property Address]

-
-
[Price]
-
- [Bedrooms] Beds - [Bathrooms] Baths - [Area] sq. ft. -
-
-
-
- -
-
-

About this Property

-

[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.]

-
- - -
- -
-
- Reference ID: [Reference ID] -
-
- Owner Info: [Owner Name], [Owner Phone] -
-
-
- -
-
-
-

In-depth Details

-

A closer look at the property's features and specifications.

-
-
- -
-
-

Specifications

-
-
Status: [Status]
-
Type: [Type]
-
Floor: [Floor]
-
Parking: [Parking]
-
Year Built: [Year Built]
-
Furnishing: [Furnishing]
-
Maintenance Fee: [Maintenance Fee]
-
Service Charge: [Service Charge]
-
-
- -
-

Amenities & Features

-
-
[Amenity/Feature 1]
-
[Amenity/Feature 2]
-
[Amenity/Feature 3]
-
[Amenity/Feature 4]
-
[Amenity/Feature 5]
-
[Amenity/Feature 6]
-
[Amenity/Feature 7]
-
[Amenity/Feature 8]
-
[Amenity/Feature 9]
-
[Amenity/Feature 10]
-
-
-
- -
-
- Reference ID: [Reference ID] -
-
- Owner Info: [Owner Name], [Owner Phone] -
-
-
- -
-
-
-

Location & Nearby

-
-
Landmarks: [Nearby Landmarks]
-
Transportation: [Transportation]
-
Schools: [Schools]
-
Hospitals: [Hospitals]
-
Shopping: [Shopping Centers]
-
Airport: [Airport Distance]
-
-
-
- -
-
- -
-

Additional Information

-
-
Pet Friendly: [Pet Friendly Status]
-
Smoking: [Smoking Allowed]
-
Available From: [Available From Date]
-
Minimum Contract: [Minimum Contract Duration]
-
Security Deposit: [Security Deposit]
-
-
- -
-
- Reference ID: [Reference ID] -
-
- Owner Info: [Owner Name], [Owner Phone] -
-
-
- - - \ No newline at end of file diff --git a/sample templates/the grand oak villa.html b/sample templates/the grand oak villa.html deleted file mode 100644 index be5a620..0000000 --- a/sample templates/the grand oak villa.html +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - Prestige Real Estate Brochure - 4 Page - - - - - - - - -
-
-
FOR SALE
-
-

The Grand Oak Villa

-

123 Luxury Lane, Prestige City, PC 45678

-
-
-
5
Bedrooms
-
6
Bathrooms
-
6,200
Sq. Ft.
-
$4,500,000
Price
-
-
- -
-
- -
-
-

Description

-
-

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.

-

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.

-
-
- -
-
-

Specifications

-
-
Reference ID: [Reference ID]
-
Status: [Status]
-
Type: [Property Type]
-
Year Built: [Year Built]
-
Floor: [Floor]
-
Parking: [Parking]
-
Furnishing: [Furnishing]
-
Maintenance Fee: [Maintenance Fee]
-
Service Charge: [Service Charge]
-
-
-
-

Amenities & Features

-
    -
  • Infinity Pool
  • Private Home Theater
  • Gourmet Chef's Kitchen
  • Wine Cellar
  • Smart Home Automation
  • Spa & Sauna Room
  • Landscaped Gardens
  • Outdoor Fire Pit
  • -
-
-
-
-
-
-
Agent: [Agent Name] | [Agent Phone] | [Agent Email]
-
Owner: [Owner Name] | [Owner Phone] | [Owner Email]
-
-
- -
-
-
-
- -
-
-
-
Schools
-
[Schools]
-
-
-
-
Shopping
-
[Shopping Centers]
-
-
-
-
Airport
-
[Airport Distance]
-
-
-
-
Landmarks
-
[Nearby Landmarks]
-
-
-
-
Transportation
-
[Transportation]
-
-
-
-
Hospitals
-
[Hospitals]
-
-
-
-
Beach
-
[Beach Distance]
-
-
-
-
Metro
-
[Metro Distance]
-
-
-
-
-
-
Agent: [Agent Name] | [Agent Phone] | [Agent Email]
-
Owner: [Owner Name] | [Owner Phone] | [Owner Email]
-
-
- -
-
- -
- -
-

Additional Information

-
-
Pet Friendly: [Pet Friendly Status]
-
Smoking: [Smoking Allowed]
-
Available From: [Available From Date]
-
Minimum Contract: [Minimum Contract Duration]
-
Security Deposit: [Security Deposit]
-
Utilities Included: [Utilities Included]
-
Internet Included: [Internet Included]
-
Cable Included: [Cable Included]
-
-
-
-
-
-
Agent: [Agent Name] | [Agent Phone] | [Agent Email]
-
Owner: [Owner Name] | [Owner Phone] | [Owner Email]
-
-
- - - - \ No newline at end of file diff --git a/sample templates/the serenity house.html b/sample templates/the serenity house.html deleted file mode 100644 index 73066c1..0000000 --- a/sample templates/the serenity house.html +++ /dev/null @@ -1,426 +0,0 @@ - - - - - - Editorial Real Estate Brochure - Updated - - - - - - - - -
-
-
-
-
-
Elysian Estates Collection
-

The Serenity House

-

123 Luxury Lane, Prestige City, PC 45678

-

Reference ID: ES-8821

-
-
-
6,200 Sq. Ft. • 5 Bedrooms • 6 Bathrooms
- An architectural marvel of curated living space. -
- Offered at $4,500,000 -
-
-
-
- -
-
- 02 -

A Sanctuary of Modern Design

-

Where light, space, and nature converge to create an unparalleled living experience.

-
-
-

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.

-

A timeless residence built not just for living, but for thriving.

-

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.

-
-
-
-
-
- -
-
- 03 -

Property Specifications

-

A comprehensive overview of the property’s features, details, and amenities.

- -
-
-
5
Bedrooms
-
6
Bathrooms
-
6,200
Square Feet
-
0.75
Acres
-
- -
- -

Property Details

-
-
StatusFor Sale
-
Year Built2023
-
TypeSingle-Family Home
-
FurnishingPartially Furnished
-
Floor2 Levels
-
Maintenance Fee$1,200 / month
-
Parking3-Car Garage
-
Service ChargeIncluded
-
- -
- -

Amenities & Features

-
    -
  • Primary Suite with Spa-Bath
  • -
  • Radiant Heated Flooring
  • -
  • Custom Walk-in Closets
  • -
  • Smart Home Automation
  • -
  • Infinity Edge Saline Pool
  • -
  • Private Cinema Room
  • -
  • Temperature-Controlled Wine Cellar
  • -
  • Landscaped Gardens & Terrace
  • -
  • Gourmet Chef's Kitchen
  • -
  • Floor-to-Ceiling Glass Walls
  • -
-
-
-
- -
-
- 04 -

Floor Plan & Details

- -
-
-

Location & Nearby

-
Schools 5 min drive
-
Shopping 10 min drive
-
Hospitals 12 min drive
-
Country Club 8 min drive
-
Airport 20 min drive
-
-
-

Additional Information

-
Pet-Friendly By Approval
-
Smoking Not Permitted
-
Availability Immediate
-
Utilities Not Included
-
-
- -
- -

Floor Plan & Location

-
- -
-
-
Owner Information
-
John & Jane Doe
-

(555) 111-2222

- -
-
-
Agent Information
-
Olivia Sterling
-

(555) 987-6543

- -
-
-
-
- - - \ No newline at end of file diff --git a/sample templates/the vertice.html b/sample templates/the vertice.html deleted file mode 100644 index 4a11026..0000000 --- a/sample templates/the vertice.html +++ /dev/null @@ -1,604 +0,0 @@ - - - - - - Modern Urban Residences Brochure - Updated - - - - - - - - -
-
-
-
An Urban Oasis
-

THE VERTICE

-
18 Skyline Avenue, Metropolis Centre, MC 90210
-
- -
- -
-
- -
-
-

Where Design Meets Desire.

-

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.

-

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.

-
-
-
-
- THE VERTICE - Page 02 / 06 -
-
-
- -
-
- -
- - - - - -
-
- THE VERTICE - Page 03 / 06 -
-
-
- -
-
- -

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.

-
-
-
-
-

Lifestyle Amenities

-
    -
  • Rooftop Infinity Pool
  • -
  • Fitness Center
  • -
  • Residents' Sky Lounge
  • -
  • Private Cinema Room
  • -
  • Wellness Spa & Sauna
  • -
  • Business Center
  • -
  • 24/7 Concierge
  • -
  • Secure Parking
  • -
-
-
-

Key Specifications

-
Status New Development
-
Property Type Condominium
-
Year Built 2025
-
Technology Integrated Smart Home
-
Design Sustainable & Eco-Friendly
-
-
-
-
- THE VERTICE - Page 04 / 06 -
-
-
- -
-
- -
-
-
-
-

Two-Bedroom Residence

-
-
-
1,450
-
SQ. FT.
-
-
-
2
-
BEDROOMS
-
-
-
2
-
BATHROOMS
-
-
-
1
-
BALCONY
-
-
-

A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.

-
-
-
-
-
-

Three-Bedroom Penthouse

-
-
-
3,200
-
SQ. FT.
-
-
-
3
-
BEDROOMS
-
-
-
3.5
-
BATHROOMS
-
-
-
1
-
TERRACE
-
-
-

The pinnacle of luxury living, this penthouse offers expansive spaces, premium finishes, and exclusive access to a private rooftop terrace.

-
-
-
-

Additional Information

-
-
Pets
Allowed (w/ restrictions)
-
Smoking
In designated areas
-
Availability
Q4 2025
-
Parking
2 Spaces per Unit
-
Security Deposit
2 Months
-
Utilities
Sub-metered
-
-
-
-
- THE VERTICE - Page 05 / 06 -
-
-
- -
-
- -
-
-

Schedule a Private Viewing

-

Experience The Vertice firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.

-
-
Alexander Valentine
-
Sales Executive, Elysian Properties
-
- (555) 123-9876
- alex.v@elysian.com -
-
-
-
-

Neighborhood Highlights

-
    -
  • Landmarks: Central Park (5 min)
  • -
  • Transportation: Metro Line A (2 min walk)
  • -
  • Schools: Metropolis Intl. (10 min)
  • -
  • Shopping: The Galleria Mall (8 min)
  • -
  • Airport: 25 min drive
  • -
-
-
- -
-
- - - \ No newline at end of file