diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js new file mode 100644 index 0000000..27abdd1 --- /dev/null +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js @@ -0,0 +1,10519 @@ +import { LightningElement, track, wire } from "lwc"; +import { CurrentPageReference } from "lightning/navigation"; +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 { + @track currentStep = 1; + htmlContent = ""; // Remove @track to prevent reactive updates + + // Lifecycle method - called when component is rendered + renderedCallback() { + // If we're on step 3 and have template/property selected, load the template + if ( + this.currentStep === 3 && + this.selectedTemplateId && + this.selectedPropertyId + ) { + this.loadTemplateInStep3(); + } + } + @track properties = []; + @track selectedPropertyId = ""; + @track propertyData = {}; + @track isLoading = false; + @track error = ""; + @track marketAnalysis = { + includeMarketData: true, + includeROIAnalysis: true, + includeComparableSales: true, + includeRentalYield: true, + includeGrowthProjection: true, + }; + + // PDF generation properties + @track exportPdfButtonText = "📄 Generate PDF"; + @track showPdfPreview = false; + @track editorContent = ""; + @track pageCount = 0; + @track progressMessage = ""; + @track selectedPageSize = "A4"; // Default page size + @track zoom = 1.0; // Step 3 viewport zoom + @track isGeneratingPdf = false; // Loading state for PDF generation + @track previewPages = []; // Array of pages for viewport display + cachedTemplateContent = null; // Cache template content to prevent regeneration + + // Image review properties + @track showImageReview = false; + @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", + }, + { + url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + title: "Interior View 2", + category: "Interior", + }, + { + url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Interior View 3", + category: "Interior", + }, + ], + Exterior: [ + { + url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Exterior View 1", + category: "Exterior", + }, + { + url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Exterior View 2", + category: "Exterior", + }, + ], + Kitchen: [ + { + url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Kitchen View 1", + category: "Kitchen", + }, + { + url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Kitchen View 2", + category: "Kitchen", + }, + ], + Bedroom: [ + { + url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Bedroom View 1", + category: "Bedroom", + }, + { + url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Bedroom View 2", + category: "Bedroom", + }, + ], + "Living Area": [ + { + url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + title: "Living Area View 1", + category: "Living Area", + }, + { + url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Living Area View 2", + category: "Living Area", + }, + ], + Parking: [ + { + url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Parking View 1", + category: "Parking", + }, + ], + Anchor: [ + { + url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Anchor View 1", + category: "Anchor", + }, + ], + Maps: [ + { + url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + title: "Map View 1", + category: "Maps", + }, + ], + }; + + // Capture URL param c__propertyId and hydrate selection + @wire(CurrentPageReference) + setPageRef(ref) { + try { + const pid = ref && ref.state && ref.state.c__propertyId; + if (pid && pid !== this.selectedPropertyId) { + this.selectedPropertyId = pid; + const hydrate = () => { + this.loadPropertyData(); + this.loadPropertyImages(); + }; + if (this.properties && this.properties.length > 0) { + hydrate(); + } else { + this._deferHydrateFromUrl = hydrate; + } + } + } catch (e) { + // ignore + } + } + + // Template selection states - simplified approach + @track selectedTemplateId = ""; + + // Image Replacement Variables + @track showImageReplacement = false; + @track selectedImageElement = null; + @track replacementActiveTab = "property"; // 'property' or 'upload' + @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; + @track savedTemplates = []; + @track saveTemplateName = ""; + @track showHtmlDialog = false; + @track exportedHtml = ""; + // Table Dialog Variables + @track showTableDialog = false; + @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 showDownloadModal = false; + @track downloadInfo = {}; + @track selectedElement = null; + // z-index controls removed per request + + // Undo functionality + @track undoStack = []; + @track redoStack = []; + maxUndoSteps = 50; + + // Computed properties for image replacement tabs + get propertyImagesTabClass() { + return this.replacementActiveTab === "property" + ? "source-tab active" + : "source-tab"; + } + + // Unified gallery section used across templates + generateUnifiedGallerySectionHTML() { + const imagesHTML = this.generatePropertyGalleryHTML(); + return ` +
+

Property Gallery

+ +
`; + } + + // z-index functions removed + + get localUploadTabClass() { + return this.replacementActiveTab === "upload" + ? "source-tab active" + : "source-tab"; + } + + get showPropertyImagesTab() { + return this.replacementActiveTab === "property"; + } + + get showLocalUploadTab() { + 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() { + if (!this.propertyImages || this.propertyImages.length === 0) { + return []; + } + + if (this.selectedImageCategory === "all") { + return this.propertyImages; + } + + const filtered = this.propertyImages.filter((image) => { + const category = image.category ? image.category.toLowerCase() : "none"; + return category === this.selectedImageCategory; + }); + + 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 === ""; + return disabled; + } + + // Computed properties for template selection + get isBlankTemplateSelected() { + return this.selectedTemplateId === "blank-template"; + } + + get isEverkindTemplateSelected() { + return this.selectedTemplateId === "everkind-template"; + } + + get isShiftTemplateSelected() { + return this.selectedTemplateId === "shift-template"; + } + + get isSaintbartsTemplateSelected() { + return this.selectedTemplateId === "saintbarts-template"; + } + + get isLearnoyTemplateSelected() { + return this.selectedTemplateId === "learnoy-template"; + } + + get isLeafampTemplateSelected() { + return this.selectedTemplateId === "leafamp-template"; + } + + get isCoreshiftTemplateSelected() { + return this.selectedTemplateId === "coreshift-template"; + } + + get isModernHomeTemplateSelected() { + return this.selectedTemplateId === "modern-home-template"; + } + + get isGrandOakVillaTemplateSelected() { + return this.selectedTemplateId === "grand-oak-villa-template"; + } + + get isSampleTemplateSelected() { + return this.selectedTemplateId === "sample-template"; + } + + get isSerenityHouseTemplateSelected() { + return this.selectedTemplateId === "serenity-house-template"; + } + + get isLuxuryMansionTemplateSelected() { + return this.selectedTemplateId === "luxury-mansion-template"; + } + + // Image review computed properties + get isFirstImage() { + return this.currentImageIndex === 0; + } + + get isLastImage() { + return this.currentImageIndex === this.totalImages - 1; + } + + get displayImageIndex() { + return this.currentImageIndex + 1; + } + + // Computed properties for step visibility + get step1Class() { + return this.currentStep === 1 ? "step-content active" : "step-content"; + } + + get step2Class() { + return this.currentStep === 2 ? "step-content active" : "step-content"; + } + get step3Class() { + return this.currentStep === 3 ? "step-content active" : "step-content"; + } + + // Step navigation classes (for header stepper circles only) + get step1NavClass() { + return this.currentStep === 1 ? "active-circle active" : ""; + } + + get step2NavClass() { + return this.currentStep === 2 ? "active-circle active" : ""; + } + + get step3NavClass() { + return this.currentStep === 3 ? "active-circle active" : ""; + } + + // Inline styles to hard-force blue fill for active circles + get step1NavStyle() { + return this.currentStep === 1 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + get step2NavStyle() { + return this.currentStep === 2 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + get step3NavStyle() { + return this.currentStep === 3 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + + // Image availability flag + get hasPropertyImages() { + return ( + Array.isArray(this.realPropertyImages) && + this.realPropertyImages.length > 0 + ); + } + + // Dynamic class for pdf viewport to enable placeholder styling when no images + get pdfViewportClass() { + return this.hasPropertyImages ? "pdf-viewport" : "pdf-viewport no-images"; + } + + originalTemplateGridHTML = null; + originalStylesText = null; + + renderedCallback() { + if (this.currentStep === 1) { + const gridLive = this.template.querySelector("#all-templates"); + if (gridLive) { + // Always keep a clean snapshot from the DOM the first time we hit step 1 in a render pass + if ( + this.originalTemplateGridHTML === null || + this.originalTemplateGridHTML.length < 50 + ) { + this.originalTemplateGridHTML = gridLive.innerHTML; + } + } + } + if (this.originalTemplateGridHTML === null) { + const grid = this.template.querySelector("#all-templates"); + if (grid) { + this.originalTemplateGridHTML = grid.innerHTML; + } + } + if (this.originalStylesText === null) { + const styles = this.template.querySelectorAll("style"); + this.originalStylesText = Array.from(styles).map((s) => s.textContent); + } + } + + resetStep1Grid() { + const grid = this.template.querySelector("#all-templates"); + if (!grid) return; + // Clear any selected states on cards + const cards = grid.querySelectorAll(".template-card"); + cards.forEach((card) => { + card.classList.remove("selected"); + }); + // DON'T clear selectedTemplateId - preserve the selection + // this.selectedTemplateId = ''; + } + + restoreComponentStyles() { + if (!this.originalStylesText) return; + const styles = this.template.querySelectorAll("style"); + const snapshots = this.originalStylesText; + styles.forEach((styleEl, idx) => { + if ( + snapshots[idx] !== undefined && + styleEl.textContent !== snapshots[idx] + ) { + styleEl.textContent = snapshots[idx]; + } + }); + } + get isNextButtonDisabled() { + return !this.selectedTemplateId; + } + + get isNextButtonDisabledStep2() { + return !this.selectedPropertyId; + } + + // Wire method to get properties + @wire(getProperties) + wiredProperties({ error, data }) { + if (data) { + this.properties = data; + if (this._deferHydrateFromUrl) { + try { + this._deferHydrateFromUrl(); + } catch (e) {} + this._deferHydrateFromUrl = null; + } + } else if (error) { + this.error = "Error loading properties: " + error.body.message; + } + } + + // Derived list with selected flag to force dropdown selection rendering + get propertiesWithSelected() { + const selectedId = this.selectedPropertyId || ""; + return (this.properties || []).map((p) => ({ + ...p, + _selected: p.Id === selectedId, + })); + } + + // Template selection handler + handleTemplateSelect(event) { + const templateId = event.currentTarget.dataset.templateId; + + // Clear cached content when selecting a new template + this.cachedTemplateContent = null; + + // Set the selected template + switch (templateId) { + case "blank-template": + this.selectedTemplateId = "blank-template"; + break; + case "everkind-template": + this.selectedTemplateId = "everkind-template"; + break; + case "shift-template": + this.selectedTemplateId = "shift-template"; + break; + case "saintbarts-template": + this.selectedTemplateId = "saintbarts-template"; + break; + case "learnoy-template": + this.selectedTemplateId = "learnoy-template"; + break; + case "leafamp-template": + this.selectedTemplateId = "leafamp-template"; + break; + case "coreshift-template": + this.selectedTemplateId = "coreshift-template"; + break; + case "modern-home-template": + this.selectedTemplateId = "modern-home-template"; + break; + case "grand-oak-villa-template": + this.selectedTemplateId = "grand-oak-villa-template"; + break; + case "serenity-house-template": + this.selectedTemplateId = "serenity-house-template"; + break; + case "sample-template": + this.selectedTemplateId = "sample-template"; + break; + case "luxury-mansion-template": + this.selectedTemplateId = "luxury-mansion-template"; + break; + default: + break; + } + + // Visually mark the selected template card with a black border + try { + this.template.querySelectorAll(".template-card").forEach((card) => { + card.classList.remove("selected"); + }); + if (event.currentTarget && event.currentTarget.classList) { + event.currentTarget.classList.add("selected"); + } + } catch (e) {} + } + + resetTemplateSelections() { + this.selectedTemplateId = ""; + } + // Page size change handler + handlePageSizeChange(event) { + const newPageSize = event.target.value; + this.selectedPageSize = newPageSize; + + // Update the preview frame with new dimensions + this.updatePreviewFrameSize(newPageSize); + + // Re-fit to width when page size changes + setTimeout(() => this.fitToWidth(), 0); + } + + // ===== Step 3 viewport zoom controls ===== + get zoomPercent() { + try { + return `${Math.round((this.zoom || 1) * 100)}%`; + } catch (e) { + return "100%"; + } + } + + get pdfCanvasStyle() { + const scale = this.zoom || 1; + return `transform: scale(${scale}) !important;`; + } + + zoomIn() { + this.zoom = Math.min((this.zoom || 1) + 0.1, 3); + } + zoomOut() { + this.zoom = Math.max((this.zoom || 1) - 0.1, 0.3); + } + resetZoom() { + this.zoom = 1; + } + + // Handler methods for HTML onclick events + handleZoomIn() { + this.zoomIn(); + } + handleZoomOut() { + this.zoomOut(); + } + handleZoom100() { + this.resetZoom(); + } + handleFitWidth() { + this.fitToWidth(); + } + handleFitPage() { + this.fitToPage(); + } + + // Page management methods + addNewPage() { + const newPage = { + id: `page-${Date.now()}`, + content: "

New page content...

", + }; + this.previewPages = [...this.previewPages, newPage]; + this.updatePageCount(); + } + + updatePageCount() { + const pageInfo = this.template?.querySelector(".page-info"); + if (pageInfo) { + const pageCount = this.previewPages.length || 1; + pageInfo.innerHTML = `${ + this.selectedPageSize + } - ${pageCount} page${pageCount > 1 ? "s" : ""}`; + } + } + + // Split content into pages based on page breaks + splitContentIntoPages(content) { + if (!content) return []; + + // Look for page break markers or split by content length + const pageBreakMarkers = [ + '
', + '
', + ]; + let pages = []; + + // Check if content has explicit page breaks + let hasPageBreaks = false; + pageBreakMarkers.forEach((marker) => { + if (content.includes(marker)) { + hasPageBreaks = true; + } + }); + + if (hasPageBreaks) { + // Split by page breaks + const pageContent = content.split(/]*page-break[^>]*><\/div>/i); + pages = pageContent.map((pageContent, index) => ({ + id: `page-${index + 1}`, + content: pageContent.trim() || "

Empty page

", + })); + } else { + // Single page for now - can be enhanced to auto-split based on content length + pages = [ + { + id: "page-1", + content: content, + }, + ]; + } + + return pages; + } + + // Update preview pages when content changes + updatePreviewPages() { + if (this.htmlContent) { + this.previewPages = this.splitContentIntoPages(this.htmlContent); + } else { + this.previewPages = []; + } + this.updatePageCount(); + } + + fitToWidth() { + const container = this.template?.querySelector(".pdf-viewport"); + if (!container) { + return; + } + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const available = Math.max((container.clientWidth || baseWidth) - 32, 100); + this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.3); + } + + fitToPage() { + const container = this.template?.querySelector(".pdf-viewport"); + if (!container) { + return; + } + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + const availableW = Math.max((container.clientWidth || baseWidth) - 32, 100); + const availableH = Math.max( + (container.clientHeight || baseHeight) - 32, + 100 + ); + const scaleW = availableW / baseWidth; + const scaleH = availableH / baseHeight; + this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.3); + } + // Update preview frame size based on selected page size + updatePreviewFrameSize(pageSize) { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + // Update the data attribute for the CSS content + previewFrame.setAttribute("data-page-size", pageSize); + + // Re-render content with new page size + this.renderContentInPages(pageSize); + + // Update page count based on new page size + this.updatePageCountForSize(pageSize); + } + } + + // Render content in separate pages based on selected size + renderContentInPages(pageSize) { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!previewFrame) return; + + // Get the current template content + const templateHTML = this.createTemplateHTML(); + if (!templateHTML) return; + + // Clear existing content + previewFrame.innerHTML = ""; + + // Split content into pages based on size + const pages = this.splitContentIntoPages(templateHTML, pageSize); + + // Create page containers + pages.forEach((pageContent, index) => { + const pageContainer = document.createElement("div"); + pageContainer.className = `preview-page page-size-${pageSize.toLowerCase()}`; + pageContainer.setAttribute("data-page-number", `Page ${index + 1}`); + pageContainer.innerHTML = pageContent; + previewFrame.appendChild(pageContainer); + }); + } + // Split HTML content into pages based on page size + splitContentIntoPages(htmlContent, pageSize) { + const pages = []; + let currentPage = ""; + let currentHeight = 0; + + // Define page heights in mm + const pageHeights = { + A4: 297, + A3: 420, + }; + + const maxHeight = pageHeights[pageSize] || 297; + + // Create a temporary div to parse HTML + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + + // Get all direct children (brochure pages) + const children = Array.from(tempDiv.children); + + children.forEach((child, index) => { + // If it's a brochure-page div, treat it as a separate page + if (child.classList.contains("brochure-page")) { + if (currentPage) { + pages.push(currentPage); + } + currentPage = child.outerHTML; + currentHeight = 0; + } else { + // For other content, check if it fits in current page + const estimatedHeight = this.estimateElementHeight(child, pageSize); + + if (currentHeight + estimatedHeight > maxHeight && currentPage) { + // Start new page + pages.push(currentPage); + currentPage = child.outerHTML; + currentHeight = estimatedHeight; + } else { + // Add to current page + currentPage += child.outerHTML; + currentHeight += estimatedHeight; + } + } + }); + + // Add the last page + if (currentPage) { + pages.push(currentPage); + } + + return pages; + } + + // Estimate element height based on page size + estimateElementHeight(element, pageSize) { + // Base height estimation in mm + let baseHeight = 50; // Default height + + // Adjust based on element type + if (element.tagName === "IMG") { + baseHeight = 100; + } else if (element.tagName === "H1") { + baseHeight = 30; + } else if (element.tagName === "H2") { + baseHeight = 25; + } else if (element.tagName === "P") { + baseHeight = 20; + } else if (element.tagName === "DIV") { + baseHeight = 80; + } + + // Adjust for page size + if (pageSize === "A3") { + baseHeight *= 1.4; // A3 is larger + } + + return baseHeight; + } + + // Update page count based on selected page size + updatePageCountForSize(pageSize) { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + let pageHeight = 297; // Default A4 height in mm + + switch (pageSize) { + case "A3": + pageHeight = 420; // A3 height in mm + break; + case "A4": + default: + pageHeight = 297; // A4 height in mm + break; + } + + // Calculate content height and estimate pages needed + const contentHeight = previewFrame.scrollHeight; + const contentHeightMm = contentHeight * 0.264583; // Convert px to mm + const pagesNeeded = Math.ceil(contentHeightMm / pageHeight); + + this.pageCount = Math.max(1, Math.min(pagesNeeded, 20)); // Limit to 1-20 pages + } + } + + // Navigation methods + nextStep() { + if (this.currentStep === 1 && !this.selectedTemplateId) { + this.showError("Please select a template first."); + return; + } + 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(); + requestAnimationFrame(() => { + this.updatePreviewFrameSize(this.selectedPageSize || "A4"); + }); + } + this.scrollToTop(); + } + } + + previousStep() { + if (this.currentStep > 1) { + this.currentStep--; + // Reset click tracking when changing steps + this.resetImageClickTracking(); + if (this.currentStep === 1) { + this.resetStep1Grid(); + } + this.scrollToTop(); + } + } + + + replaceStaticWithDynamic(content) { +if (!this.propertyData || !content) return content; + +let result = content; + +// Replace hardcoded values with actual property data +result = result.replace(/Concorde Tower/g, this.propertyData.propertyName || 'Property Name'); +result = result.replace(/AED 81,999/g, this.propertyData.rentPriceMin || this.propertyData.price || 'Price'); +result = result.replace(/Modern Villa/g, this.propertyData.propertyName || 'Property Name'); +result = result.replace(/AED 2,500,000/g, this.propertyData.salePriceMin || this.propertyData.price || 'Price'); +result = result.replace(/Dubai/g, this.propertyData.city || 'Location'); +result = result.replace(/This beautiful property offers exceptional value and modern amenities\./g, +this.propertyData.descriptionEnglish || 'Property description'); + +return result; +} + + goToStep(event) { + const step = parseInt(event.currentTarget.dataset.step); + this.currentStep = step; + // Reset click tracking when changing steps + this.resetImageClickTracking(); + if (this.currentStep === 1) { + this.resetStep1Grid(); + // Also reconstruct grid HTML from original snapshot if available to fully reset content + if ( + this.originalTemplateGridHTML && + this.originalTemplateGridHTML.length > 50 + ) { + const grid = this.template.querySelector("#all-templates"); + if (grid) { + grid.innerHTML = this.originalTemplateGridHTML; + } + } + // Restore any styles that could have been mutated during step 3 + this.restoreComponentStyles(); + // Rebind click handlers for fresh grid without page reload + requestAnimationFrame(() => { + const cards = this.template.querySelectorAll( + "#all-templates .template-card" + ); + cards.forEach((card) => { + card.onclick = this.handleTemplateSelect.bind(this); + // Restore selected state if this card matches the selected template + if ( + this.selectedTemplateId && + card.dataset.templateId === this.selectedTemplateId + ) { + card.classList.add("selected"); + } + }); + }); + } + // If going directly to step 3, load the template only if not already loaded + if ( + this.currentStep === 3 && + (!this.htmlContent || this.htmlContent.trim() === "") + ) { + this.loadTemplateInStep3(); + requestAnimationFrame(() => { + this.updatePreviewFrameSize(this.selectedPageSize || "A4"); + // Auto fit width for better initial experience + this.fitToWidth && this.fitToWidth(); + }); + } + this.scrollToTop(); + } + + // Scroll to top of page when changing steps + scrollToTop() { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + // Load template content into step 3 enhanced editor + async loadTemplateInStep3() { + if (this.selectedTemplateId && this.selectedPropertyId) { + try { + // Use cached content if available to prevent regeneration + if ( + this.cachedTemplateContent && + this.cachedTemplateContent.templateId === this.selectedTemplateId && + this.cachedTemplateContent.propertyId === this.selectedPropertyId + ) { + this.htmlContent = this.cachedTemplateContent.html; + this.updatePreviewPages(); + this.updatePreviewFrameSize(this.selectedPageSize); + setTimeout(() => { + this.updatePageCountForSize(this.selectedPageSize); + this.updatePageCount(); + this.fitToWidth && this.fitToWidth(); + }, 100); + return; + } + + // Ensure property images are loaded before creating template + if (this.realPropertyImages.length === 0) { + await this.loadPropertyImages(); + } + + const templateHTML = this.createTemplateHTML(); + + // Replace any hardcoded background-image URLs with property images + const processedTemplateHTML = + this.replaceBackgroundImagesInHTML(templateHTML); + + // Cache the template content + this.cachedTemplateContent = { + templateId: this.selectedTemplateId, + propertyId: this.selectedPropertyId, + html: processedTemplateHTML, + }; + + // Set the HTML content for the template binding + this.htmlContent = processedTemplateHTML; + + // Update preview pages with the new content + this.updatePreviewPages(); + + // Set initial page size class and data attribute + this.updatePreviewFrameSize(this.selectedPageSize); + + // Update page count after template is loaded + setTimeout(() => { + this.updatePageCountForSize(this.selectedPageSize); + this.updatePageCount(); // Update page count display + // After content settles, fit viewport to width + this.fitToWidth && this.fitToWidth(); + // Ensure CSS background images reflect current property images + this.updateCSSBackgroundImages(); + }, 100); + } catch (error) { + this.error = "Error loading template: " + error.message; + + // Show error message in preview frame + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + previewFrame.innerHTML = `
+

Error Loading Template

+

${error.message}

+

Please try selecting a different template or contact support.

+
`; + } + } + } else { + // Show a message if template or property is not selected + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + if (!this.selectedTemplateId) { + previewFrame.innerHTML = + '

No Template Selected

Please go back to step 1 and select a template.

'; + } else if (!this.selectedPropertyId) { + previewFrame.innerHTML = + '

No Property Selected

Please go back to step 2 and select a property.

'; + } + } + } + } + + // Property selection handler + handlePropertySelection(event) { + this.selectedPropertyId = event.target.value; + + // Clear cached content when selecting a new property + this.cachedTemplateContent = null; + + if (this.selectedPropertyId) { + this.loadPropertyData(); + // Auto-scroll to property preview section after a short delay to ensure data is loaded + setTimeout(() => { + this.scrollToPropertyPreview(); + // Initialize image review with Interior category + // Auto-select category will be handled in loadPropertyImages + }, 300); + } + } + + // Scroll to property preview section + scrollToPropertyPreview() { + const propertyPreviewSection = this.template.querySelector( + ".property-details-layout" + ); + if (propertyPreviewSection) { + // Get the element's position and add offset for better positioning + const elementTop = propertyPreviewSection.offsetTop; + const offset = 100; // 100px offset from the top + + window.scrollTo({ + top: elementTop - offset, + behavior: "smooth", + }); + } + } + + // Helper function to safely get property values with validation + getPropertyValue(property, fieldPath, defaultValue = "N/A") { + try { + if (!property) return defaultValue; + + // Handle nested object paths like 'Contact__r.FirstName' + if (fieldPath.includes(".")) { + const parts = fieldPath.split("."); + let value = property; + for (const part of parts) { + if (value && typeof value === "object" && value[part] !== undefined) { + value = value[part]; + } else { + return defaultValue; + } + } + return value || defaultValue; + } + + // Handle direct field access + return property[fieldPath] || defaultValue; + } catch (error) { + return defaultValue; + } + } + // Load property data with comprehensive validation + async loadPropertyData() { + const selectedProperty = this.properties.find( + (prop) => prop.Id === this.selectedPropertyId + ); + if (selectedProperty) { + // Set the selectedProperty for use in PDF generation + this.selectedProperty = selectedProperty; + + // Helper function for safe property access + const get = (fieldPath, defaultValue = "N/A") => + this.getPropertyValue(selectedProperty, fieldPath, defaultValue); + + this.propertyData = { + // Basic Information + propertyName: + get("pcrm__Property_Name_Propertyfinder__c") !== "N/A" + ? get("pcrm__Property_Name_Propertyfinder__c") + : get("Name", "Property Name"), + propertyType: get("pcrm__Property_Type__c", "Property Type"), + status: get("pcrm__Status__c", "Available"), + referenceNumber: get("Name", "REF-001"), + + // Location Details + location: + get("pcrm__City_Bayut_Dubizzle__c") !== "N/A" + ? get("pcrm__City_Bayut_Dubizzle__c") + : get("pcrm__City_Propertyfinder__c") !== "N/A" + ? get("pcrm__City_Propertyfinder__c") + : "Location", + city: get("pcrm__City_Propertyfinder__c", "City"), + community: get("pcrm__Community_Propertyfinder__c", "Community"), + subCommunity: get( + "pcrm__Sub_Community_Propertyfinder__c", + "Sub Community" + ), + locality: get("pcrm__Locality_Bayut_Dubizzle__c", "Locality"), + subLocality: get( + "pcrm__Sub_Locality_Bayut_Dubizzle__c", + "Sub Locality" + ), + tower: get("pcrm__Tower_Bayut_Dubizzle__c", "Tower"), + unitNumber: get("pcrm__Unit_Number__c", "Unit Number"), + + // Additional Location Fields + cityBayut: get("pcrm__City_Bayut_Dubizzle__c"), + cityPropertyfinder: get("pcrm__City_Propertyfinder__c"), + communityBayut: get("pcrm__Community_Propertyfinder__c"), + subCommunityBayut: get("pcrm__Sub_Community_Propertyfinder__c"), + localityBayut: get("pcrm__Locality_Bayut_Dubizzle__c"), + subLocalityBayut: get("pcrm__Sub_Locality_Bayut_Dubizzle__c"), + towerBayut: get("pcrm__Tower_Bayut_Dubizzle__c"), + + // Rent Availability + rentAvailableFrom: get("pcrm__Rent_Available_From__c"), + rentAvailableTo: get("pcrm__Rent_Available_To__c"), + + // Contact Details + contactName: (() => { + const firstName = get("Contact__r.FirstName", ""); + const lastName = get("Contact__r.LastName", ""); + if (firstName !== "N/A" && lastName !== "N/A") { + return `${firstName} ${lastName}`.trim(); + } else if (firstName !== "N/A") { + return firstName; + } else if (lastName !== "N/A") { + return lastName; + } + return "Contact Not Linked"; + })(), + // Prefer property-level fields if populated; otherwise fall back to linked Contact + contactEmail: (() => { + const propEmail = get("Email__c"); + if (propEmail && propEmail !== "N/A") return propEmail; + const contactEmail = get("Contact__r.Email"); + return contactEmail && contactEmail !== "N/A" ? contactEmail : "N/A"; + })(), + contactPhone: (() => { + const propPhone = get("Phone__c"); + if (propPhone && propPhone !== "N/A") return propPhone; + const contactPhone = get("Contact__r.Phone"); + return contactPhone && contactPhone !== "N/A" ? contactPhone : "N/A"; + })(), + + // Specifications + bedrooms: get("pcrm__Bedrooms__c"), + bathrooms: get("pcrm__Bathrooms__c"), + floor: get("pcrm__Floor__c"), + size: get("pcrm__Size__c"), + sizeUnit: "sq ft", // Default unit since field doesn't exist + buildYear: get("pcrm__Build_Year__c"), + + // Parking & Amenities + parkingSpaces: get("pcrm__Parking_Spaces__c"), + + // Furnishing & Details + furnished: get("pcrm__Furnished__c"), + + // Pricing Information + rentPriceMin: (() => { + const value = get("pcrm__Rent_Price_min__c"); + return value !== "N/A" && !isNaN(value) + ? `AED ${parseFloat(value).toLocaleString()}` + : "N/A"; + })(), + rentPriceMax: (() => { + const value = get("pcrm__Rent_Price_max__c"); + return value !== "N/A" && !isNaN(value) + ? `AED ${parseFloat(value).toLocaleString()}` + : "N/A"; + })(), + salePriceMin: (() => { + const value = get("pcrm__Sale_Price_min__c"); + return value !== "N/A" && !isNaN(value) + ? `AED ${parseFloat(value).toLocaleString()}` + : "N/A"; + })(), + salePriceMax: (() => { + const value = get("pcrm__Sale_Price_max__c"); + return value !== "N/A" && !isNaN(value) + ? `AED ${parseFloat(value).toLocaleString()}` + : "N/A"; + })(), + + // Description & Title + titleEnglish: get("pcrm__Title_English__c"), + descriptionEnglish: get( + "pcrm__Description_English__c", + "This beautiful property offers exceptional value and modern amenities." + ), + + // Offering Type + offeringType: get("pcrm__Offering_Type__c"), + + // Legacy fields for backward compatibility + price: (() => { + const value = get("pcrm__Rent_Price_min__c"); + return value !== "N/A" && !isNaN(value) + ? `AED ${parseFloat(value).toLocaleString()}` + : "Price on Request"; + })(), + area: (() => { + const value = get("pcrm__Size__c"); + return value !== "N/A" && !isNaN(value) ? `${value} sq ft` : "N/A"; + })(), + yearBuilt: get("pcrm__Build_Year__c"), + parking: (() => { + const value = get("pcrm__Parking_Spaces__c"); + return value !== "N/A" && !isNaN(value) + ? `${value} Parking Space(s)` + : "N/A"; + })(), + furnishing: get("pcrm__Furnished__c"), + }; + + // Load property images + await this.loadPropertyImages(); + } + } + // Load property images from Image Genie + async loadPropertyImages() { + if (!this.selectedPropertyId) { + this.realPropertyImages = []; + this.initialCategorySelected = false; // Reset flag when no property selected + return; + } + + try { + const images = await getPropertyImages({ + propertyId: this.selectedPropertyId, + }); + + // 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, + })); + + if (this.realPropertyImages && this.realPropertyImages.length > 0) { + // Find the first category that has images + const firstAvailableCategory = this.findFirstAvailableCategory(); + 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) { + this.realPropertyImages = []; + } + } + + // Market analysis change handler + handleMarketAnalysisChange(event) { + const { name, checked } = event.target; + this.marketAnalysis[name] = checked; + } + // Generate template content + async generateTemplateContent() { + if (!this.selectedTemplateId || !this.selectedPropertyId) { + this.error = "Please select both a template and a property."; + return; + } + + this.isLoading = true; + this.error = ""; + + try { + // 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; + } else { + // Fallback: generate template HTML if editor is empty + htmlContent = this.createCompleteTemplateHTML(); + } + + // Generate PDF using the template HTML + const result = await generatePropertyPDF({ + propertyData: JSON.stringify(this.propertyData), + templateName: this.selectedTemplateId, + generatePDF: true, + htmlContent: htmlContent, + }); + + if (result.success) { + // Handle successful PDF generation + this.showSuccess("PDF generated successfully!"); + // You can add logic here to display the PDF or provide download link + } else { + this.error = + result.message || result.error || "Failed to generate PDF."; + } + } catch (error) { + this.error = + "Error generating PDF: " + + (error.body?.message || error.message || "Unknown error"); + } finally { + this.isLoading = false; + } + } + + // Scroll to Generate PDF button at the top + scrollToGeneratePdf() { + try { + // Find the Generate PDF button at the top + const generatePdfButton = this.template.querySelector(".export-pdf-btn"); + if (generatePdfButton) { + // Scroll to the button with smooth animation + generatePdfButton.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + + // Add a subtle highlight effect + generatePdfButton.style.boxShadow = "0 0 20px rgba(102, 126, 234, 0.6)"; + setTimeout(() => { + generatePdfButton.style.boxShadow = ""; + }, 2000); + } else { + } + } catch (error) {} + } + + // Simple PDF generation method for the export button - using the working approach from first prompt + async generatePdfSimple() { + if (!this.selectedTemplateId || !this.selectedPropertyId) { + this.error = "Please select both a template and a property."; + return; + } + + try { + // Set loading state + this.isGeneratingPdf = true; + + // Show progress + this.showProgress("Starting AI-powered PDF generation..."); + + // Get current editor content + const editorFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorFrame) { + throw new Error("Editor frame not found"); + } + + this.editorContent = editorFrame.innerHTML; + + // Generate PDF using external API + await this.generatePdfViaExternalApi(); + + // Success message is handled in generatePdfViaExternalApi + } catch (error) { + // 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; + } + } + // Clean HTML content for PDF generation by removing editor controls + cleanHtmlForPdf(htmlContent) { + // Wrap editor content so we can sanitize before PDF + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + + // Ensure list styling is preserved in output + const lists = tempDiv.querySelectorAll("ul, ol"); + lists.forEach((list) => { + if (list.tagName.toLowerCase() === "ul") { + list.style.listStyleType = "disc"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + } else { + list.style.listStyleType = "decimal"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + } + }); + + // Handle
-separated bullet/number lines inside a single block + const brBlocks = tempDiv.querySelectorAll("p, div"); + brBlocks.forEach((block) => { + if (block.closest("ul,ol")) return; + const html = block.innerHTML || ""; + if (!/br\s*\/?/i.test(html)) return; + const parts = html + .split(/(?:\s*)/i) + .map((s) => s.trim()) + .filter(Boolean); + if (parts.length < 2) return; + const bulletMarker = /^\s*(?: \s*)*(\*|\-|•)\s+/i; + const numberMarker = /^\s*(?: \s*)*\d+[\.)]\s+/i; + const allBullets = parts.every((p) => + bulletMarker.test(p.replace(/<[^>]+>/g, "")) + ); + const allNumbers = parts.every((p) => + numberMarker.test(p.replace(/<[^>]+>/g, "")) + ); + if (!(allBullets || allNumbers)) return; + const list = document.createElement(allNumbers ? "ol" : "ul"); + list.style.listStyleType = allNumbers ? "decimal" : "disc"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + list.style.breakInside = "avoid"; + list.style.pageBreakInside = "avoid"; + parts.forEach((line) => { + const li = document.createElement("li"); + li.innerHTML = line.replace( + /^\s*(?: \s*)*(\*|\-|•|\d+[\.)])\s+/i, + "" + ); + li.style.breakInside = "avoid"; + li.style.pageBreakInside = "avoid"; + list.appendChild(li); + }); + block.replaceWith(list); + }); + + return tempDiv.innerHTML; + } + // Generate PDF via external API using Apex proxy + async generatePdfViaExternalApi() { + try { + // Show loading state + this.isLoading = true; + this.showProgress("Preparing content for AI processing..."); + + // First, ensure we have template content loaded + let htmlContent = ""; + + // Check if preview frame has content + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!previewFrame) { + throw new Error("Editor content not found"); + } + + htmlContent = previewFrame.innerHTML; + + // Debug: Check if draggable elements are present + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + const draggableElements = tempDiv.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + draggableElements.forEach((el, index) => {}); + + // If preview frame is empty, generate the template HTML first + if ( + !htmlContent || + htmlContent.trim() === "" || + htmlContent.length < 100 + ) { + this.showProgress("Generating template content..."); + + // Generate the template HTML using the selected template and property + if (this.selectedTemplateId && this.selectedPropertyId) { + // Create a complete HTML template with property data + htmlContent = this.createCompleteTemplateHTML(); + + // Load it into the preview frame so user can see it + previewFrame.innerHTML = htmlContent; + } else { + throw new Error("No template or property selected"); + } + } + + // Ensure we have a complete HTML document with page size information + if (!htmlContent.includes("")) { + htmlContent = ` + + + + + Property Brochure - ${this.selectedPageSize} + + + + + + ${htmlContent} + +`; + } + + // 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 + // 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); + + // 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." + ); + } + }); + + // Clear progress timer on success + clearInterval(progressInterval); + + // Handle the new response format + if (pdfResult && pdfResult.success) { + // Update progress message + this.showProgress("PDF ready for download..."); + + // 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); + } + } else { + // Handle error response + const errorMessage = + pdfResult?.error || + pdfResult?.message || + "PDF generation failed with unknown error"; + throw new Error(errorMessage); + } + } catch (error) { + this.showError("PDF generation failed: " + error.message); + } finally { + this.isLoading = false; + this.hideProgress(); + } + } + + // Helper method to convert base64 to blob (from first prompt) + base64ToBlob(base64, mimeType) { + const byteCharacters = atob(base64); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + return new Blob([byteArray], { type: mimeType }); + } + // Create complete template HTML with property data + createCompleteTemplateHTML() { + try { + if (!this.selectedProperty) { + throw new Error("No property data available"); + } + + // Create a professional property brochure HTML with proper page breaks + const html = ` + + + + + + Property Brochure - ${this.selectedPageSize} + + + +
+ +
+

${ + this.selectedProperty?.Name || + this.propertyData?.propertyName || + "Property Details" + }

+

Exclusive Property Brochure

+
+ + +
+
🏠
+

Property Image

+
+ + +
+
+

Property Information

+ +
+ Property Type: + ${ + this.selectedProperty.Property_Type__c || + "N/A" + } +
+ +
+ Location: + ${ + this.selectedProperty.Location__c || "N/A" + } +
+ +
+ Price: + ${ + this.selectedProperty.Price__c + ? "$" + + this.selectedProperty.Price__c.toLocaleString() + : "N/A" + } +
+
+ +
+

Additional Details

+ +
+ Bedrooms: + ${ + this.selectedProperty.Bedrooms__c || "N/A" + } +
+ +
+ Bathrooms: + ${ + this.selectedProperty.Bathrooms__c || "N/A" + } +
+ +
+ Square Feet: + ${ + this.selectedProperty.Square_Feet__c + ? this.selectedProperty.Square_Feet__c.toLocaleString() + + " sq ft" + : "N/A" + } +
+
+
+ + +
+

Description

+

+ ${ + this.selectedProperty.Description__c || + "This beautiful property offers exceptional value and is located in a prime area. Contact us for more details and to schedule a viewing." + } +

+
+ + +
+

Interested in this property?

+

Contact our team for more information and to schedule a viewing.

+
+
📞 Call Us
+
📧 Email Us
+
💬 Chat
+
+
+ + + +
+ + + `; + + return html; + } catch (error) { + // Return a fallback HTML if there's an error + return ` +
+

Property Brochure

+

Property: ${ + this.selectedProperty?.Name || "Selected Property" + }

+

Template: ${ + this.selectedTemplateId || "Selected Template" + }

+

Generated on: ${new Date().toLocaleDateString()}

+
+ `; + } + } + + // Progress and message methods (from first prompt) + showProgress(message) { + this.isLoading = true; + this.progressMessage = message; + } + + hideProgress() { + this.isLoading = false; + this.progressMessage = ""; + } + + showSuccess(message) { + this.progressMessage = message; + this.error = ""; + } + + showError(message) { + this.error = message; + this.progressMessage = ""; + } + + // Handle PDF response using the working approach from first prompt + handlePdfResponse(result) { + try { + if (result.pdfUrl) { + // For the working approach, we need to create a proper download + this.downloadPDF(result.pdfUrl); + } else { + this.error = "PDF generated but no download URL received"; + } + } catch (error) { + this.error = "Error handling PDF response: " + error.message; + } + } + // Download PDF method + downloadPDF(pdfUrl) { + try { + // Create a temporary link element + const link = document.createElement("a"); + link.href = pdfUrl; + link.download = `property-brochure-${ + this.selectedTemplateId + }-${Date.now()}.pdf`; + + // Append to body, click, and remove + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + this.progressMessage = "PDF download started!"; + + // Fallback: If download doesn't work, show instructions + setTimeout(() => { + if (this.template.querySelector(".progress-message")) { + const progressMsg = this.template.querySelector(".progress-message"); + progressMsg.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"; + + progressMsg.appendChild(fallbackLink); + } + }, 2000); + } catch (error) { + this.error = "Error downloading PDF: " + error.message; + } + } + // Handle large PDF responses + async handleLargePDFResponse(decodedResponse) { + try { + const responseData = JSON.parse(decodedResponse); + + this.hideProgress(); + this.isLoading = false; + + if (responseData.status === "large_pdf") { + // Show options for large PDFs + this.showLargePDFOptions(responseData); + } + } catch (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) { + this.showError("Failed to generate compressed PDF: " + error.message); + } + } + + // Handle PDF download ready response + async handlePDFDownloadReady(pdfResult) { + try { + // Hide loading state + this.isLoading = false; + this.hideProgress(); + + // Set the download info for the modal + this.downloadInfo = { + filename: pdfResult.filename || "Unknown", + fileSize: pdfResult.file_size_mb + ? pdfResult.file_size_mb + " MB" + : "Unknown", + generatedAt: this.formatDate(pdfResult.generated_at), + expiresAt: this.formatDate(pdfResult.expires_at), + downloadUrl: pdfResult.download_url, + }; + // Automatically open download URL in new tab + window.open(pdfResult.download_url, "_blank"); + + // Show simple success message + this.showSuccess( + `✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB) - Download opened in new tab.` + ); + this.showSuccess( + `✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB)` + ); + } catch (error) { + this.showError("Error handling PDF download: " + error.message); + } + } + + // Modal control methods + closeDownloadModal() { + this.showDownloadModal = false; + } + + stopPropagation(event) { + event.stopPropagation(); + } + + copyDownloadLink() { + if (navigator.clipboard && this.downloadInfo) { + navigator.clipboard + .writeText(this.downloadInfo.downloadUrl) + .then(() => { + // Show feedback + const copyBtn = this.template.querySelector(".copy-btn"); + if (copyBtn) { + const originalText = copyBtn.textContent; + copyBtn.textContent = "✅ Copied!"; + copyBtn.classList.add("copied"); + + setTimeout(() => { + copyBtn.textContent = originalText; + copyBtn.classList.remove("copied"); + }, 2000); + } + }) + .catch((err) => { + alert("Failed to copy link to clipboard"); + }); + } + } + + openInNewTab() { + if (this.downloadInfo && this.downloadInfo.downloadUrl) { + window.open(this.downloadInfo.downloadUrl, "_blank"); + } + } + // Helper method to format dates + formatDate(dateString) { + if (!dateString) return "Unknown"; + const date = new Date(dateString); + return date.toLocaleString(); + } + // Create template HTML based on selection + createTemplateHTML() { + switch (this.selectedTemplateId) { + case "blank-template": + return this.createBlankTemplate(); + + case "modern-home-template": + return this.createModernHomeTemplate(); + case "grand-oak-villa-template": + // Grand Oak Villa (black theme with gold accents) + return this.createGrandOakVillaTemplate(); + + case "serenity-house-template": + return this.createSerenityHouseTemplate(); + case "luxury-mansion-template": + return this.createLuxuryMansionTemplate(); + default: + return this.createBlankTemplate(); + } + } + + // Format description for PDF generation + formatDescriptionForPDF(description) { + if (!description || description.trim() === "") { + return "Property description not available."; + } + + // Clean up the description + let formattedDescription = description.trim(); + + // Ensure proper paragraph breaks + formattedDescription = formattedDescription.replace(/\n\s*\n/g, "

"); + + // Wrap in paragraph tags if not already wrapped + if (!formattedDescription.startsWith("

")) { + formattedDescription = "

" + formattedDescription + "

"; + } + + // Ensure proper spacing between paragraphs + formattedDescription = formattedDescription.replace(/<\/p>

/g, "

"); + + return formattedDescription; + } + + // 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.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__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"; + + // Build gallery pages so ALL images render in the empty template + const allImages = Array.isArray(this.realPropertyImages) + ? this.realPropertyImages + : []; + const imagesPerPage = 8; + const firstChunk = allImages.slice(0, imagesPerPage); + let additionalGalleryPagesHTML = ""; + if (allImages.length > imagesPerPage) { + for (let i = imagesPerPage; i < allImages.length; i += imagesPerPage) { + const chunk = allImages.slice(i, i + imagesPerPage); + additionalGalleryPagesHTML += ` +
+
+

Property Gallery

+ +
+
`; + } + } + return ` +
+ +
+ Property Header Image +
+
+

${propertyName}

+

${location}

+

${price}

+
+
+ + +
+

Basic Information

+
+
Property Type: ${propertyType}
+
Status: ${status}
+
City: ${city}
+
Community: ${community}
+
Sub Community: ${subCommunity}
+
Furnished: ${furnished}
+
+
+ + +
+

Contact Details

+
+
Name: ${ + data.contactName || "N/A" + }
+
Email: ${ + data.contactEmail || "N/A" + }
+
Phone: ${ + data.contactPhone || "N/A" + }
+
+
+ + +
+

Location Details

+
+
City (Bayut): ${ + data.cityBayut || "N/A" + }
+
City (Propertyfinder): ${ + data.cityPropertyfinder || "N/A" + }
+
Community (Bayut): ${ + data.communityBayut || "N/A" + }
+
Sub Community (Bayut): ${ + data.subCommunityBayut || "N/A" + }
+
Locality (Bayut): ${ + data.localityBayut || "N/A" + }
+
Sub Locality (Bayut): ${ + data.subLocalityBayut || "N/A" + }
+
Tower (Bayut): ${ + data.towerBayut || "N/A" + }
+
Unit Number: ${ + data.unitNumber || "N/A" + }
+
+
+ + +
+

Specifications

+
+
Bedrooms: ${bedrooms}
+
Bathrooms: ${bathrooms}
+
Size: ${size} ${sizeUnit}
+
Parking Spaces: ${parkingSpaces}
+
Build Year: ${buildYear}
+
Floor: ${ + data.floor || "N/A" + }
+
+
+ +
+

Pricing Information

+
+
Rent Price: ${rentPriceMin}
+
Sale Price: ${salePriceMin}
+
Rent Price (Max): ${ + data.rentPriceMax || "N/A" + }
+
Sale Price (Max): ${ + data.salePriceMax || "N/A" + }
+
+
+ + +
+

Rent Availability

+
+
Rent Available From: ${ + data.rentAvailableFrom || "N/A" + }
+
Rent Available To: ${ + data.rentAvailableTo || "N/A" + }
+
+
+ + +
+

Property Description

+
+

${titleEnglish}

+
+
+ ${descriptionEnglish} +
+
+
+
Property Type: ${propertyType}
+
Status: ${status}
+
Furnished: ${furnished}
+
Build Year: ${buildYear}
+
+
+
+ + +
+

Amenities & Features

+
+
Parking Spaces: ${ + data.parkingSpaces || "N/A" + }
+
Furnished: ${ + data.furnished || "N/A" + }
+
Offering Type: ${ + data.offeringType || "N/A" + }
+
Build Year: ${ + data.buildYear || "N/A" + }
+
+
+ +
+

Property Gallery

+ +
+
+ ${additionalGalleryPagesHTML} + `; + } + + createEverkindTemplate() { + return `Luxury Villa

Luxury Villa Template

Elegance. Precision. Luxury.

`; + } + createShiftTemplate() { + 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.pcrm__Title_English__c || data.Name || data.propertyName || ""; + 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(); + // Gallery HTML generated once in each template scope when needed + const allGalleryImages = this.realPropertyImages || []; + const firstGalleryCount = Math.min(4, allGalleryImages.length); + const firstPageImages = allGalleryImages.slice(0, firstGalleryCount); + const propertyGallery = this.generatePropertyGalleryHTML(); + const interiorImage = this.getSmartImageForSection( + "interior", + "" + ); + const kitchenImage = this.getSmartImageForSection( + "kitchen", + "" + ); + + // Debug logging + + // Generate property gallery for uncategorized images (already declared above) + + 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.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.pcrm__Title_English__c || data.Name || data.propertyName || ""; + 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."; + + // Get smart images - use direct method for exterior + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage = this.getSmartImageForSection("interior", ""); + const bedroomImage = this.getSmartImageForSection("bedroom", ""); + + // 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.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.pcrm__Title_English__c || data.Name || data.propertyName || ""; + + // 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() { + 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.pcrm__Title_English__c || data.Name || data.propertyName || ""; + 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 + + // 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}
`; + } + + + + + + + + createModernHomeTemplate() { + const data = this.propertyData || {}; + 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 = this.formatDescriptionForPDF( + 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.pcrm__Title_English__c || data.Name || data.propertyName || ""; + + 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 gallery and amenities + const propertyGallery = this.generatePropertyGalleryHTML(); + const amenitiesHTML = this.generateAmenitiesHTML(data); + + + 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"; + + 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"; + + const acres = data.acres || "0.75"; + + 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"; + + 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.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.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.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 ` + + + + + Property Brochure - A4 Size + + + + + +
+
+
+

${propertyName}

+

${location}

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

About this Property

+

${description}

+
+ + +
+ +
+
+ Reference ID: ${referenceId} +
+
+ Owner Info: ${ownerName}, ${ownerPhone} +
+
+
+ +
+
+
+

In-depth Details

+

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

+
+
+ +
+
+

Specifications

+
+
Status: ${status}
+
Type: ${propertyType}
+
Floor: ${floor}
+
Parking: ${parking}
+
Year Built: ${yearBuilt}
+
Furnishing: ${furnishing}
+
Maintenance Fee: ${maintenanceFee}
+
Service Charge: ${serviceCharge}
+
+
+ +
+

Amenities & Features

+
+ ${amenitiesHTML} +
+
+
+ +
+
+ Reference ID: ${referenceId} +
+
+ Owner Info: ${ownerName}, ${ownerPhone} +
+
+
+ +
+
+
+

Location & Nearby

+
+
Landmarks: ${nearbyLandmarks}
+
Transportation: ${transportation}
+
Schools: ${schools}
+
Hospitals: ${hospitals}
+
Shopping: ${shoppingCenters}
+
Airport: ${airportDistance}
+
+
+
+ +
+
+ +
+

Additional Information

+
+
Pet Friendly: ${petFriendly}
+
Smoking: ${smokingAllowed}
+
Available From: ${availableFrom}
+
Minimum Contract: ${minimumContract}
+
Security Deposit: ${securityDeposit}
+
+
+ +
+
+ Reference ID: ${referenceId} +
+
+ Owner Info: ${ownerName}, ${ownerPhone} +
+
+
+ +
+ + +
+
+ Reference ID: ${referenceId} +
+
+ Owner Info: ${ownerName}, ${ownerPhone} +
+
+
+ + + + ` + } + + +// grand oak villa template + createGrandOakVillaTemplate() { + const data = this.propertyData || {}; + + + // Enhanced property data extraction with better fallbacks + const propertyName = + data.Name || + data.propertyName || + data.pcrm__Title_English__c || + "The Grand Oak Villa"; + const location = + data.Address__c || + data.location || + "123 Luxury Lane, Prestige City, PC 45678"; + const price = + data.Sale_Price_Min__c || + data.Rent_Price_Min__c || + data.Price__c || + data.price || + "$4,500,000"; + const referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName || ""; + const bedrooms = data.Bedrooms__c || data.bedrooms || "5"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "6"; + const squareFeet = + data.Square_Feet__c || data.squareFeet || data.area || "6,200"; + const status = (data.Status__c || data.status || "FOR SALE").toString(); + + // Enhanced property details + const propertyType = data.Property_Type__c || data.propertyType || "Villa"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || "2020"; + const furnishing = + data.Furnished__c || data.furnishing || "Fully Furnished"; + const parking = data.Parking_Spaces__c || data.parking || "2"; + const description = this.formatDescriptionForPDF( + data.Description_English__c || + data.descriptionEnglish || + data.description || + "An exquisite villa offering unparalleled luxury and sophistication in one of the most prestigious locations." + ); + const floor = data.Floor__c || data.floor || "Ground Floor"; + const maintenanceFee = + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; + + // Additional property details + const lotSize = data.Lot_Size__c || data.lotSize || "0.5 acres"; + const heating = data.Heating__c || data.heating || "Central Air"; + const cooling = data.Cooling__c || data.cooling || "Central Air"; + const roof = data.Roof__c || data.roof || "Tile"; + const exterior = data.Exterior__c || data.exterior || "Stone & Brick"; + const foundation = data.Foundation__c || data.foundation || "Concrete"; + const utilities = data.Utilities__c || data.utilities || "All Connected"; + const zoning = data.Zoning__c || data.zoning || "Residential"; + const hoa = data.HOA__c || data.hoa || "Yes"; + const hoaFee = data.HOA_Fee__c || data.hoaFee || "$500/month"; + const taxYear = data.Tax_Year__c || data.taxYear || "2024"; + const taxAmount = data.Tax_Amount__c || data.taxAmount || "$12,000/year"; + const lastSold = data.Last_Sold__c || data.lastSold || "2020"; + const lastSoldPrice = + data.Last_Sold_Price__c || data.lastSoldPrice || "$3,200,000"; + + // Location and POI data + const schools = data.Schools__c || data.schools || "5 min drive"; + const shoppingCenters = + data.Shopping_Centers__c || data.shoppingCenters || "10 min drive"; + const airportDistance = + data.Airport_Distance__c || data.airportDistance || "25 min drive"; + const nearbyLandmarks = + data.Nearby_Landmarks__c || data.nearbyLandmarks || "City Center 15 min"; + const transportation = + data.Transportation__c || data.transportation || "Metro 5 min walk"; + const hospitals = data.Hospitals__c || data.hospitals || "10 min drive"; + const beachDistance = + data.Beach_Distance__c || data.beachDistance || "30 min drive"; + const metroDistance = + data.Metro_Distance__c || data.metroDistance || "5 min walk"; + + // Additional information + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "Yes"; + const smokingAllowed = + data.Smoking_Allowed__c || data.smokingAllowed || "No"; + const availableFrom = + data.Available_From__c || data.availableFrom || "Immediate"; + const minimumContract = + data.Minimum_Contract__c || data.minimumContract || "12 months"; + const securityDeposit = + data.Security_Deposit__c || data.securityDeposit || "2 months rent"; + const utilitiesIncluded = + data.Utilities_Included__c || + data.utilitiesIncluded || + "Water, Electricity"; + const internetIncluded = + data.Internet_Included__c || data.internetIncluded || "Yes"; + const cableIncluded = data.Cable_Included__c || data.cableIncluded || "Yes"; + + // Agent and owner information + const agentName = data.Agent_Name__c || data.agentName || "Olivia Sterling"; + const agentPhone = + data.Agent_Phone__c || data.agentPhone || "(555) 987-6543"; + const agentEmail = + data.Agent_Email__c || data.agentEmail || "olivia@elysianestates.com"; + const ownerName = data.Owner_Name__c || data.ownerName || "John & Jane Doe"; + const ownerPhone = + data.Owner_Phone__c || data.ownerPhone || "(555) 111-2222"; + const ownerEmail = + data.Owner_Email__c || data.ownerEmail || "owner@email.com"; + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage1 = 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 interiorImage2 = 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" + ); + const kitchenImage = this.getSmartImageForSection( + "kitchen", + "https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?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-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + + // Generate amenities HTML + const amenitiesHTML = this.generateAmenitiesHTML(data); + + // Generate property gallery HTML + const propertyGalleryHTML = this.generatePropertyGalleryHTML(data); + + // Return the complete Grand Oak Villa template with all dynamic data + return ` + + + + + Prestige Real Estate Brochure - 4 Page - A3 Size + + + + + + + + +
+
+
FOR SALE
+
+

${propertyName}

+

${location}

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

Description

+
+ ${description} +
+
+ +
+
+

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]
+
+ + + //
+ //
+ // 06 + //

${propertyName} - Property Gallery

+ + // + //
+ //
+ +
+
+
Agent: [Agent Name] | [Agent Phone] | [Agent Email]
+
Owner: [Owner Name] | [Owner Phone] | [Owner Email]
+
+ + + + +` + } + + // luxury mansion template (Vertice) + createLuxuryMansionTemplate() { + const data = this.propertyData || {}; + + const propertyName = data.Name || data.propertyName || "Property Name"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const location = data.Address__c || data.location || "Location"; + 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 referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A"; + + const description = this.formatDescriptionForPDF( + data.Description_English__c || + data.descriptionEnglish || + data.description || + "Property description not available." + ); + + const propertyGallery = this.generatePropertyGalleryHTML(); + + return ` + + + + + Modern Urban Residences Brochure - Updated - A4 Size + + + + + + + + +
+
+
+
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
  • +
+
+
+
+ THE VERTICE + Page 06 / 06 +
+
+
+ + + +` + + } + +// serenity house template + createSerenityHouseTemplate() { + const data = this.propertyData || {}; + + // Extract all available property data with fallbacks + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName || ""; + 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.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__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 = this.formatDescriptionForPDF( + 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__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.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 ` + + + + + Editorial Real Estate Brochure - Updated - A4 Size + + + + + + + + +
+
+
+
+
+

${propertyName}

+

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 ${price} +
+
+
+
+ +
+
+ 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

+ +
+
+
+
+ + + +` + } + + // Error handling methods + clearError() { + this.error = ""; + } + + // Development mode properties + @track debugMode = false; + + // Development page event handlers + handleClearData() { + this.currentStep = 1; + this.selectedTemplateId = ""; + this.selectedPropertyId = ""; + this.propertyData = {}; + this.htmlContent = ""; + this.editorContent = ""; + this.error = ""; + this.showPdfPreview = false; + this.showImageReview = false; + this.showImageReplacement = false; + this.showSaveDialog = false; + this.undoStack = []; + this.redoStack = []; + this.showSuccess("All data cleared"); + } + + handleResetTemplates() { + this.currentStep = 1; + this.selectedTemplateId = ""; + this.selectedPropertyId = ""; + this.propertyData = {}; + this.htmlContent = ""; + this.editorContent = ""; + this.showSuccess("Templates reset to default"); + } + + handleTestPdf() { + if (this.selectedTemplateId && this.selectedPropertyId) { + this.generatePdfViaExternalApi(); + } else { + this.showError("Please select a template and property first"); + } + } + + + handleToggleDebug(event) { + this.debugMode = event.detail.debugMode; + if (this.debugMode) { + this.showSuccess("Debug mode enabled - check console for detailed logs"); + } + } + + // PDF Preview methods + closePdfPreview() { + this.showPdfPreview = false; + } + + // Editor methods (placeholder implementations) + handleSave() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + const content = editorContent.innerHTML; + const blob = new Blob([content], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "template.html"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + this.showSuccess("Template saved successfully"); + } + } + handleReset() { + // Reload the template + this.loadTemplateInStep3(); + } + + handleLoad() { + // Create a file input to load template + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".html,.txt"; + input.onchange = (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + editorContent.innerHTML = content; + this.htmlContent = content; + this.showSuccess("Template loaded successfully"); + } + }; + reader.readAsText(file); + } + }; + input.click(); + } + handleFontFamilyChange(event) {} + + handleFontSizeChange(event) { + const fontSize = event.target.value; + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("fontSize", false, fontSize); + this.showSuccess(`Font size changed to ${fontSize}`); + } else { + this.showError("Please select text first"); + } + } + + handleBold() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("bold", false, null); + this.showSuccess("Text made bold"); + } else { + this.showError("Please select text first"); + } + } + + handleItalic() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("italic", false, null); + this.showSuccess("Text made italic"); + } else { + this.showError("Please select text first"); + } + } + + handleUnderline() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("underline", false, null); + this.showSuccess("Text underlined"); + } else { + this.showError("Please select text first"); + } + } + + handleHighlight() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("hiliteColor", false, "#ffff00"); + this.showSuccess("Text highlighted"); + } else { + this.showError("Please select text first"); + } + } + + // Helper function to ensure editor is properly focused and editable + ensureEditorFocus() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + return false; + } + + // Ensure contenteditable is enabled + editorContent.setAttribute("contenteditable", "true"); + editorContent.style.userSelect = "text"; + editorContent.style.webkitUserSelect = "text"; + editorContent.style.cursor = "text"; + + // Focus the editor + editorContent.focus(); + + // Ensure the editor is in the document's active element chain + if (document.activeElement !== editorContent) { + // Try to focus a child element if the parent won't focus + const focusableChild = editorContent.querySelector( + "p, div, span, h1, h2, h3, h4, h5, h6" + ); + if (focusableChild) { + focusableChild.focus(); + } else { + editorContent.focus(); + } + } + + return true; + } + + // ENHANCED BULLET FUNCTION + handleBulletList() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + + editorContent.focus(); + // Use native command so selection toggles bullet list and supports nesting + document.execCommand("insertUnorderedList", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + // ENHANCED NUMBERED LIST FUNCTION + handleNumberedList() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + + editorContent.focus(); + // Use native command so selection toggles ordered list and supports nesting + document.execCommand("insertOrderedList", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + + // Alias for numbered list + handleNumberList() { + this.handleNumberedList(); + } + + // 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 #6b7280"; + element.style.outlineOffset = "2px"; + // Reflect current z-index in toolbox + const target = + element.classList && + element.classList.contains("draggable-image-container") + ? element + : (element.closest && element.closest(".draggable-image-container")) || + element; + const currentZ = + target && target.style && target.style.zIndex ? target.style.zIndex : ""; + this.zIndexInput = currentZ; + } + // 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 #6b7280; + 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 #6b7280; + 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) => { + // Prevent position changes on click + img.style.position = "relative"; + img.style.zIndex = "1000"; + img.style.transition = "none"; // Disable transitions during drag + + // Add resize handles + this.addResizeHandles(img); + + // Add smooth drag event listeners + img.addEventListener("mousedown", this.handleImageMouseDown.bind(this)); + img.addEventListener("mousemove", this.handleImageMouseMove.bind(this)); + img.addEventListener("mouseup", this.handleImageMouseUp.bind(this)); + img.addEventListener("mouseleave", this.handleImageMouseUp.bind(this)); + }); + } + // Smooth drag handlers for images + handleImageMouseDown(e) { + if (e.target.tagName !== "IMG") return; + + e.preventDefault(); + this.isDraggingImage = true; + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + this.dragElement = e.target; + this.dragInitiated = false; // will flip to true only after threshold is exceeded + + // Store initial position + const rect = this.dragElement.getBoundingClientRect(); + const editor = this.template.querySelector(".enhanced-editor-content"); + const editorRect = editor.getBoundingClientRect(); + + this.initialLeft = rect.left - editorRect.left; + this.initialTop = rect.top - editorRect.top; + + // Add dragging class for visual feedback + this.dragElement.style.cursor = "grabbing"; + + // Prevent text selection during drag + document.body.style.userSelect = "none"; + } + + handleImageMouseMove(e) { + if (!this.isDraggingImage || !this.dragElement) return; + + e.preventDefault(); + + const deltaX = e.clientX - this.dragStartX; + const deltaY = e.clientY - this.dragStartY; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // Only start moving the image if the cursor moved beyond a small threshold + if (!this.dragInitiated && distance > 5) { + this.dragInitiated = true; + this.dragElement.style.opacity = "0.85"; + this.dragElement.style.position = "absolute"; + } + if (!this.dragInitiated) return; + + // Update position smoothly after drag actually begins + this.dragElement.style.left = this.initialLeft + deltaX + "px"; + this.dragElement.style.top = this.initialTop + deltaY + "px"; + } + + handleImageMouseUp(e) { + if (!this.isDraggingImage || !this.dragElement) return; + + this.isDraggingImage = false; + + // Restore cursor and opacity + this.dragElement.style.cursor = "grab"; + this.dragElement.style.opacity = ""; + + // Re-enable text selection + document.body.style.userSelect = ""; + + // Save undo state after drag + if (this.dragInitiated) { + this.saveUndoState(); + } + + this.dragElement = null; + this.dragInitiated = false; + } + // 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: #6b7280; + 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, target, handle) { + const container = target.classList.contains("draggable-image-container") + ? target + : target.parentElement; + const startX = event.clientX; + const startY = event.clientY; + const startWidth = container.offsetWidth; + const startHeight = container.offsetHeight; + const startLeft = container.offsetLeft; + const startTop = container.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; + } + + container.style.width = Math.max(50, newWidth) + "px"; + container.style.height = Math.max(50, newHeight) + "px"; + container.style.left = newLeft + "px"; + container.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) { + document.execCommand("justifyLeft", false, null); + this.showSuccess("Text aligned left"); + } else { + this.showError("Please select text first"); + } + } + handleAlignCenter() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("justifyCenter", false, null); + this.showSuccess("Text aligned center"); + } else { + this.showError("Please select text first"); + } + } + + handleAlignRight() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("justifyRight", false, null); + this.showSuccess("Text aligned right"); + } else { + this.showError("Please select text first"); + } + } + + handleTextColorChange(event) { + const color = event.target.value; + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("foreColor", false, color); + this.showSuccess(`Text color changed to ${color}`); + } else { + this.showError("Please select text first"); + } + } + + handleBackgroundColorChange(event) { + const color = event.target.value; + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("hiliteColor", false, color); + this.showSuccess(`Background color changed to ${color}`); + } else { + this.showError("Please select text first"); + } + } + + handleIndent() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + editorContent.focus(); + // For list contexts, execCommand handles nesting properly + document.execCommand("indent", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + + handleOutdent() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + editorContent.focus(); + document.execCommand("outdent", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + + handleFontFamilyChange(event) { + const fontFamily = event.target.value; + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("fontName", false, fontFamily); + this.showSuccess(`Font family changed to ${fontFamily}`); + } else { + this.showError("Please select text first"); + } + } + handleContentChange() { + // Update the HTML content when user types in the editor + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + this.htmlContent = editorContent.innerHTML; + } + } + + openPdfPreview() { + // Get current content from editor + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + this.htmlContent = editorContent.innerHTML; + } + this.showPdfPreview = true; + } + + closePdfPreview() { + this.showPdfPreview = false; + } + + generatePdfFromPreview() { + // Close preview and generate PDF + this.showPdfPreview = false; + this.generatePdfSimple(); + } + + // Property insertion functions + insertPropertyName() { + const propertyName = this.propertyData.Name || "Property Name"; + this.insertTextAtCursor(propertyName); + } + + insertPropertyPrice() { + const price = this.propertyData.Price__c || "$0"; + this.insertTextAtCursor(price); + } + + insertPropertyType() { + const type = this.propertyData.Property_Type__c || "Property Type"; + this.insertTextAtCursor(type); + } + + insertPropertyBedrooms() { + const bedrooms = this.propertyData.Bedrooms__c || "0"; + this.insertTextAtCursor(bedrooms + " Bedrooms"); + } + + insertPropertyBathrooms() { + const bathrooms = this.propertyData.Bathrooms__c || "0"; + this.insertTextAtCursor(bathrooms + " Bathrooms"); + } + + insertPropertySqft() { + const sqft = this.propertyData.Square_Footage__c || "0"; + this.insertTextAtCursor(sqft + " sq ft"); + } + + insertPropertyAddress() { + const address = this.propertyData.Location__c || "Property Address"; + this.insertTextAtCursor(address); + } + + insertPropertyDescription() { + const description = + this.propertyData.Description_English__c || + this.propertyData.pcrm__Description_English__c || + this.propertyData.Description__c || + "Property Description"; + // Wrap into paragraphs and basic formatting + const lines = String(description) + .split(/\n+/) + .map((l) => l.trim()) + .filter(Boolean); + const html = lines.map((l) => `

${l}

`).join(""); + this.insertHtmlAtCursor(html); + } + + // Helper function to insert text at cursor position + insertTextAtCursor(text) { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); + this.showSuccess(`Inserted: ${text}`); + } else { + this.showError("Please place cursor in the editor first"); + } + } + // Helper to insert HTML at cursor + insertHtmlAtCursor(html) { + const selection = window.getSelection(); + if (!selection.rangeCount) { + this.showError("Please place cursor in the editor first"); + return; + } + const range = selection.getRangeAt(0); + range.deleteContents(); + const temp = document.createElement("div"); + temp.innerHTML = html; + const fragment = document.createDocumentFragment(); + while (temp.firstChild) { + fragment.appendChild(temp.firstChild); + } + range.insertNode(fragment); + // Move caret to end of inserted content + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + + // Setup editor click handler to deselect elements + setupEditorClickHandler() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor && !editor.hasClickHandler) { + editor.addEventListener("click", (e) => { + // Enhanced image detection - check multiple ways to find images + let clickedImage = null; + + // Method 1: Direct image click + if ( + e.target.tagName === "IMG" && + e.target.src && + e.target.src.trim() !== "" + ) { + clickedImage = e.target; + } + + // Method 2: Click on element containing an image (children) + if (!clickedImage && e.target.querySelector) { + const containedImg = e.target.querySelector("img"); + 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) { + // Additional validation to ensure we have a valid image + if ( + clickedImage.tagName === "IMG" || + clickedImage.isBackgroundImage + ) { + this.handleImageClick(clickedImage, e); + return; + } else { + } + } + + // 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")) + ) { + // Remove selection from all draggable elements + const allDraggable = editor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + allDraggable.forEach((el) => { + el.classList.remove("selected"); + // Remove any resize handles + const resizeHandles = el.querySelectorAll(".resize-handle"); + resizeHandles.forEach((handle) => handle.remove()); + // Remove any delete buttons + const deleteButtons = el.querySelectorAll( + ".delete-handle, .delete-image-btn" + ); + deleteButtons.forEach((btn) => btn.remove()); + }); + + // Clear the selected element reference + this.clearSelection(); + } + }); + + // Ensure contenteditable is always enabled + editor.setAttribute("contenteditable", "true"); + + // Prevent default scroll behavior when selecting draggable elements + editor.addEventListener("selectstart", (e) => { + if ( + e.target.classList.contains("draggable-element") && + !e.target.classList.contains("draggable-text") + ) { + e.preventDefault(); + } + }); + + // Prevent focus from jumping to top + editor.addEventListener( + "focus", + (e) => { + 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; + } + } + + addDeselectFunctionality() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor || editor.hasDeselectHandler) return; + + editor.addEventListener( + "click", + (e) => { + // Only deselect if we're NOT clicking on: + // 1. Images or image containers + // 2. Resize handles + // 3. Delete buttons + // 4. Any draggable elements + + const isImageClick = + e.target.tagName === "IMG" || + e.target.closest(".draggable-image-container") || + e.target.closest(".draggable-table-container") || + e.target.classList.contains("resize-handle") || + e.target.classList.contains("delete-handle") || + e.target.closest(".resize-handle") || + e.target.closest(".delete-handle"); + + if (!isImageClick) { + this.deselectAllElements(); + } + }, + true + ); // Use capture phase to run before your existing handlers + + editor.hasDeselectHandler = true; + } + + // Keep the deselectAllElements method as I suggested + deselectAllElements() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + const allDraggable = editor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + allDraggable.forEach((el) => { + el.classList.remove("selected"); + el.style.border = ""; + el.style.boxShadow = ""; + + const resizeHandles = el.querySelectorAll(".resize-handle"); + resizeHandles.forEach((handle) => handle.remove()); + + const deleteButtons = el.querySelectorAll( + ".delete-handle, .delete-image-btn" + ); + deleteButtons.forEach((btn) => btn.remove()); + }); + + this.selectedElement = null; + } + + // Insert draggable text element + insertDraggableText() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + this.setupEditorClickHandler(); + const textElement = document.createElement("div"); + textElement.className = "draggable-element draggable-text"; + textElement.contentEditable = true; + textElement.innerHTML = "Click to edit text"; + textElement.style.left = "50px"; + textElement.style.top = "50px"; + textElement.style.width = "200px"; + textElement.style.height = "40px"; + textElement.style.zIndex = "1000"; + textElement.style.position = "absolute"; + + // Add resize handles + this.addResizeHandles(textElement); + + // Add drag functionality + this.makeDraggable(textElement); + + // Focus on the text element after a short delay + setTimeout(() => { + textElement.focus(); + textElement.classList.add("selected"); + }, 100); + + editor.appendChild(textElement); + } + } + + // 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(), + }); + }); + } + } + // 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"; + + if (!imageUrl) { + 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; + + // 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 for main image modal + triggerFileUpload() { + const fileInput = this.template.querySelector(".file-input"); + if (fileInput) { + fileInput.click(); + } else { + } + } + + // Handle file upload + handleFileUpload(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + this.uploadedImageData = e.target.result; + this.selectedImageUrl = e.target.result; + this.selectedImageName = file.name; + this.insertButtonDisabled = false; + + // 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) { + 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(); + } + + // 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() { + this.logCurrentState(); + this.insertSelectedImage(); + } + // Insert selected image + insertSelectedImage() { + // Check if we have a valid image URL + const imageUrl = this.selectedImageUrl || this.uploadedImageData; + const imageName = this.selectedImageName || "Uploaded Image"; + + if (this.insertButtonDisabled || !imageUrl) { + 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"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const editor = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editor) { + // Save undo state before making changes + this.saveUndoState(); + this.setupEditorClickHandler(); + const imageContainer = document.createElement("div"); + imageContainer.className = "draggable-element"; + 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"; + + const img = document.createElement("img"); + img.src = event.target.result; + img.className = "draggable-image"; + img.alt = "Inserted Image"; + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + + imageContainer.appendChild(img); + + // Add resize handles + this.addResizeHandles(imageContainer); + + // Add drag functionality + this.makeDraggable(imageContainer); + + // Select the image after a short delay + setTimeout(() => { + imageContainer.classList.add("selected"); + }, 100); + + editor.appendChild(imageContainer); + } + }; + reader.readAsDataURL(file); + } + }; + input.click(); + } + + // Add resize handles to element + addResizeHandles(element) { + // Avoid duplicate handles + const existing = element.querySelectorAll(".resize-handle"); + if (existing && existing.length > 0) return; + + const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"]; + handles.forEach((direction) => { + const handle = document.createElement("div"); + handle.className = `resize-handle ${direction}`; + handle.style.position = "absolute"; + handle.style.width = "8px"; + handle.style.height = "8px"; + handle.style.background = "#6c63ff"; + handle.style.border = "2px solid white"; + handle.style.borderRadius = "50%"; + handle.style.zIndex = "1001"; + handle.addEventListener("mousedown", (e) => + this.startResize(e, element, direction) + ); + element.appendChild(handle); + }); + } + + // Make element draggable + makeDraggable(element) { + let isDragging = false; + let startX, startY, startLeft, startTop; + let dragStarted = false; + + // Handle mousedown on the element (not on resize handles) + const handleMouseDown = (e) => { + if (e.target.classList.contains("resize-handle")) return; + + isDragging = true; + dragStarted = false; + element.classList.add("selected"); + + // Remove selection from other elements + const editor = element.closest(".enhanced-editor-content"); + if (editor) { + const allDraggable = editor.querySelectorAll(".draggable-element"); + allDraggable.forEach((el) => { + if (el !== element) el.classList.remove("selected"); + }); + } + + startX = e.clientX; + startY = e.clientY; + startLeft = parseInt(element.style.left) || 0; + startTop = parseInt(element.style.top) || 0; + + if (editor) { +editor.initialScrollLeft = editor.scrollLeft; +editor.initialScrollTop = editor.scrollTop; +} + + e.preventDefault(); + e.stopPropagation(); + + // Prevent scrolling while dragging + document.body.style.overflow = "hidden"; + }; + const handleMouseMove = (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + // Only start dragging if mouse moved more than 5px + if (!dragStarted && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) { + dragStarted = true; + element.classList.add("dragging"); + } + + if (dragStarted) { + const editor = element.closest(".enhanced-editor-content"); + const editorRect = editor + ? editor.getBoundingClientRect() + : { + left: 0, + top: 0, + width: window.innerWidth, + height: window.innerHeight, + }; + + // Calculate new position relative to editor + // let newLeft = startLeft + deltaX; + // let newTop = startTop + deltaY; + + // Calculate new position relative to editor (FIXED for smooth scroll) + // Use existing 'editor' and 'editorRect' variables declared above to avoid redeclaration + let newLeft = startLeft + deltaX; + let newTop = startTop + deltaY; + +// Account for editor scroll position for smooth dragging +if (editor) { +const currentScrollLeft = editor.scrollLeft; +const currentScrollTop = editor.scrollTop; + +// Adjust position based on scroll changes since drag started +const scrollDeltaX = currentScrollLeft - (editor.initialScrollLeft || 0); +const scrollDeltaY = currentScrollTop - (editor.initialScrollTop || 0); + +newLeft -= scrollDeltaX; +newTop -= scrollDeltaY; +} + + + + // Keep element within editor bounds - use scrollHeight for full template height + const maxWidth = editor ? editor.clientWidth : editorRect.width; + const maxHeight = editor ? editor.scrollHeight : editorRect.height; + + newLeft = Math.max( + 0, + Math.min(newLeft, maxWidth - element.offsetWidth) + ); + newTop = Math.max( + 0, + Math.min(newTop, maxHeight - element.offsetHeight) + ); + + element.style.left = newLeft + "px"; + element.style.top = newTop + "px"; + element.style.position = "absolute"; + } + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleMouseUp = () => { + if (isDragging) { + isDragging = false; + dragStarted = false; + element.classList.remove("dragging"); + + // Restore scrolling + document.body.style.overflow = ""; + } + }; + + element.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + // Handle click to select without dragging + element.addEventListener("click", (e) => { + if (!dragStarted) { + e.stopPropagation(); + element.classList.add("selected"); + + // Remove selection from other elements + const editor = element.closest(".enhanced-editor-content"); + if (editor) { + const allDraggable = editor.querySelectorAll(".draggable-element"); + allDraggable.forEach((el) => { + if (el !== element) el.classList.remove("selected"); + }); + } + } + }); + + // Handle text editing for text elements + if (element.classList.contains("draggable-text")) { + element.addEventListener("dblclick", (e) => { + if (!dragStarted) { + e.stopPropagation(); + element.focus(); + element.style.cursor = "text"; + } + }); + + element.addEventListener("input", (e) => { + e.stopPropagation(); + }); + + element.addEventListener("keydown", (e) => { + e.stopPropagation(); + }); + } + } + + // Start resize operation + startResize(e, element, direction) { + e.preventDefault(); + e.stopPropagation(); + + const startX = e.clientX; + const startY = e.clientY; + const startWidth = parseInt(element.style.width) || element.offsetWidth; + const startHeight = parseInt(element.style.height) || element.offsetHeight; + const startLeft = parseInt(element.style.left) || 0; + const startTop = parseInt(element.style.top) || 0; + + // Add resizing class and prevent scrolling + element.classList.add("resizing"); + document.body.style.overflow = "hidden"; + + const handleMouseMove = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newWidth = startWidth; + let newHeight = startHeight; + let newLeft = startLeft; + let newTop = startTop; + + const editor = element.closest(".enhanced-editor-content"); + const editorRect = editor + ? editor.getBoundingClientRect() + : { width: window.innerWidth, height: window.innerHeight }; + // Use scrollHeight for full template height + const maxWidth = editor ? editor.clientWidth : editorRect.width; + const maxHeight = editor ? editor.scrollHeight : editorRect.height; + + switch (direction) { + case "se": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + break; + case "sw": + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + break; + case "ne": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + newHeight = Math.max(20, startHeight - deltaY); + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + case "nw": + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max(20, startHeight - deltaY); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + case "e": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + break; + case "w": + newWidth = Math.max(50, startWidth - deltaX); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + break; + case "s": + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + break; + case "n": + newHeight = Math.max(20, startHeight - deltaY); + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + } + + // Apply the new dimensions and position + element.style.width = newWidth + "px"; + element.style.height = newHeight + "px"; + element.style.left = newLeft + "px"; + element.style.top = newTop + "px"; + element.style.position = "absolute"; + }; + + const handleMouseUp = () => { + element.classList.remove("resizing"); + document.body.style.overflow = ""; + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + handleBringForward() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + const currentZIndex = parseInt(element.style.zIndex) || 0; + element.style.zIndex = currentZIndex + 1; + this.showSuccess(`Z-index increased to ${currentZIndex + 1}`); + } + } else { + this.showError("Please select an element first"); + } + } + + handleSendBackward() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + const currentZIndex = parseInt(element.style.zIndex) || 0; + element.style.zIndex = Math.max(0, currentZIndex - 1); + this.showSuccess( + `Z-index decreased to ${Math.max(0, currentZIndex - 1)}` + ); + } + } else { + this.showError("Please select an element first"); + } + } + + setZIndex() { + const zIndexInput = this.template.querySelector("#zIndexInput"); + const zIndex = parseInt(zIndexInput.value) || 0; + + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + element.style.zIndex = zIndex; + this.showSuccess(`Z-index set to ${zIndex}`); + } + } else { + this.showError("Please select an element first"); + } + } + + // Helper method to make elements draggable + makeDraggable(element) { + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + element.addEventListener("mousedown", (e) => { + // Only start dragging if clicking on the element itself (not on text inside) + if ( + e.target === element || + (element.classList.contains("draggable-text-box") && + e.target.parentNode === element) + ) { + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + isDragging = true; + element.style.cursor = "grabbing"; + } + }); + + document.addEventListener("mousemove", (e) => { + if (isDragging) { + e.preventDefault(); + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + + element.style.left = currentX + "px"; + element.style.top = currentY + "px"; + } + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { + isDragging = false; + element.style.cursor = element.classList.contains("draggable-text-box") + ? "text" + : "move"; + } + }); + } + // Helper method to make elements resizable + makeResizable(element) { + const resizer = document.createElement("div"); + resizer.className = "resizer"; + resizer.style.position = "absolute"; + resizer.style.width = "10px"; + resizer.style.height = "10px"; + resizer.style.background = "#667eea"; + resizer.style.borderRadius = "50%"; + resizer.style.bottom = "-5px"; + resizer.style.right = "-5px"; + resizer.style.cursor = "se-resize"; + resizer.style.zIndex = "1001"; + + element.appendChild(resizer); + + let isResizing = false; + let startWidth, startHeight, startX, startY; + + resizer.addEventListener("mousedown", (e) => { + isResizing = true; + startX = e.clientX; + startY = e.clientY; + startWidth = parseInt(element.style.width) || element.offsetWidth; + startHeight = parseInt(element.style.height) || element.offsetHeight; + e.stopPropagation(); + }); + + document.addEventListener("mousemove", (e) => { + if (isResizing) { + const newWidth = startWidth + (e.clientX - startX); + const newHeight = startHeight + (e.clientY - startY); + + if (newWidth > 50) element.style.width = newWidth + "px"; + if (newHeight > 30) element.style.height = newHeight + "px"; + } + }); + + document.addEventListener("mouseup", () => { + isResizing = false; + }); + } + + insertText() { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + // Create draggable and resizable text box + const textBox = document.createElement("div"); + textBox.className = "draggable-text-box"; + textBox.contentEditable = true; + textBox.textContent = "Double-click to edit text"; + textBox.style.position = "absolute"; + textBox.style.left = "50px"; + textBox.style.top = "50px"; + textBox.style.width = "150px"; + textBox.style.height = "40px"; + textBox.style.minWidth = "100px"; + textBox.style.minHeight = "30px"; + textBox.style.padding = "8px"; + textBox.style.border = "2px solid #ddd"; + textBox.style.borderRadius = "4px"; + textBox.style.backgroundColor = "white"; + textBox.style.cursor = "text"; + textBox.style.zIndex = "1000"; + textBox.style.fontSize = "14px"; + textBox.style.fontFamily = "Arial, sans-serif"; + textBox.style.color = "#333"; + textBox.style.boxSizing = "border-box"; + textBox.style.outline = "none"; + + // Handle Enter key to keep text in place + textBox.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Insert a line break instead of creating new element + document.execCommand("insertLineBreak", false); + } + }); + + // Handle selection like Word/Google Docs + textBox.addEventListener("click", (e) => { + e.stopPropagation(); + this.selectElement(textBox); + }); + + // Make text box draggable + this.makeDraggable(textBox); + + // Make text box resizable + this.makeResizable(textBox); + + previewFrame.appendChild(textBox); + textBox.focus(); + + // Select the text for easy editing + const range = document.createRange(); + range.selectNodeContents(textBox); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + } + insertImage() { + // Create file input for local image upload + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "image/*"; + fileInput.style.display = "none"; + + fileInput.onchange = (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + // Create draggable and resizable image container + const imageContainer = document.createElement("div"); + imageContainer.className = "draggable-image-container"; + imageContainer.style.position = "absolute"; + imageContainer.style.left = "50px"; + imageContainer.style.top = "50px"; + imageContainer.style.width = "300px"; + imageContainer.style.height = "200px"; + imageContainer.style.cursor = "move"; + imageContainer.style.zIndex = "1000"; + imageContainer.style.border = "2px dashed #667eea"; + imageContainer.style.borderRadius = "4px"; + imageContainer.style.overflow = "hidden"; + + const img = document.createElement("img"); + img.src = e.target.result; + img.alt = "Inserted Image"; + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.borderRadius = "4px"; + img.style.boxShadow = "0 2px 8px rgba(0,0,0,0.1)"; + + // Handle selection like Word/Google Docs + imageContainer.addEventListener("click", (e) => { + e.stopPropagation(); + this.selectElement(imageContainer); + }); + + // Add delete button (only visible when selected) + const deleteBtn = document.createElement("button"); + deleteBtn.className = "delete-btn"; + deleteBtn.innerHTML = "×"; + deleteBtn.style.position = "absolute"; + deleteBtn.style.top = "-10px"; + deleteBtn.style.right = "-10px"; + deleteBtn.style.width = "20px"; + deleteBtn.style.height = "20px"; + deleteBtn.style.borderRadius = "50%"; + deleteBtn.style.background = "#ff4757"; + deleteBtn.style.color = "white"; + deleteBtn.style.border = "none"; + deleteBtn.style.cursor = "pointer"; + deleteBtn.style.fontSize = "16px"; + deleteBtn.style.fontWeight = "bold"; + deleteBtn.style.zIndex = "1002"; + deleteBtn.style.opacity = "0"; + deleteBtn.style.transition = "opacity 0.2s ease"; + + deleteBtn.onclick = (e) => { + e.stopPropagation(); + imageContainer.remove(); + }; + + imageContainer.appendChild(deleteBtn); + + // Make image container draggable + this.makeDraggable(imageContainer); + + // Make image container resizable + this.makeResizable(imageContainer); + + imageContainer.appendChild(img); + previewFrame.appendChild(imageContainer); + } + }; + reader.readAsDataURL(file); + } + }; + + // Trigger file selection + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + } + // Helper method to duplicate an image + duplicateImage(originalContainer) { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + const newContainer = originalContainer.cloneNode(true); + newContainer.style.left = + parseInt(originalContainer.style.left) + 20 + "px"; + newContainer.style.top = + parseInt(originalContainer.style.top) + 20 + "px"; + newContainer.style.zIndex = parseInt(originalContainer.style.zIndex) + 1; + + // Reattach event listeners + this.makeDraggable(newContainer); + this.makeResizable(newContainer); + + // Update control panel event listeners + const controlPanel = newContainer.querySelector(".image-control-panel"); + if (controlPanel) { + controlPanel.addEventListener("mouseenter", () => { + controlPanel.style.opacity = "1"; + }); + + controlPanel.addEventListener("mouseleave", () => { + controlPanel.style.opacity = "0"; + }); + } + + previewFrame.appendChild(newContainer); + this.showSuccess("Image duplicated successfully!"); + } + } + + // Select element like Word/Google Docs + selectElement(element) { + // Remove selection from all other elements + const allElements = this.template.querySelectorAll( + ".draggable-text-box, .draggable-image-container" + ); + allElements.forEach((el) => { + el.classList.remove("selected"); + el.style.border = el.classList.contains("draggable-text-box") + ? "2px solid #ddd" + : "2px solid #ddd"; + + // Hide delete buttons + const deleteBtn = el.querySelector(".delete-btn"); + if (deleteBtn) { + deleteBtn.style.opacity = "0"; + } + }); + + // Select current element + element.classList.add("selected"); + element.style.border = "2px solid #667eea"; + + // Show delete button + const deleteBtn = element.querySelector(".delete-btn"); + if (deleteBtn) { + deleteBtn.style.opacity = "1"; + } + + // Show selection handles + this.showSelectionHandles(element); + } + + // Show selection handles like Word/Google Docs + showSelectionHandles(element) { + // Remove existing handles + const existingHandles = element.querySelectorAll(".selection-handle"); + existingHandles.forEach((handle) => handle.remove()); + + // Create selection handles + const handles = [ + { position: "top-left", cursor: "nw-resize" }, + { position: "top-right", cursor: "ne-resize" }, + { position: "bottom-left", cursor: "sw-resize" }, + { position: "bottom-right", cursor: "se-resize" }, + ]; + + handles.forEach((handle) => { + const handleElement = document.createElement("div"); + handleElement.className = "selection-handle"; + handleElement.style.position = "absolute"; + handleElement.style.width = "8px"; + handleElement.style.height = "8px"; + handleElement.style.background = "#667eea"; + handleElement.style.border = "1px solid white"; + handleElement.style.borderRadius = "50%"; + handleElement.style.cursor = handle.cursor; + handleElement.style.zIndex = "1003"; + + // Position handles + switch (handle.position) { + case "top-left": + handleElement.style.top = "-4px"; + handleElement.style.left = "-4px"; + break; + case "top-right": + handleElement.style.top = "-4px"; + handleElement.style.right = "-4px"; + break; + case "bottom-left": + handleElement.style.bottom = "-4px"; + handleElement.style.left = "-4px"; + break; + case "bottom-right": + handleElement.style.bottom = "-4px"; + handleElement.style.right = "-4px"; + break; + } + + element.appendChild(handleElement); + }); + } + addShape() {} + + // Helper method to build amenities list dynamically + buildAmenitiesList(data) { + let amenitiesList = ""; + + // First priority: Use amenities array if available + if ( + data.amenities && + Array.isArray(data.amenities) && + data.amenities.length > 0 + ) { + amenitiesList = data.amenities + .map( + (amenity) => `
  • ${amenity}
  • ` + ) + .join(""); + } + // Second priority: Use individual amenity fields if available + else if ( + data.amenity1 || + data.amenity2 || + data.amenity3 || + data.amenity4 || + data.amenity5 || + data.amenity6 || + data.amenity7 || + data.amenity8 || + data.amenity9 || + data.amenity10 + ) { + const individualAmenities = [ + data.amenity1, + data.amenity2, + data.amenity3, + data.amenity4, + data.amenity5, + data.amenity6, + data.amenity7, + data.amenity8, + data.amenity9, + data.amenity10, + ].filter((amenity) => amenity && amenity.trim() !== ""); + + amenitiesList = individualAmenities + .map( + (amenity) => `
  • ${amenity}
  • ` + ) + .join(""); + } + // Fallback: Use default luxury amenities + else { + amenitiesList = ` +
  • 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
  • + `; + } + + return amenitiesList; + } + // Helper method to build amenities list for THE VERTICE template + buildAmenitiesListForVertice(data) { + let amenitiesList = ""; + + // First priority: Use amenities array if available + if ( + data.amenities && + Array.isArray(data.amenities) && + data.amenities.length > 0 + ) { + amenitiesList = data.amenities + .map( + (amenity) => + `
  • ${amenity}
  • ` + ) + .join(""); + } + // Second priority: Use individual amenity fields if available + else if ( + data.amenity1 || + data.amenity2 || + data.amenity3 || + data.amenity4 || + data.amenity5 || + data.amenity6 || + data.amenity7 || + data.amenity8 || + data.amenity9 || + data.amenity10 + ) { + const individualAmenities = [ + data.amenity1, + data.amenity2, + data.amenity3, + data.amenity4, + data.amenity5, + data.amenity6, + data.amenity7, + data.amenity8, + data.amenity9, + data.amenity10, + ].filter((amenity) => amenity && amenity.trim() !== ""); + + amenitiesList = individualAmenities + .map( + (amenity) => + `
  • ${amenity}
  • ` + ) + .join(""); + } + // Fallback: Use default luxury amenities + else { + amenitiesList = ` +
  • Rooftop Infinity Pool
  • +
  • Fitness Center
  • +
  • Residents' Sky Lounge
  • +
  • Private Cinema Room
  • +
  • Wellness Spa & Sauna
  • +
  • Business Center
  • +
  • 24/7 Concierge
  • +
  • Secure Parking
  • + `; + } + + return amenitiesList; + } + + // Image Review Methods + openImageReview() { + this.showImageReview = true; + // Auto-select category will be handled in loadPropertyImages // Default to Interior category + } + + closeImageReview() { + this.showImageReview = false; + this.currentImageIndex = 0; + this.currentImage = null; + } + + selectCategory(event) { + let category; + + // 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 { + return; + } + + this.selectedCategory = category; + + // Filter real property images by category + this.filterImagesByCategory(category); + } + + // Add new method to show all images (no filtering) + filterImagesByCategory(category) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + return; + } + + // Show all images instead of filtering by category + this.propertyImages = this.realPropertyImages; + this.totalImages = this.realPropertyImages.length; + + if (this.realPropertyImages.length > 0) { + this.currentImage = this.realPropertyImages[0]; + this.currentImageIndex = 0; + } else { + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + } + } + + getImagesForCategory(category) { + // First try to get real images from Salesforce + if (this.realPropertyImages && this.realPropertyImages.length > 0) { + // Filter images by category + const categoryImages = this.realPropertyImages + .filter((img) => { + // Handle case-insensitive matching and variations + const imgCategory = img.category ? img.category.toLowerCase() : ""; + const searchCategory = category.toLowerCase(); + + // Direct match + if (imgCategory === searchCategory) { + return true; + } + + // Category mapping for common variations + const categoryMappings = { + interior: ["interior", "inside", "indoor"], + exterior: ["exterior", "outside", "outdoor", "facade"], + kitchen: ["kitchen", "dining"], + bedroom: ["bedroom", "bed", "room"], + "living area": ["living", "lounge", "sitting"], + parking: ["parking", "garage"], + anchor: ["anchor", "main", "hero"], + maps: ["map", "location", "area"], + }; + + const mappings = categoryMappings[searchCategory] || [searchCategory]; + return mappings.some((mapping) => imgCategory.includes(mapping)); + }) + .map((img) => ({ + url: img.url || `/servlet/FileDownload?file=${img.id}`, + id: img.id, + title: img.name || `${category} Image`, + category: category, + })); + + if (categoryImages.length > 0) { + return categoryImages; + } + } + + // Get images based on the selected template and property + if (!this.selectedTemplateId || !this.propertyData) { + return []; + } + + // Template-specific image mapping + const templateImages = this.getTemplateSpecificImages(category); + if (templateImages && templateImages.length > 0) { + return templateImages; + } + // No images found + return []; + } + getTemplateSpecificImages(category) { + const templateId = this.selectedTemplateId; + const propertyData = this.propertyData; + + // Map category names to property fields + const categoryFieldMap = { + Interior: ["interiorImage1", "interiorImage2", "interiorImage3"], + Exterior: ["exteriorImage1", "exteriorImage2", "exteriorImage3"], + Kitchen: ["kitchenImage1", "kitchenImage2", "kitchenImage3"], + Bedroom: ["bedroomImage1", "bedroomImage2", "bedroomImage3"], + "Living Area": [ + "livingAreaImage1", + "livingAreaImage2", + "livingAreaImage3", + ], + Parking: ["parkingImage1", "parkingImage2"], + Anchor: ["anchorImage1", "anchorImage2"], + Maps: ["mapImage1", "mapImage2"], + }; + + const fields = categoryFieldMap[category] || []; + const images = []; + + // Check if property has images for this category + fields.forEach((field) => { + if (propertyData[field] && propertyData[field].trim() !== "") { + images.push({ + url: propertyData[field], + title: `${category} - ${field.replace("Image", " View ")}`, + category: category, + }); + } + }); + + return images; + } + + generateImagesFromPropertyData(category, propertyData) { + const images = []; + + // Generate placeholder images based on property type and category + const propertyType = propertyData.propertyType || "Property"; + const location = propertyData.city || propertyData.community || "Location"; + + // Create sample images based on category and property data + const sampleImages = { + Interior: [ + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + ], + Exterior: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Kitchen: [ + "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Bedroom: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + "Living Area": [ + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + ], + Parking: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Anchor: [ + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Maps: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + }; + + const urls = sampleImages[category] || []; + urls.forEach((url, index) => { + images.push({ + url: url, + title: `${propertyType} - ${category} View ${index + 1}`, + category: category, + }); + }); + + return images; + } + + nextImage() { + if (this.currentImageIndex < this.totalImages - 1) { + this.currentImageIndex++; + this.updateCurrentImage(); + } else { + } + } + + previousImage() { + if (this.currentImageIndex > 0) { + this.currentImageIndex--; + this.updateCurrentImage(); + } else { + } + } + + // Add new method to update current image + updateCurrentImage() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return; + } + + // Use all images instead of filtering by category + if ( + this.realPropertyImages.length > 0 && + this.currentImageIndex < this.realPropertyImages.length + ) { + this.currentImage = this.realPropertyImages[this.currentImageIndex]; + // Revert: only enable drag & drop; no auto-wrap on click + const imgEl = this.template.querySelector( + ".property-image-step2, .review-image" + ); + if (imgEl) { + imgEl.setAttribute("draggable", "true"); + imgEl.addEventListener( + "dragstart", + this.handleImageDragStart.bind(this) + ); + imgEl.style.cursor = "zoom-in"; + imgEl.onclick = () => { + const w = window.open(); + if (w && w.document) { + w.document.write( + `` + ); + } + }; + } + } + } + + // Ensure editor is always editable + ensureEditorEditable() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.setAttribute("contenteditable", "true"); + editor.style.userSelect = "text"; + editor.style.webkitUserSelect = "text"; + editor.style.cursor = "text"; + + // Remove any potential pointer-events restrictions + editor.style.pointerEvents = "auto"; + + // Add event listeners to ensure editing works + if (!editor.hasEditListeners) { + editor.addEventListener("input", this.handleContentChange.bind(this)); + editor.addEventListener("keydown", (e) => { + // Handle undo/redo and other special keys + this.handleEditorKeydown(e); + // Allow all key presses for editing + e.stopPropagation(); + }); + editor.addEventListener("keyup", this.handleContentChange.bind(this)); + editor.addEventListener("paste", this.handleContentChange.bind(this)); + // NEW: single-click any image to show resize controls + editor.addEventListener( + "click", + (e) => { + const target = e.target; + if ( + target && + target.tagName && + target.tagName.toLowerCase() === "img" + ) { + e.preventDefault(); + e.stopPropagation(); + this.selectDraggableElement(target); + } + }, + true + ); + + editor.hasEditListeners = true; + } + } + } + // Connected callback to initialize + connectedCallback() { + // Ensure editor is editable after component loads + setTimeout(() => { + this.ensureEditorEditable(); + }, 1000); + + // Keyboard shortcuts for Word-like experience + this._keyHandler = (e) => { + if (this.currentStep !== 3) return; + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + const mod = isMac ? e.metaKey : e.ctrlKey; + if (!mod) return; + switch (e.key.toLowerCase()) { + case "b": + e.preventDefault(); + this.handleBold(); + break; + case "i": + e.preventDefault(); + this.handleItalic(); + break; + case "u": + e.preventDefault(); + this.handleUnderline(); + break; + case "z": + e.preventDefault(); + this.undo(); + break; + case "y": + e.preventDefault(); + this.redo(); + break; + } + }; + window.addEventListener("keydown", this._keyHandler); + + // Auto-fit when window resizes in Step 3 + this._resizeHandler = () => { + if (this.currentStep === 3 && this.fitToWidth) this.fitToWidth(); + }; + window.addEventListener("resize", this._resizeHandler); + } + // Called after template loads + renderedCallback() { + this.ensureEditorEditable(); + this.setupEditorClickHandler(); + this.addDeselectFunctionality(); + + // Save initial state for undo functionality + setTimeout(() => { + this.saveUndoState(); + }, 100); + // Ensure initial fit + if (this.currentStep === 3 && this.fitToWidth) { + setTimeout(() => this.fitToWidth(), 0); + } + } + + // Test editor functionality - can be called from toolbar + testEditor() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.focus(); + this.ensureEditorEditable(); + } + } + + // Helper method to determine if element is likely positioned over an image + isElementLikelyOverImage(element) { + if (!element || element.tagName === "IMG") return false; + + // Be much more restrictive - only trigger for elements that are clearly over images + const style = window.getComputedStyle(element); + + // Only check for images underneath if the element has strong indicators + const isTransparentText = this.isTransparentTextElement(element, style); + const isPositionedOverlay = this.isPositionedOverlay(element, style); + const hasImageParent = this.hasDirectImageParent(element); + + // Only return true if there are very specific indicators + return ( + (isTransparentText && hasImageParent) || + (isPositionedOverlay && this.checkBackgroundImages(element)) + ); + } + + isTransparentTextElement(element, style) { + // Text elements that are likely overlays + const textTags = [ + "P", + "SPAN", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "A", + "STRONG", + "EM", + "B", + "I", + "DIV", + ]; + const isTextElement = textTags.includes(element.tagName); + + // Check if background is transparent or semi-transparent + const bg = style.backgroundColor; + const isTransparent = + bg === "rgba(0, 0, 0, 0)" || + bg === "transparent" || + bg === "" || + (bg.includes("rgba") && + (bg.includes(", 0)") || + (bg.includes(", 0.") && parseFloat(bg.split(",")[3]) < 0.5))); + + return isTextElement && isTransparent; + } + + isPositionedOverlay(element, style) { + const isPositioned = ["absolute", "relative", "fixed"].includes( + style.position + ); + const hasLowOpacity = parseFloat(style.opacity) < 1; + const hasTransformOrZ = + style.transform !== "none" || parseInt(style.zIndex) > 0; + + return isPositioned && (hasLowOpacity || hasTransformOrZ); + } + + hasDirectImageParent(element) { + // Only check immediate parent and grandparent + let current = element.parentElement; + let depth = 0; + + while (current && depth < 2) { + if (current.querySelector("img")) return true; + current = current.parentElement; + depth++; + } + return false; + } + + checkBackgroundImages(element) { + let current = element; + let depth = 0; + + while (current && depth < 5) { + const style = window.getComputedStyle(current); + if (style.backgroundImage && style.backgroundImage !== "none") { + return true; + } + current = current.parentElement; + depth++; + } + return false; + } + + // Helper method to check if there are actual image indicators before expensive search + hasImageIndicators(clickedElement, x, y, editor) { + // Quick check: if the element or its parents have background images + let current = clickedElement; + let depth = 0; + + while (current && current !== editor && depth < 3) { + const style = window.getComputedStyle(current); + if (style.backgroundImage && style.backgroundImage !== "none") { + return true; + } + current = current.parentElement; + depth++; + } + + // Quick check: if there are any img elements in the nearby area + const allElementsAtPoint = document.elementsFromPoint + ? document.elementsFromPoint(x, y) + : []; + const hasDirectImage = allElementsAtPoint.some((el) => { + return ( + el.tagName === "IMG" || + (el.querySelector && el.querySelector("img")) || + window.getComputedStyle(el).backgroundImage !== "none" + ); + }); + + if (!hasDirectImage) { + // Final check: look for images in the clicked element's container + const container = clickedElement.closest("div, section, article"); + if (container && container !== editor) { + return container.querySelector("img") !== null; + } + } + + return hasDirectImage; + } + // Helper method to find images under click coordinates - including low z-index images + findImageUnderClick(x, y, editor) { + // Check for images directly at the click point + const allElementsAtPoint = document.elementsFromPoint + ? document.elementsFromPoint(x, y) + : []; + + for (const element of allElementsAtPoint) { + // Skip if it's part of the UI (toolbar, navigation, etc.) + if ( + element.closest(".editor-left") || + element.closest(".toolbar-section") || + element.closest(".step-navigation") || + element.closest(".page-controls") + ) { + continue; + } + + // Direct image - highest priority + if (element.tagName === "IMG") { + return element; + } + + // Check for contained images + const containedImg = element.querySelector("img"); + if (containedImg) { + return containedImg; + } + + // Check for background images + const style = window.getComputedStyle(element); + if (style.backgroundImage && style.backgroundImage !== "none") { + return { + src: style.backgroundImage.slice(5, -2), + element: element, + isBackgroundImage: true, + }; + } + } + + 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 + const debugInfo = { + 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++; + } else { + // Different image clicked, reset counter + this.imageClickCount = 1; + this.lastClickedImage = clickedImage; + } + + // Set timeout to reset counter after 1 second + this.clickTimeout = setTimeout(() => { + this.imageClickCount = 0; + this.lastClickedImage = null; + }, 1000); + + // Check if we've reached exactly 3 clicks + if (this.imageClickCount === 3) { + 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) { + return; + } + + this.selectedImageElement = imageElement; + this.showImageReplacement = true; + this.replacementActiveTab = "property"; + + // Use smart category selection like Step 2 + this.replacementSelectedCategory = this.findFirstAvailableCategory(); + + 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) { + } else if (imageElement.tagName === "IMG") { + } else { + } + } + + closeImageReplacement() { + this.showImageReplacement = false; + 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(); + } + + selectLocalUploadTab() { + this.replacementActiveTab = "upload"; + this.uploadedImagePreview = null; + + // Force re-render to ensure the upload area is visible + this.forceRerender(); + + // Add a small delay to ensure DOM is updated + setTimeout(() => { + const uploadDropzone = this.template.querySelector(".upload-dropzone"); + if (uploadDropzone) { + } else { + } + }, 100); + } + selectReplacementCategory(event) { + const category = event.target.dataset.category; + + this.replacementSelectedCategory = category; + this.filterReplacementImages(); + + // Update active state for category buttons + 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"); + } + }); + } + + filterReplacementImages() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + 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; + }); + + this.filteredReplacementImages = filteredImages.map((img, index) => ({ + id: `${this.replacementSelectedCategory}-${index}`, + url: img.url, + title: img.title || img.name || `Image ${index + 1}`, + category: img.category || img.pcrm__Category__c || "None", + })); + } + + selectReplacementImage(event) { + const imageUrl = event.currentTarget.dataset.imageUrl; + + if (!imageUrl) { + this.showError("Failed to get image URL. Please try again."); + return; + } + + this.replaceImageSrc(imageUrl); + this.closeImageReplacement(); + } + triggerImageReplacementFileUpload() { + // Try to find the image upload input in the replacement modal + const fileInput = this.template.querySelector(".image-upload-input"); + if (fileInput) { + // Reset the input to allow selecting the same file again + fileInput.value = ""; + fileInput.click(); + } else { + // Fallback: create a new input programmatically + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.style.display = "none"; + input.onchange = (e) => this.handleImageUpload(e); + document.body.appendChild(input); + input.click(); + // Don't remove immediately, let the handler process first + setTimeout(() => { + if (document.body.contains(input)) { + document.body.removeChild(input); + } + }, 100); + } + } + + handleImageUpload(event) { + const file = event.target.files[0]; + + if (!file) { + return; + } + + // Validate file type + if (!file.type.startsWith("image/")) { + this.showError("Please select a valid image file (JPG, PNG, GIF, WebP)"); + return; + } + + // Validate file size (e.g., max 10MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + this.showError("File size must be less than 10MB"); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + this.uploadedImagePreview = e.target.result; + + // Show success message + this.showSuccess( + '✅ Image uploaded successfully! Click "Use This Image" to apply it.' + ); + + // Force re-render to show the preview + this.forceRerender(); + }; + + reader.onerror = (e) => { + this.showError("Error reading the selected file. Please try again."); + }; + + reader.readAsDataURL(file); + } + + useUploadedImage() { + if (this.uploadedImagePreview) { + this.replaceImageSrc(this.uploadedImagePreview); + this.closeImageReplacement(); + } + } + + // Drag and drop handlers for image upload + handleDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + const dropzone = event.currentTarget; + dropzone.classList.add("drag-over"); + } + + handleDragLeave(event) { + event.preventDefault(); + event.stopPropagation(); + const dropzone = event.currentTarget; + dropzone.classList.remove("drag-over"); + } + + handleDrop(event) { + event.preventDefault(); + event.stopPropagation(); + + const dropzone = event.currentTarget; + dropzone.classList.remove("drag-over"); + + const files = event.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + + // Validate file type + if (!file.type.startsWith("image/")) { + this.showError("Please drop a valid image file (JPG, PNG, GIF, WebP)"); + return; + } + + // Validate file size (e.g., max 10MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + this.showError("File size must be less than 10MB"); + return; + } + + // Process the dropped file + const reader = new FileReader(); + reader.onload = (e) => { + this.uploadedImagePreview = e.target.result; + + // Show success message + this.showSuccess( + '✅ Image uploaded successfully! Click "Use This Image" to apply it.' + ); + + // Force re-render to show the preview + this.forceRerender(); + }; + + reader.onerror = (e) => { + this.showError("Error reading the dropped file. Please try again."); + }; + + reader.readAsDataURL(file); + } + } + replaceImageSrc(newImageUrl) { + if (!this.selectedImageElement || !newImageUrl) { + return; + } + + try { + // Save undo state before making changes + this.saveUndoState(); + + // Handle background images + if (this.selectedImageElement.isBackgroundImage) { + // Use the stored original element reference if available + if (this.selectedImageElement.originalElement) { + this.selectedImageElement.originalElement.style.backgroundImage = `url("${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}")`; + this.showSuccess("Background image updated successfully!"); + return; + } + } + } + } + this.showError("Failed to update background image. Please try again."); + return; + } + + // Handle regular img elements + if (this.selectedImageElement.tagName === "IMG") { + this.selectedImageElement.src = newImageUrl; + + // If the image is inside a draggable container, ensure it maintains proper styling + const draggableContainer = + this.selectedImageElement.closest(".draggable-element"); + if (draggableContainer) { + // Reset any max-width/max-height constraints that might interfere + this.selectedImageElement.style.width = "100%"; + this.selectedImageElement.style.height = "100%"; + this.selectedImageElement.style.objectFit = "cover"; + } + + this.showSuccess("Image updated successfully!"); + } else { + this.showError("Failed to update image: Invalid element type"); + } + } catch (error) { + this.showError("Failed to update image. Please try again."); + } + } + + // Template Save/Load/Export Methods + openSaveDialog() { + this.showSaveDialog = true; + this.saveTemplateName = ""; + document.body.style.overflow = "hidden"; + } + closeSaveDialog() { + this.showSaveDialog = false; + document.body.style.overflow = ""; + } + + handleSaveNameChange(event) { + this.saveTemplateName = event.target.value; + } + + saveTemplate() { + if (!this.saveTemplateName.trim()) { + this.showError("Please enter a template name"); + return; + } + + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("No template content to save"); + return; + } + + const templateData = { + id: Date.now().toString(), + name: this.saveTemplateName.trim(), + content: editor.innerHTML, + pageSize: this.selectedPageSize, + baseTemplateId: this.selectedTemplateId, + propertyId: this.selectedPropertyId, + savedAt: new Date().toISOString(), + thumbnail: this.generateThumbnail(editor), + }; + + // Get existing saved templates from localStorage + const savedTemplates = JSON.parse( + localStorage.getItem("savedTemplates") || "[]" + ); + savedTemplates.push(templateData); + localStorage.setItem("savedTemplates", JSON.stringify(savedTemplates)); + + this.loadSavedTemplates(); + this.closeSaveDialog(); + this.showSuccess(`Template "${this.saveTemplateName}" saved successfully!`); + } + + generateThumbnail(editor) { + // Create a simple text preview of the template + const textContent = editor.textContent || editor.innerText || ""; + return ( + textContent.substring(0, 100) + (textContent.length > 100 ? "..." : "") + ); + } + + openLoadDialog() { + this.loadSavedTemplates(); + this.showLoadDialog = true; + document.body.style.overflow = "hidden"; + } + + closeLoadDialog() { + this.showLoadDialog = false; + document.body.style.overflow = ""; + } + + loadSavedTemplates() { + const saved = JSON.parse(localStorage.getItem("savedTemplates") || "[]"); + this.savedTemplates = saved.map((template) => ({ + ...template, + formattedDate: new Date(template.savedAt).toLocaleDateString(), + })); + } + + loadTemplate(event) { + const templateId = event.currentTarget.dataset.templateId; + const template = this.savedTemplates.find((t) => t.id === templateId); + + if (template) { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.innerHTML = template.content; + this.selectedPageSize = template.pageSize || "A4"; + this.htmlContent = template.content; + + // Update page size radio buttons + const pageRadios = this.template.querySelectorAll( + 'input[name="pageSize"]' + ); + pageRadios.forEach((radio) => { + radio.checked = radio.value === this.selectedPageSize; + }); + + this.closeLoadDialog(); + this.showSuccess(`Template "${template.name}" loaded successfully!`); + + // Re-setup editor functionality + setTimeout(() => { + this.ensureEditorEditable(); + this.setupEditorClickHandler(); + }, 100); + } + } + } + + deleteTemplate(event) { + event.stopPropagation(); + const templateId = event.currentTarget.dataset.templateId; + const template = this.savedTemplates.find((t) => t.id === templateId); + + if ( + template && + confirm(`Are you sure you want to delete "${template.name}"?`) + ) { + const savedTemplates = JSON.parse( + localStorage.getItem("savedTemplates") || "[]" + ); + const filtered = savedTemplates.filter((t) => t.id !== templateId); + localStorage.setItem("savedTemplates", JSON.stringify(filtered)); + this.loadSavedTemplates(); + this.showSuccess("Template deleted successfully"); + } + } + exportHtml() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("No template content to export"); + return; + } + + // Use the raw HTML content + const htmlContent = editor.innerHTML; + + // Create a complete HTML document + const fullHtml = ` + + + + + Property Brochure + + + +
    + ${htmlContent} +
    + +`; + + this.exportedHtml = fullHtml; + this.showHtmlDialog = true; + document.body.style.overflow = "hidden"; + } + + closeHtmlDialog() { + this.showHtmlDialog = false; + document.body.style.overflow = ""; + } + + copyHtmlToClipboard() { + if (navigator.clipboard) { + navigator.clipboard + .writeText(this.exportedHtml) + .then(() => { + this.showSuccess("HTML copied to clipboard!"); + }) + .catch(() => { + this.fallbackCopyToClipboard(); + }); + } else { + this.fallbackCopyToClipboard(); + } + } + + fallbackCopyToClipboard() { + const textArea = document.createElement("textarea"); + textArea.value = this.exportedHtml; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + this.showSuccess("HTML copied to clipboard!"); + } catch (err) { + this.showError("Failed to copy HTML"); + } + + document.body.removeChild(textArea); + } + + downloadHtml() { + const blob = new Blob([this.exportedHtml], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `property-brochure-${Date.now()}.html`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + this.showSuccess("HTML file downloaded!"); + } + + // Table Dialog Methods + openTableDialog() { + this.showTableDialog = true; + document.body.style.overflow = "hidden"; + } + + closeTableDialog() { + this.showTableDialog = false; + document.body.style.overflow = ""; + } + + handleTableRowsChange(event) { + this.tableRows = parseInt(event.target.value) || 3; + } + + handleTableColsChange(event) { + this.tableCols = parseInt(event.target.value) || 3; + } + + handleHeaderChange(event) { + this.includeHeader = event.target.checked; + } + + insertTable() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + // Save undo state + this.saveUndoState(); + + // Create table element using our new method (draggable/resizeable container like images) + const tableContainer = this.createTableElement(); + editor.appendChild(tableContainer); + // Default placement similar to images + tableContainer.style.left = "50px"; + tableContainer.style.top = "50px"; + // Enable drag + resize + this.addTableResizeHandles(tableContainer); + this.makeDraggable(tableContainer); + this.setupTableEventListeners(tableContainer); + + this.closeTableDialog(); + this.showSuccess("Table inserted successfully!"); + } + + // ===== DYNAMIC IMAGE REPLACEMENT UTILITIES ===== + + // Get first image from a specific category + getFirstImageByCategory(category) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return null; + } + + const categoryImages = this.realPropertyImages.filter((img) => { + const imgCategory = img.category || img.pcrm__Category__c; + return ( + imgCategory && imgCategory.toLowerCase() === category.toLowerCase() + ); + }); + + return categoryImages.length > 0 ? categoryImages[0] : null; + } + // Direct method to get exterior image URL + getExteriorImageUrl() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return ""; + } + + // 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) { + return exteriorImages[0].url; + } + + // If no exterior, use first available image + if (this.realPropertyImages.length > 0) { + return this.realPropertyImages[0].url; + } + + return ""; + } + + // Direct method to get maps image URL + getMapsImageUrl() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return ""; + } + + // 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; + return ( + category && + (category.toLowerCase() === "maps" || + category.toLowerCase().includes("maps")) + ); + }); + + if (mapsImages.length > 0) { + return mapsImages[0].url; + } + + // Look for anchor images as fallback + const anchorImages = this.realPropertyImages.filter((img) => { + const category = img.category || img.pcrm__Category__c; + return ( + category && + (category.toLowerCase() === "anchor" || + category.toLowerCase().includes("anchor")) + ); + }); + + if (anchorImages.length > 0) { + return anchorImages[0].url; + } + + return ""; + } + + // Method to replace background-image URLs in CSS at runtime + replaceBackgroundImagesInHTML(htmlContent) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return htmlContent; + } + + const exteriorImageUrl = this.getExteriorImageUrl(); + + // 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, (match) => { + return exteriorImageUrl ? `background-image: url('${exteriorImageUrl}')` : "background-image: none"; + }); + + return updatedHTML; + } + // Method to dynamically update CSS background-image rules after template loads + updateCSSBackgroundImages() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + if (!this.realPropertyImages || this.realPropertyImages.length === 0) + return; + const exteriorImageUrl = this.getExteriorImageUrl(); + // Scope to styles inside the editor only + const styleElements = editor.querySelectorAll("style"); + styleElements.forEach((styleElement) => { + const cssText = styleElement.textContent || ""; + const backgroundImagePattern = + /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; + const updatedCSS = cssText.replace(backgroundImagePattern, (match) => { + return exteriorImageUrl + ? `background-image: url('${exteriorImageUrl}')` + : "background-image: none"; + }); + if (updatedCSS !== cssText) styleElement.textContent = updatedCSS; + }); + // Update inline background-image styles only within editor + const elementsWithBackground = editor.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, (match) => { + return exteriorImageUrl + ? `background-image: url('${exteriorImageUrl}')` + : "background-image: none"; + }); + if (updatedStyle !== currentStyle) + element.setAttribute("style", updatedStyle); + }); + } + + // 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() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return []; + } + + const uncategorized = this.realPropertyImages.filter( + (img) => + !img.category || + img.category.trim() === "" || + img.category.toLowerCase() === "none" + ); + + return uncategorized; + } + + // Smart image replacement - tries multiple categories in order of preference + getSmartImageForSection(sectionType, fallbackUrl) { + 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", + ]; + + for (const category of categories) { + const image = this.getFirstImageByCategory(category); + if (image && image.url) { + return image.url; + } + } + + return fallbackUrl; + } + + // Generate Property Gallery HTML for uncategorized images + generatePropertyGalleryHTML() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return ""; + } + + let galleryHTML = ""; + this.realPropertyImages.forEach((image, index) => { + const title = + image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; + galleryHTML += `${title}`; + }); + + return galleryHTML; + } + // Generate gallery HTML for a provided subset of images + generatePropertyGalleryHTMLForImages(imagesSubset) { + if (!imagesSubset || imagesSubset.length === 0) { + return ""; + } + let galleryHTML = ""; + imagesSubset.forEach((image, index) => { + const title = + image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; + galleryHTML += `${title}`; + }); + return galleryHTML; + } + + // ===== TABLE DRAG AND DROP FUNCTIONALITY ===== + + // Handle table drag start + handleTableDragStart(event) { + 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) { + // Allow dropping tables and images + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + } + + // Handle editor drop + handleEditorDrop(event) { + event.preventDefault(); + + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + const dataType = event.dataTransfer.getData("text/plain"); + if (dataType === "image" && this.currentImage) { + // Insert draggable image at drop + const img = document.createElement("img"); + img.src = this.currentImage.url; + img.style.maxWidth = "300px"; + img.style.height = "auto"; + img.className = "draggable-image"; + + const container = document.createElement("div"); + container.className = "draggable-image-container"; + container.style.position = "absolute"; + container.style.left = + event.clientX - editor.getBoundingClientRect().left + "px"; + container.style.top = + event.clientY - editor.getBoundingClientRect().top + "px"; + container.appendChild(img); + editor.appendChild(container); + this.makeImagesDraggableAndResizable([img]); + this.showSuccess("Image inserted via drag and drop!"); + return; + } + + if (!this.isDraggingTable || !this.draggedTableData) { + return; + } + + // Remove visual feedback + this.removeTableDragFeedback(); + + // Get drop position + // editor already resolved above + + // 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 (use draggable-table-container to get drag/resize behavior) + const container = document.createElement("div"); + container.className = "draggable-table-container"; + container.setAttribute("data-table-id", tableId); + container.style.cssText = + "position: absolute; left: 0; top: 0; width: 400px; min-width: 200px; min-height: 150px; z-index: 1000; border: 2px dashed #667eea; border-radius: 8px; background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;"; + + // 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; + } + } + + // Insert into editor (append at end for simplicity) + editor.appendChild(tableElement); + // Position at drop point + tableElement.style.left = + Math.max(0, Math.min(x, editor.clientWidth - tableElement.offsetWidth)) + + "px"; + tableElement.style.top = + Math.max( + 0, + Math.min(y, editor.scrollHeight - tableElement.offsetHeight) + ) + "px"; + + // Add drag/resize to the new table + this.addTableResizeHandles(tableElement); + this.makeDraggable(tableElement); + 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; + + 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"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + // Save undo state before making changes + this.saveUndoState(); + + // Create draggable text element + const textElement = document.createElement("div"); + textElement.className = "draggable-element draggable-text"; + textElement.contentEditable = true; + textElement.innerHTML = "Click to edit text"; + + // Position absolutely for free placement + textElement.style.position = "absolute"; + textElement.style.left = "20px"; + textElement.style.top = "20px"; + textElement.style.minWidth = "150px"; + textElement.style.minHeight = "30px"; + textElement.style.padding = "8px"; + textElement.style.border = "2px dashed #4f46e5"; + textElement.style.borderRadius = "4px"; + textElement.style.backgroundColor = "rgba(255, 255, 255, 0.9)"; + textElement.style.zIndex = "1000"; + textElement.style.cursor = "move"; + textElement.style.fontFamily = "Inter, sans-serif"; + textElement.style.fontSize = "14px"; + textElement.style.lineHeight = "1.4"; + + // Add to editor + editor.appendChild(textElement); + + // Make it draggable and resizable + this.addResizeHandles(textElement); + this.makeDraggable(textElement); + + // Select the text for immediate editing + setTimeout(() => { + textElement.focus(); + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(textElement); + selection.removeAllRanges(); + selection.addRange(range); + }, 100); + } + + // Undo/Redo functionality + saveUndoState() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + const currentState = { + content: editor.innerHTML, + timestamp: Date.now(), + }; + + this.undoStack.push(currentState); + + // Limit undo stack size + if (this.undoStack.length > this.maxUndoSteps) { + this.undoStack.shift(); + } + + // Clear redo stack when new action is performed + this.redoStack = []; + } + undo() { + if (this.undoStack.length === 0) return; + + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Save current state to redo stack + const currentState = { + content: editor.innerHTML, + timestamp: Date.now(), + }; + this.redoStack.push(currentState); + + // Restore previous state + const previousState = this.undoStack.pop(); + editor.innerHTML = previousState.content; + + // Re-setup event handlers for any dynamic elements + this.setupEditorEventHandlers(); + } + + redo() { + if (this.redoStack.length === 0) return; + + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Save current state to undo stack + const currentState = { + content: editor.innerHTML, + timestamp: Date.now(), + }; + this.undoStack.push(currentState); + + // Restore next state + const nextState = this.redoStack.pop(); + editor.innerHTML = nextState.content; + + // Re-setup event handlers for any dynamic elements + this.setupEditorEventHandlers(); + } + + // 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) { + 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(); + 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) + if ( + (event.ctrlKey || event.metaKey) && + event.key === "z" && + !event.shiftKey + ) { + event.preventDefault(); + this.undo(); + return; + } + + // Check for Ctrl+Y or Ctrl+Shift+Z (Redo) + if ( + (event.ctrlKey || event.metaKey) && + (event.key === "y" || (event.key === "z" && event.shiftKey)) + ) { + event.preventDefault(); + this.redo(); + return; + } + + // Save state before modifications (with debouncing) + if (!this.pendingUndoSave) { + this.pendingUndoSave = true; + setTimeout(() => { + this.saveUndoState(); + this.pendingUndoSave = false; + }, 500); + } + } + // 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 + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + const allDraggable = editor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + allDraggable.forEach((el) => { + if (el !== element) { + el.classList.remove("selected"); + // Remove any resize handles + const resizeHandles = el.querySelectorAll(".resize-handle"); + resizeHandles.forEach((handle) => handle.remove()); + // Remove any delete buttons + const deleteButtons = el.querySelectorAll( + ".delete-handle, .delete-image-btn" + ); + deleteButtons.forEach((btn) => btn.remove()); + } + }); + } + + // Add selection to clicked element + element.classList.add("selected"); + + // Add resize handles and controls to the selected element + if (element.classList.contains("draggable-image-container")) { + const img = element.querySelector("img"); + if (img) { + this.addResizeHandles(img); + this.addDeleteButton(element); + } + } else if (element.classList.contains("draggable-table-container")) { + this.addTableResizeHandles(element); + this.addDeleteButton(element); + } else if (element.tagName && element.tagName.toLowerCase() === "img") { + // If already wrapped, ensure handles on container + if ( + element.parentElement && + element.parentElement.classList && + element.parentElement.classList.contains("draggable-image-container") + ) { + const container = element.parentElement; + this.addResizeHandles(container); + this.makeDraggable(container); + this.addDeleteButton(container); + this.highlightSelectedElement(container); + return; + } + // Wrap plain image and add handles; preserve on-screen size and position + const editor = this.template.querySelector(".enhanced-editor-content"); + const rect = element.getBoundingClientRect(); + const editorRect = editor + ? editor.getBoundingClientRect() + : { left: 0, top: 0 }; + const currentWidth = element.offsetWidth; + const currentHeight = element.offsetHeight; + + const container = document.createElement("div"); + container.className = "draggable-image-container"; + container.style.position = "absolute"; + container.style.left = + rect.left - editorRect.left + (editor ? editor.scrollLeft : 0) + "px"; + container.style.top = + rect.top - editorRect.top + (editor ? editor.scrollTop : 0) + "px"; + container.style.zIndex = + window.getComputedStyle(element).zIndex || "auto"; + container.style.display = "inline-block"; + + // Move the image into container and preserve size + // Set container and image sizing + container.style.width = currentWidth + "px"; + container.style.height = currentHeight + "px"; + element.style.width = "100%"; + element.style.height = "100%"; + element.style.display = "block"; + element.style.objectFit = "cover"; + + if (editor) { + editor.appendChild(container); + } else { + element.parentNode.insertBefore(container, element); + } + container.appendChild(element); + container.classList.add("no-frame"); + this.addResizeHandles(container); + this.makeDraggable(container); + this.addDeleteButton(container); + this.highlightSelectedElement(container); + } + } + + // Add delete button to element + addDeleteButton(element) { + // Remove existing delete button if any + const existingDelete = element.querySelector( + ".delete-handle, .delete-image-btn" + ); + if (existingDelete) { + existingDelete.remove(); + } + + const deleteBtn = document.createElement("div"); + deleteBtn.className = "delete-handle"; + deleteBtn.innerHTML = "×"; + deleteBtn.style.cssText = ` + position: absolute; + top: -10px; + right: -10px; + width: 20px; + height: 20px; + background: #dc3545; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 12px; + font-weight: bold; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + `; + + deleteBtn.addEventListener("click", (e) => { + e.stopPropagation(); + element.remove(); + }); + + element.appendChild(deleteBtn); + } + // Add table resize handles + addTableResizeHandles(tableContainer) { + // Remove existing resize handles if any + const existingHandles = tableContainer.querySelectorAll(".resize-handle"); + existingHandles.forEach((handle) => handle.remove()); + + const table = tableContainer.querySelector("table"); + if (!table) return; + + // Add resize handles to table corners + const positions = ["nw", "ne", "sw", "se"]; + positions.forEach((pos) => { + const handle = document.createElement("div"); + handle.className = `resize-handle resize-${pos}`; + handle.dataset.position = pos; + handle.style.cssText = ` + position: absolute; + width: 8px; + height: 8px; + background: #007bff; + border: 1px solid white; + cursor: ${pos}-resize; + z-index: 1000; + `; + + // Position the handle + switch (pos) { + case "nw": + handle.style.top = "-4px"; + handle.style.left = "-4px"; + break; + case "ne": + handle.style.top = "-4px"; + handle.style.right = "-4px"; + break; + case "sw": + handle.style.bottom = "-4px"; + handle.style.left = "-4px"; + break; + case "se": + handle.style.bottom = "-4px"; + handle.style.right = "-4px"; + break; + } + + // Enable resizing using shared startResize + handle.addEventListener("mousedown", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.startResize(e, tableContainer, pos); + }); + + tableContainer.appendChild(handle); + }); + } + + // 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() {} + + // Test method to manually set an image (for debugging) + testSetImage() { + 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(); + } +} \ No newline at end of file diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js index 44f5e4f..ae60515 100644 --- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js @@ -228,18 +228,6 @@ export default class PropertyTemplateSelector extends LightningElement { maxUndoSteps = 50; // Computed properties for image replacement tabs - get modernHomePreview() { - const data = this.propertyData || {}; - const raw = - data.pcrm__Description_English__c || - data.Description_English__c || - data.descriptionEnglish || - data.Description__c || - data.description || - "Description not available."; - // Lightweight formatting for preview (single paragraph, trimmed) - return (raw || "").toString().trim().replace(/\s+/g, " "); - } get propertyImagesTabClass() { return this.replacementActiveTab === "property" ? "source-tab active" @@ -598,13 +586,6 @@ export default class PropertyTemplateSelector extends LightningElement { event.currentTarget.classList.add("selected"); } } catch (e) {} - - // Immediately regenerate preview if already in Step 3 - try { - if (this.currentStep === 3 && this.selectedPropertyId) { - this.loadTemplateInStep3(); - } - } catch (e) {} } resetTemplateSelections() { @@ -941,23 +922,36 @@ export default class PropertyTemplateSelector extends LightningElement { } } - replaceStaticWithDynamic(content) { -if (!this.propertyData || !content) return content; + if (!this.propertyData || !content) return content; -let result = content; + let result = content; -// Replace hardcoded values with actual property data -result = result.replace(/Concorde Tower/g, this.propertyData.propertyName || 'Property Name'); -result = result.replace(/AED 81,999/g, this.propertyData.rentPriceMin || this.propertyData.price || 'Price'); -result = result.replace(/Modern Villa/g, this.propertyData.propertyName || 'Property Name'); -result = result.replace(/AED 2,500,000/g, this.propertyData.salePriceMin || this.propertyData.price || 'Price'); -result = result.replace(/Dubai/g, this.propertyData.city || 'Location'); -result = result.replace(/This beautiful property offers exceptional value and modern amenities\./g, -this.propertyData.descriptionEnglish || 'Property description'); + // Replace hardcoded values with actual property data + result = result.replace( + /Concorde Tower/g, + this.propertyData.propertyName || "Property Name" + ); + result = result.replace( + /AED 81,999/g, + this.propertyData.rentPriceMin || this.propertyData.price || "Price" + ); + result = result.replace( + /Modern Villa/g, + this.propertyData.propertyName || "Property Name" + ); + result = result.replace( + /AED 2,500,000/g, + this.propertyData.salePriceMin || this.propertyData.price || "Price" + ); + result = result.replace(/Dubai/g, this.propertyData.city || "Location"); + result = result.replace( + /This beautiful property offers exceptional value and modern amenities\./g, + this.propertyData.descriptionEnglish || "Property description" + ); -return result; -} + return result; + } goToStep(event) { const step = parseInt(event.currentTarget.dataset.step); @@ -1021,8 +1015,22 @@ return result; async loadTemplateInStep3() { if (this.selectedTemplateId && this.selectedPropertyId) { try { - // Always regenerate to reflect latest template edits - this.cachedTemplateContent = null; + // Use cached content if available to prevent regeneration + if ( + this.cachedTemplateContent && + this.cachedTemplateContent.templateId === this.selectedTemplateId && + this.cachedTemplateContent.propertyId === this.selectedPropertyId + ) { + this.htmlContent = this.cachedTemplateContent.html; + this.updatePreviewPages(); + this.updatePreviewFrameSize(this.selectedPageSize); + setTimeout(() => { + this.updatePageCountForSize(this.selectedPageSize); + this.updatePageCount(); + this.fitToWidth && this.fitToWidth(); + }, 100); + return; + } // Ensure property images are loaded before creating template if (this.realPropertyImages.length === 0) { @@ -1099,13 +1107,6 @@ return result; // Clear cached content when selecting a new property this.cachedTemplateContent = null; - // Reset images to avoid showing previous property's images - this.realPropertyImages = []; - this.propertyImages = []; - this.totalImages = 0; - this.currentImage = null; - this.currentImageIndex = 0; - if (this.selectedPropertyId) { this.loadPropertyData(); // Auto-scroll to property preview section after a short delay to ensure data is loaded @@ -2209,29 +2210,15 @@ return result; switch (this.selectedTemplateId) { case "blank-template": return this.createBlankTemplate(); - case "everkind-template": - return this.createEverkindTemplate(); - case "shift-template": - return this.createShiftTemplate(); - case "saintbarts-template": - return this.createSaintbartsTemplate(); - case "learnoy-template": - return this.createLearnoyTemplate(); - case "leafamp-template": - return this.createLeafampTemplate(); - case "coreshift-template": - return this.createCoreshiftTemplate(); case "modern-home-template": return this.createModernHomeTemplate(); case "grand-oak-villa-template": // Grand Oak Villa (black theme with gold accents) return this.createGrandOakVillaTemplate(); - case "sample-template": - return this.createSampleTemplate(); case "serenity-house-template": return this.createSerenityHouseTemplate(); case "luxury-mansion-template": - return this.createVerticeTemplate(); + return this.createLuxuryMansionTemplate(); default: return this.createBlankTemplate(); } @@ -2541,182 +2528,6 @@ return result; `; } - createEverkindTemplate() { - return `Luxury Villa

    Luxury Villa Template

    Elegance. Precision. Luxury.

    `; - } - createShiftTemplate() { - 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.pcrm__Title_English__c || data.Name || data.propertyName || ""; - 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(); - // Gallery HTML generated once in each template scope when needed - const allGalleryImages = this.realPropertyImages || []; - const firstGalleryCount = Math.min(4, allGalleryImages.length); - const firstPageImages = allGalleryImages.slice(0, firstGalleryCount); - const propertyGallery = this.generatePropertyGalleryHTML(); - const interiorImage = this.getSmartImageForSection( - "interior", - "" - ); - const kitchenImage = this.getSmartImageForSection( - "kitchen", - "" - ); - - // Debug logging - - // Generate property gallery for uncategorized images (already declared above) - - 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.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.pcrm__Title_English__c || data.Name || data.propertyName || ""; - 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."; - - // Get smart images - use direct method for exterior - const exteriorImage = this.getExteriorImageUrl(); - const interiorImage = this.getSmartImageForSection("interior", ""); - const bedroomImage = this.getSmartImageForSection("bedroom", ""); - - // 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.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.pcrm__Title_English__c || data.Name || data.propertyName || ""; - - // 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() { - 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.pcrm__Title_English__c || data.Name || data.propertyName || ""; - 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 - - // 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() { - return `Commercial Properties

    Commercial Properties Template

    Unlock Your Business Potential

    `; - } createModernHomeTemplate() { const data = this.propertyData || {}; const propertyName = data.Name || data.propertyName || "Property Name"; @@ -2728,12 +2539,9 @@ return result; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const area = data.Square_Feet__c || data.area || "N/A"; const description = this.formatDescriptionForPDF( - data.pcrm__Description_English__c || data.Description_English__c || - data.descriptionEnglish || - data.Description__c || data.description || - "Property description not available." + "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity." ); const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; @@ -2746,92 +2554,510 @@ return result; const agentEmail = data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A"; - // Gallery images for 3x2 grid (fallbacks to exterior) - const exteriorImage = this.getExteriorImageUrl(); - const galleryImg1 = this.getSmartImageForSection("interior", exteriorImage); - const galleryImg2 = this.getSmartImageForSection("bedroom", exteriorImage); - const galleryImg3 = this.getSmartImageForSection("kitchen", exteriorImage); - const galleryImg4 = this.getSmartImageForSection("bathroom", exteriorImage); - const g1 = galleryImg1 || exteriorImage; - const g2 = galleryImg2 || exteriorImage; - const g3 = galleryImg3 || exteriorImage; - const g4 = galleryImg4 || exteriorImage; - // Dynamic gallery and amenities const propertyGallery = this.generatePropertyGalleryHTML(); const amenitiesHTML = this.generateAmenitiesHTML(data); - 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" - }
    `; + return ` + + + + + Property Brochure - A4 Size + + + + + +
    +
    +
    +

    [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] +
    +
    +
    + + + +`; } createAsgar1Template() { @@ -2961,156 +3187,104 @@ return result; data.Name || data.propertyName || data.pcrm__Title_English__c || - "Property Name"; + "The Grand Oak Villa"; const location = data.Address__c || data.location || - "Location"; + "123 Luxury Lane, Prestige City, PC 45678"; const price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.price || - "Price on Request"; + "$4,500,000"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; - const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; - const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const bedrooms = data.Bedrooms__c || data.bedrooms || "5"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "6"; const squareFeet = - data.Square_Feet__c || data.squareFeet || data.area || "N/A"; - const status = (data.Status__c || data.status || "N/A").toString(); + data.Square_Feet__c || data.squareFeet || data.area || "6,200"; + const status = (data.Status__c || data.status || "FOR SALE").toString(); // Enhanced property details - const propertyType = data.Property_Type__c || data.propertyType; - const yearBuilt = data.Build_Year__c || data.yearBuilt; + const propertyType = data.Property_Type__c || data.propertyType || "Villa"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || "2020"; const furnishing = - data.Furnished__c || data.furnishing; - const parking = data.Parking_Spaces__c || data.parking; + data.Furnished__c || data.furnishing || "Fully Furnished"; + const parking = data.Parking_Spaces__c || data.parking || "2"; const description = this.formatDescriptionForPDF( data.Description_English__c || data.descriptionEnglish || - data.description + data.description || + "An exquisite villa offering unparalleled luxury and sophistication in one of the most prestigious locations." ); - const floor = data.Floor__c || data.floor; + const floor = data.Floor__c || data.floor || "Ground Floor"; const maintenanceFee = - data.Maintenance_Fee__c || data.maintenanceFee; - const serviceCharge = data.Service_Charge__c || data.serviceCharge; - - // Pricing ranges for screenshot fields - const rentMinVal = data.Rent_Price_Min__c || data.rentPriceMin; - const rentMaxVal = data.Rent_Price_Max__c || data.rentPriceMax; - const saleMinVal = data.Sale_Price_Min__c || data.salePriceMin; - const saleMaxVal = data.Sale_Price_Max__c || data.salePriceMax; - - // Location details for screenshot fields - const cityVal = data.City__c || data.city; - const communityVal = data.Community__c || data.community; - const subCommunityVal = data.Sub_Community__c || data.subCommunity; - const localityVal = data.Locality__c || data.locality; - const towerVal = data.Tower__c || data.tower; - const unitNumberVal = data.Unit_Number__c || data.unitNumber; - - // Offering and availability - const offeringTypeVal = data.Offering_Type__c || data.offeringType; - const availableTo = data.Available_To__c || data.availableTo || data.rentAvailableTo; + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; // Additional property details - const lotSize = data.Lot_Size__c || data.lotSize; - const heating = data.Heating__c || data.heating; - const cooling = data.Cooling__c || data.cooling; - const roof = data.Roof__c || data.roof; - const exterior = data.Exterior__c || data.exterior; - const foundation = data.Foundation__c || data.foundation; - const utilities = data.Utilities__c || data.utilities; - const zoning = data.Zoning__c || data.zoning; - const hoa = data.HOA__c || data.hoa; - const hoaFee = data.HOA_Fee__c || data.hoaFee; - const taxYear = data.Tax_Year__c || data.taxYear; - const taxAmount = data.Tax_Amount__c || data.taxAmount; - const lastSold = data.Last_Sold__c || data.lastSold; + const lotSize = data.Lot_Size__c || data.lotSize || "0.5 acres"; + const heating = data.Heating__c || data.heating || "Central Air"; + const cooling = data.Cooling__c || data.cooling || "Central Air"; + const roof = data.Roof__c || data.roof || "Tile"; + const exterior = data.Exterior__c || data.exterior || "Stone & Brick"; + const foundation = data.Foundation__c || data.foundation || "Concrete"; + const utilities = data.Utilities__c || data.utilities || "All Connected"; + const zoning = data.Zoning__c || data.zoning || "Residential"; + const hoa = data.HOA__c || data.hoa || "Yes"; + const hoaFee = data.HOA_Fee__c || data.hoaFee || "$500/month"; + const taxYear = data.Tax_Year__c || data.taxYear || "2024"; + const taxAmount = data.Tax_Amount__c || data.taxAmount || "$12,000/year"; + const lastSold = data.Last_Sold__c || data.lastSold || "2020"; const lastSoldPrice = - data.Last_Sold_Price__c || data.lastSoldPrice; + data.Last_Sold_Price__c || data.lastSoldPrice || "$3,200,000"; // Location and POI data - const schools = data.Schools__c || data.schools; + const schools = data.Schools__c || data.schools || "5 min drive"; const shoppingCenters = - data.Shopping_Centers__c || data.shoppingCenters; + data.Shopping_Centers__c || data.shoppingCenters || "10 min drive"; const airportDistance = - data.Airport_Distance__c || data.airportDistance; + data.Airport_Distance__c || data.airportDistance || "25 min drive"; const nearbyLandmarks = - data.Nearby_Landmarks__c || data.nearbyLandmarks; + data.Nearby_Landmarks__c || data.nearbyLandmarks || "City Center 15 min"; const transportation = - data.Transportation__c || data.transportation; - const hospitals = data.Hospitals__c || data.hospitals; + data.Transportation__c || data.transportation || "Metro 5 min walk"; + const hospitals = data.Hospitals__c || data.hospitals || "10 min drive"; const beachDistance = - data.Beach_Distance__c || data.beachDistance; + data.Beach_Distance__c || data.beachDistance || "30 min drive"; const metroDistance = - data.Metro_Distance__c || data.metroDistance; + data.Metro_Distance__c || data.metroDistance || "5 min walk"; // Additional information - const petFriendly = data.Pet_Friendly__c || data.petFriendly; + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "Yes"; const smokingAllowed = - data.Smoking_Allowed__c || data.smokingAllowed; + data.Smoking_Allowed__c || data.smokingAllowed || "No"; const availableFrom = - data.Available_From__c || data.availableFrom; + data.Available_From__c || data.availableFrom || "Immediate"; const minimumContract = - data.Minimum_Contract__c || data.minimumContract; + data.Minimum_Contract__c || data.minimumContract || "12 months"; const securityDeposit = - data.Security_Deposit__c || data.securityDeposit; + data.Security_Deposit__c || data.securityDeposit || "2 months rent"; const utilitiesIncluded = data.Utilities_Included__c || - data.utilitiesIncluded; + data.utilitiesIncluded || + "Water, Electricity"; const internetIncluded = - data.Internet_Included__c || data.internetIncluded; - const cableIncluded = data.Cable_Included__c || data.cableIncluded; + data.Internet_Included__c || data.internetIncluded || "Yes"; + const cableIncluded = data.Cable_Included__c || data.cableIncluded || "Yes"; - // Minimal Grand Oak Villa template: screenshot fields only; no gallery or extras - const basicPropertyName = - data.Name || data.propertyName || data.pcrm__Title_English__c; - const basicLocation = data.Address__c || data.location; - const basicStatus = (data.Status__c || data.status).toString(); - const basicPropertyType = data.Property_Type__c || data.propertyType; - const specBedrooms = data.Bedrooms__c || data.bedrooms; - const specBathrooms = data.Bathrooms__c || data.bathrooms; - const specFloor = data.Floor__c || data.floor; - const specSize = - data.Square_Feet__c || data.squareFeet || data.area; - const specBuildYear = - data.Build_Year__c || data.yearBuilt || data.buildYear; - const cName = data.contactName || data.Agent_Name__c; - const cEmail = data.contactEmail || data.Agent_Email__c; - const cPhone = data.contactPhone || data.Agent_Phone__c; - const rentMin = data.Rent_Price_Min__c || data.rentPriceMin; - const rentMax = data.Rent_Price_Max__c || data.rentPriceMax; - const saleMin = data.Sale_Price_Min__c || data.salePriceMin; - const saleMax = data.Sale_Price_Max__c || data.salePriceMax; - const city = data.City__c || data.city; - const community = data.Community__c || data.community; - const subCommunity = data.Sub_Community__c || data.subCommunity; - const locality = data.Locality__c || data.locality; - const tower = data.Tower__c || data.tower; - const unitNumber = data.Unit_Number__c || data.unitNumber; - const parkingSpaces = - data.Parking_Spaces__c || data.parkingSpaces || data.parking; - const furnishedSimple = data.Furnished__c || data.furnishing; - const offeringType = data.Offering_Type__c || data.offeringType; - const availableFromMin = - data.Available_From__c || data.rentAvailableFrom || data.availableFrom; - const availableToMax = - data.Available_To__c || data.rentAvailableTo || data.availableTo; - - if (false) return `${basicPropertyName} - Brochure

    ${basicPropertyName}

    ${basicLocation}
    ${basicStatus}

    Basic Information

    Property Name:${basicPropertyName}
    Property Type:${basicPropertyType}
    Status:${basicStatus}

    Specifications

    Bedrooms:${specBedrooms}
    Bathrooms:${specBathrooms}
    Floor:${specFloor}
    Size:${specSize}
    Build Year:${specBuildYear}

    Contact Details

    Name:${cName}
    Email:${cEmail}
    Phone:${cPhone}

    Pricing Information

    Rent Price (Min):${rentMin}
    Rent Price (Max):${rentMax}
    Sale Price (Min):${saleMin}
    Sale Price (Max):${saleMax}

    Location Details

    City:${city}
    Community:${community}
    Sub Community:${subCommunity}
    Locality:${locality}
    Tower:${tower}
    Unit Number:${unitNumber}

    Amenities & Features

    Parking Spaces:${parkingSpaces}
    Furnished:${furnishedSimple}
    Offering Type:${offeringType}

    Availability

    Available From:${availableFromMin}
    Available To:${availableToMax}
    `; // Agent and owner information - const agentName = data.Agent_Name__c || data.agentName; + const agentName = data.Agent_Name__c || data.agentName || "Olivia Sterling"; const agentPhone = - data.Agent_Phone__c || data.agentPhone; + data.Agent_Phone__c || data.agentPhone || "(555) 987-6543"; const agentEmail = - data.Agent_Email__c || data.agentEmail; - const ownerName = data.Owner_Name__c || data.ownerName; + data.Agent_Email__c || data.agentEmail || "olivia@elysianestates.com"; + const ownerName = data.Owner_Name__c || data.ownerName || "John & Jane Doe"; const ownerPhone = - data.Owner_Phone__c || data.ownerPhone; + data.Owner_Phone__c || data.ownerPhone || "(555) 111-2222"; const ownerEmail = - data.Owner_Email__c || data.ownerEmail; + data.Owner_Email__c || data.ownerEmail || "owner@email.com"; // Get smart images const exteriorImage = this.getExteriorImageUrl(); const interiorImage1 = this.getSmartImageForSection( @@ -3137,659 +3311,435 @@ return result; const propertyGalleryHTML = this.generatePropertyGalleryHTML(data); // Return the complete Grand Oak Villa template with all dynamic data - return `Grand Oak Villa - ${propertyName}
    ${status.toUpperCase()}

    ${propertyName}

    ${location}

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

    Description

    ${description}

    Specifications

    Reference ID: ${referenceId}
    Status: ${status}
    Type: ${propertyType}
    Year Built: ${yearBuilt}
    Floor: ${floor}
    Parking: ${parking}
    Furnishing: ${furnishing}
    Lot Size: ${lotSize}
    Heating: ${heating}
    Cooling: ${cooling}
    Roof: ${roof}
    Exterior: ${exterior}
    Foundation: ${foundation}
    Utilities: ${utilities}
    Zoning: ${zoning}
    HOA: ${hoa}
    HOA Fee: ${hoaFee}

    Amenities & Features

      ${amenitiesHTML}
    Agent: ${agentName} | ${agentPhone} | ${agentEmail}
    Owner: ${ownerName} | ${ownerPhone}

    Nearby Locations

    Schools: ${schools}
    Shopping Centers: ${shoppingCenters}
    Airport: ${airportDistance}
    Landmarks: ${nearbyLandmarks}
    Transportation: ${transportation}
    Hospitals: ${hospitals}
    Beach: ${beachDistance}
    Metro: ${metroDistance}

    Financial Information

    Tax Year: ${taxYear}
    Tax Amount: ${taxAmount}
    Last Sold: ${lastSold}
    Last Sold Price: ${lastSoldPrice}
    Maintenance Fee: ${maintenanceFee}
    Service Charge: ${serviceCharge}

    Additional Information

    Pet Friendly: ${petFriendly}
    Smoking Allowed: ${smokingAllowed}
    Available From: ${availableFrom}
    Minimum Contract: ${minimumContract}
    Security Deposit: ${securityDeposit}
    Utilities Included: ${utilitiesIncluded}
    Internet Included: ${internetIncluded}
    Cable Included: ${cableIncluded}
    Agent: ${agentName} | ${agentPhone} | ${agentEmail}
    Owner: ${ownerName} | ${ownerPhone}

    Image Gallery

    Agent: ${agentName} | ${agentPhone} | ${agentEmail}
    Owner: ${ownerName} | ${ownerPhone}
    `; - } + return ` + + + + + Prestige Real Estate Brochure - 4 Page - A4 Size + + + + + + + + +
    +
    +
    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.

    -
    -
    -
    - 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, ${propertyName} 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.

    -

    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.

    + +
    +
    +

    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
    • +
    -
    -
    `; - } - createSampleTemplate() { - const d = this.propertyData || {}; + +
    +
    +
    Agent: [Agent Name] | [Agent Phone] | [Agent Email]
    +
    Owner: [Owner Name] | [Owner Phone] | [Owner Email]
    +
    +
    - const hasValue = (v) => v !== undefined && v !== null && v !== "" && v !== "N/A"; - - // Basic info (Step 2 fields) - const propertyName = hasValue(d.propertyName) ? d.propertyName : ""; - const propertyType = hasValue(d.propertyType) ? d.propertyType : ""; - const status = hasValue(d.status) ? d.status : ""; - const referenceId = hasValue(d.referenceNumber) ? d.referenceNumber : ""; - const location = hasValue(d.location) ? d.location : ""; - - // Contact - const agentName = hasValue(d.contactName) ? d.contactName : ""; - const agentPhone = hasValue(d.contactPhone) ? d.contactPhone : ""; - const agentEmail = hasValue(d.contactEmail) ? d.contactEmail : ""; - const ownerName = hasValue(d.ownerName) ? d.ownerName : ""; - const ownerPhone = hasValue(d.ownerPhone) ? d.ownerPhone : ""; - const ownerEmail = hasValue(d.ownerEmail) ? d.ownerEmail : ""; - - // Specifications - const bedrooms = hasValue(d.bedrooms) ? d.bedrooms : ""; - const bathrooms = hasValue(d.bathrooms) ? d.bathrooms : ""; - const floor = hasValue(d.floor) ? d.floor : ""; - const squareFeet = hasValue(d.area) - ? d.area - : (hasValue(d.size) ? `${d.size}${d.sizeUnit ? " " + d.sizeUnit : ""}` : ""); - const yearBuilt = hasValue(d.buildYear) ? d.buildYear : (hasValue(d.yearBuilt) ? d.yearBuilt : ""); - - // Pricing - const rentMinVal = hasValue(d.rentPriceMin) ? d.rentPriceMin : ""; - const rentMaxVal = hasValue(d.rentPriceMax) ? d.rentPriceMax : ""; - const saleMinVal = hasValue(d.salePriceMin) ? d.salePriceMin : ""; - const saleMaxVal = hasValue(d.salePriceMax) ? d.salePriceMax : ""; - - // Amenities & availability - const parking = hasValue(d.parking) ? d.parking : (hasValue(d.parkingSpaces) ? d.parkingSpaces : ""); - const furnishing = hasValue(d.furnishing) ? d.furnishing : (hasValue(d.furnished) ? d.furnished : ""); - const offeringTypeVal = hasValue(d.offeringType) ? d.offeringType : ""; - const availableFrom = hasValue(d.rentAvailableFrom) ? d.rentAvailableFrom : ""; - const availableTo = hasValue(d.rentAvailableTo) ? d.rentAvailableTo : ""; - - // Location details - const cityVal = hasValue(d.city) ? d.city : ""; - const communityVal = hasValue(d.community) ? d.community : ""; - const subCommunityVal = hasValue(d.subCommunity) ? d.subCommunity : ""; - const localityVal = hasValue(d.locality) ? d.locality : ""; - const towerVal = hasValue(d.tower) ? d.tower : ""; - const unitNumberVal = hasValue(d.unitNumber) ? d.unitNumber : ""; - - // Descriptions - const description1 = hasValue(d.description) ? this.formatDescriptionForPDF(d.description) : ""; - const descriptionEng = hasValue(d.descriptionEnglish) ? this.formatDescriptionForPDF(d.descriptionEnglish) : ""; - - // Images - const exteriorImage = this.getExteriorImageUrl(); - const interiorImage1 = this.getSmartImageForSection("interior", exteriorImage); - const interiorImage2 = this.getSmartImageForSection("interior", interiorImage1); - const kitchenImage = this.getSmartImageForSection("kitchen", exteriorImage); - const bedroomImage = this.getSmartImageForSection("bedroom", exteriorImage); - const mapsImage = this.getMapsImageUrl(); - - // Helpers - const row = (label, value) => (hasValue(value) ? `
    ${label}: ${value}
    ` : ""); - const feature = (label, value) => (hasValue(value) ? `
    ${value}
    ${label}
    ` : ""); - - const coverFeaturesHTML = [ - feature("Bedrooms", bedrooms), - feature("Bathrooms", bathrooms), - feature("Sq. Ft.", squareFeet), - feature("Price", saleMinVal || rentMinVal) - ].join(""); - - const specsHTML = [ - row("Reference ID", referenceId), - row("Status", status), - row("Type", propertyType), - row("Year Built", yearBuilt), - row("Floor", floor), - row("Parking", parking), - row("Furnishing", furnishing) - ].join(""); - - const amenitiesFeaturesHTML = [ - row("Parking Spaces", parking), - row("Furnished", furnishing), - row("Offering Type", offeringTypeVal) - ].join(""); - - const pricingHTML = [ - row("Rent Price (Min)", rentMinVal), - row("Rent Price (Max)", rentMaxVal), - row("Sale Price (Min)", saleMinVal), - row("Sale Price (Max)", saleMaxVal) - ].join(""); - - const availabilityHTML = [ - row("Available From", availableFrom), - row("Available To", availableTo) - ].join(""); - - const locationHTML = [ - row("City", cityVal), - row("Community", communityVal), - row("Sub Community", subCommunityVal), - row("Locality", localityVal), - row("Tower", towerVal), - row("Unit Number", unitNumberVal) - ].join(""); - - const agentLine = (hasValue(agentName) || hasValue(agentPhone) || hasValue(agentEmail)) - ? `
    Agent: ${[agentName, agentPhone, agentEmail].filter(hasValue).join(" | ")}
    ` - : ""; - const ownerLine = (hasValue(ownerName) || hasValue(ownerPhone) || hasValue(ownerEmail)) - ? `
    Owner: ${[ownerName, ownerPhone, ownerEmail].filter(hasValue).join(" | ")}
    ` - : ""; - - // Return the complete dynamic template using only populated values - return `
    - - - -
    -
    -
    - - ${hasValue(status) ? `
    ${status}
    ` : ""} +
    +
    +
    +
    + -
    -

    ${propertyName}

    - ${hasValue(location) ? `

    ${location}

    ` : ""} -
    -
    ${coverFeaturesHTML}
    -
    - -
    -
    - -
    -
    -

    Description

    -
    - ${hasValue(description1) ? `

    ${description1}

    ` : ""} - ${hasValue(descriptionEng) ? `

    ${descriptionEng}

    ` : ""} -
    -
    - -
    -
    -

    Specifications

    -
    ${specsHTML}
    -
    -
    -

    Amenities & Features

    -
    ${amenitiesFeaturesHTML}
    -
    -
    -
    -
    -
    - ${agentLine}${ownerLine} -
    -
    - -
    -
    - -
    -
    -
    -

    Basic Information

    -
    ${[row("Property Name", propertyName), row("Property Type", propertyType), row("Status", status)].join("")}
    -

    Contact Details

    -
    ${[row("Name", agentName), row("Email", agentEmail), row("Phone", agentPhone)].join("")}
    -

    Location Details

    -
    ${locationHTML}
    -
    -
    -

    Specifications

    -
    ${[row("Bedrooms", bedrooms), row("Bathrooms", bathrooms), row("Floor", floor), row("Size", squareFeet), row("Build Year", yearBuilt)].join("")}
    -

    Pricing Information

    -
    ${pricingHTML}
    -

    Amenities & Features

    -
    ${amenitiesFeaturesHTML}
    -

    Availability

    -
    ${availabilityHTML}
    -
    +
    +
    +
    +
    Schools
    +
    [Schools]
    +
    +
    +
    +
    Shopping
    +
    [Shopping Centers]
    +
    +
    +
    +
    Airport
    +
    [Airport Distance]
    +
    +
    +
    +
    Landmarks
    +
    [Nearby Landmarks]
    +
    +
    +
    +
    Transportation
    +
    [Transportation]
    +
    +
    +
    +
    Hospitals
    +
    [Hospitals]
    +
    +
    +
    +
    Beach
    +
    [Beach Distance]
    +
    +
    +
    +
    Metro
    +
    [Metro Distance]
    -
    -
    - ${agentLine}${ownerLine} -
    - - -
    -
    - -
    - -
    -

    Location Details

    -
    ${locationHTML}
    -
    -
    -
    -
    - ${agentLine}${ownerLine} -
    -
    -
    `; +
    +
    +
    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]
    +
    +
    + + + + +` } - createGrandOakVillaTemplate() { - const d = this.propertyData || {}; - const has = (v) => v !== undefined && v !== null && v !== "" && v !== "N/A"; - // Basic - const propertyName = d.propertyName || d.Name || ""; - const location = d.location || d.Address__c || ""; - const status = (d.status || d.Status__c || "").toString(); - const propertyType = d.propertyType || d.Property_Type__c || ""; - - // Contact - const agentName = d.contactName || d.Agent_Name__c || ""; - const agentPhone = d.contactPhone || d.Agent_Phone__c || ""; - const agentEmail = d.contactEmail || d.Agent_Email__c || ""; - const ownerName = d.ownerName || d.Owner_Name__c || ""; - const ownerPhone = d.ownerPhone || d.Owner_Phone__c || ""; - const ownerEmail = d.ownerEmail || d.Owner_Email__c || ""; - - // Specs - const bedrooms = d.bedrooms || d.Bedrooms__c || ""; - const bathrooms = d.bathrooms || d.Bathrooms__c || ""; - const floor = d.floor || d.Floor__c || ""; - const yearBuilt = d.buildYear || d.yearBuilt || d.Build_Year__c || ""; - const sizeText = (d.area && d.area) || (d.size ? `${d.size}${d.sizeUnit ? ` ${d.sizeUnit}` : ""}` : (d.Square_Feet__c ? `${d.Square_Feet__c} sq ft` : "")); - - // Pricing - const rentMinVal = d.rentPriceMin || d.Rent_Price_Min__c || ""; - const rentMaxVal = d.rentPriceMax || d.Rent_Price_Max__c || ""; - const saleMinVal = d.salePriceMin || d.Sale_Price_Min__c || ""; - const saleMaxVal = d.salePriceMax || d.Sale_Price_Max__c || ""; - - // Amenities & availability - const parking = d.parking || d.Parking_Spaces__c || d.parkingSpaces || ""; - const furnishing = d.furnishing || d.Furnished__c || d.furnished || ""; - const offeringTypeVal = d.offeringType || d.Offering_Type__c || ""; - const availableFrom = d.rentAvailableFrom || d.Available_From__c || ""; - const availableTo = d.rentAvailableTo || d.Available_To__c || ""; - - // Location details - const cityVal = d.city || d.City__c || d.cityPropertyfinder || d.cityBayut || ""; - const communityVal = d.community || d.Community__c || d.communityBayut || ""; - const subCommunityVal = d.subCommunity || d.Sub_Community__c || d.subCommunityBayut || ""; - const localityVal = d.locality || d.Locality__c || d.localityBayut || ""; - const towerVal = d.tower || d.Tower__c || d.towerBayut || ""; - const unitNumberVal = d.unitNumber || d.Unit_Number__c || ""; - - // Description - const description = this.formatDescriptionForPDF(d.descriptionEnglish || d.description || ""); - - // Images and gallery - const exteriorImage = this.getExteriorImageUrl(); - const propertyGalleryHTML = this.generatePropertyGalleryHTML(d); - - // Helpers - const row = (label, value) => (has(value) ? `
    ${label}: ${value}
    ` : ""); - const feature = (label, value) => (has(value) ? `
    ${value}
    ${label}
    ` : ""); - - const coverFeaturesHTML = [ - feature("Bedrooms", bedrooms), - feature("Bathrooms", bathrooms), - feature("Sq. Ft.", sizeText), - feature("Price", saleMinVal || rentMinVal) - ].join(""); - - const locationHTML = [ - row("City", cityVal), - row("Community", communityVal), - row("Sub Community", subCommunityVal), - row("Locality", localityVal), - row("Tower", towerVal), - row("Unit Number", unitNumberVal) - ].join(""); - - const specsRightHTML = [ - row("Bedrooms", bedrooms), - row("Bathrooms", bathrooms), - row("Floor", floor), - row("Size", sizeText), - row("Build Year", yearBuilt) - ].join(""); - - const pricingHTML = [ - row("Rent Price (Min)", rentMinVal), - row("Rent Price (Max)", rentMaxVal), - row("Sale Price (Min)", saleMinVal), - row("Sale Price (Max)", saleMaxVal) - ].join(""); - - const amenitiesFeaturesHTML = [ - row("Parking Spaces", parking), - row("Furnished", furnishing), - row("Offering Type", offeringTypeVal) - ].join(""); - - const availabilityHTML = [ - row("Available From", availableFrom), - row("Available To", availableTo) - ].join(""); - - const agentLine = (has(agentName) || has(agentPhone) || has(agentEmail)) - ? `
    Agent: ${[agentName, agentPhone, agentEmail].filter(has).join(" | ")}
    ` - : ""; - const ownerLine = (has(ownerName) || has(ownerPhone) || has(ownerEmail)) - ? `
    Owner: ${[ownerName, ownerPhone, ownerEmail].filter(has).join(" | ")}
    ` - : ""; - - return `
    - -
    ${has(status) ? `
    ${status}
    ` : ""}

    ${propertyName}

    ${has(location) ? `
    ${location}
    ` : ""}
    ${coverFeaturesHTML}
    -

    Description

    ${has(description) ? `

    ${description}

    ` : ""}
    ${agentLine}${ownerLine}
    -

    Basic Information

    ${[row("Property Name", propertyName), row("Property Type", propertyType), row("Status", status)].join("")}

    Contact Details

    ${[row("Name", agentName), row("Email", agentEmail), row("Phone", agentPhone)].join("")}

    Location Details

    ${locationHTML}

    Specifications

    ${specsRightHTML}

    Pricing Information

    ${pricingHTML}

    Amenities & Features

    ${amenitiesFeaturesHTML}

    Availability

    ${availabilityHTML}
    ${agentLine}${ownerLine}
    -
    ${propertyGalleryHTML}
    ${agentLine}${ownerLine}
    -
    `; - } createLuxuryMansionTemplate() { - // Keep for backward-compatibility; delegate to Vertice generator - return this.createVerticeTemplate(); - } - - // Vertice-style dynamic brochure (cover, description, amenities/specs, highlights, gallery) - createVerticeTemplate() { const data = this.propertyData || {}; - // Use same safe fallback style as templates 1/2 + const propertyName = data.Name || data.propertyName || "Property Name"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const location = data.Address__c || data.location || "Location"; @@ -3802,105 +3752,646 @@ return result; data.price || "Price on Request"; const referenceId = - data.Reference_Number__c || data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A"; + data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A"; const description = this.formatDescriptionForPDF( - data.Description_English__c || data.descriptionEnglish || data.description || "Property description not available." + data.Description_English__c || + data.descriptionEnglish || + data.description || + "Property description not available." ); - const status = data.Status__c || data.status || "N/A"; - const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; - const technology = data.Technology__c || data.technology || "N/A"; - const design = data.Design__c || data.design || "N/A"; - const pets = 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 parking = data.Parking_Spaces__c || data.parking || "N/A"; - const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; - const utilities = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; - const landmarks = data.Landmarks__c || data.nearbyLandmarks || data.Nearby_Landmarks__c || "N/A"; - const transportation = data.Transportation__c || data.transportation || "N/A"; - const schools = data.Schools__c || data.schools || "N/A"; - const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; - const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; - - const amenitiesHTML = this.buildAmenitiesListForVertice(data); const propertyGallery = this.generatePropertyGalleryHTML(); - const coverImg = this.getExteriorImageUrl(); - const featureImg = this.getSmartImageForSection("interior", coverImg); - const visionHeading = data.Title_English__c || data.titleEnglish || ""; - const pageSubtitle = propertyType && propertyType !== "N/A" ? propertyType : ""; - return `${propertyName} - Urban Brochure -
    - Cover -
    -
    -
    ${propertyType}
    -
    ${propertyName}
    -
    ${location}
    -
    - -
    + return ` + + + + + Modern Urban Residences Brochure - Updated - A4 Size + + + + + + + + +
    +
    +
    +
    An Urban Oasis
    +

    THE VERTICE

    +
    18 Skyline Avenue, Metropolis Centre, MC 90210
    -
    -
    -
    ${propertyName}Page 02 / 04
    -
    -
    - -
    -
    - -
    -

    Lifestyle Amenities

      ${amenitiesHTML}
    -

    Key Specifications

    -
    Status${status}
    -
    Type${propertyType}
    -
    Year Built${yearBuilt}
    -
    Technology${technology}
    -
    Design${design}
    -
    Pets${pets}
    -
    Smoking${smoking}
    -
    Availability${availability}
    -
    Parking${parking}
    -
    Security Deposit${securityDeposit}
    -
    Utilities${utilities}
    + -
    -
    ${propertyName}Page 03 / 04
    -
    -
    -
    - -
    -
      -
    • Landmarks: ${landmarks}
    • -
    • Transportation: ${transportation}
    • -
    • Schools: ${schools}
    • -
    • Shopping: ${shopping}
    • -
    • Airport: ${airport}
    • -
    -
    - - -
    ${propertyName}Page 04 / 04
    +
    +
    + +
    +
    +

    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
    • +
    +
    +
    +
    + THE VERTICE + Page 06 / 06 +
    +
    +
    + + + +`; } -/* + /* // Dynamic additional info const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; @@ -3923,11 +4414,11 @@ return result; createSerenityHouseTemplate() { const data = this.propertyData || {}; - // Extract all available property data with safe fallbacks (match templates 1/2) + // Extract all available property data with fallbacks const propertyName = data.Name || data.propertyName || "Property Name"; const location = data.Address__c || data.location || "Location"; const referenceId = - data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A"; + data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const agentName = data.contactName || data.Agent_Name__c || data.agentName || "N/A"; const agentPhone = @@ -3972,8 +4463,14 @@ return result; // Get smart images const exteriorImage = this.getExteriorImageUrl(); - const interiorImage = this.getSmartImageForSection("interior", exteriorImage); - const bedroomImage = this.getSmartImageForSection("bedroom", exteriorImage); + 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(); @@ -4000,23 +4497,50 @@ return result; - Editorial Real Estate Brochure - ${propertyName} + Editorial Real Estate Brochure - Updated - A4 Size - +
    -
    ${ - data.collection || "Elysian Estates Collection" - }
    -

    ${propertyName}

    -

    ${location}

    -

    Property: ${propertyName}

    +
    Elysian Estates Collection
    +

    The Serenity House

    +

    123 Luxury Lane, Prestige City, PC 45678

    +

    Reference ID: ES-8821

    -
    ${squareFeet} Sq. Ft. • ${bedrooms} Bedrooms • ${bathrooms} Bathrooms
    -

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

    ${propertyName}

    -

    A Sanctuary of Modern Design

    -
    - ${description} +

    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.

    +
    +
    -
    - 04 + 03

    Property Specifications

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

    -
    -
    ${bedrooms}
    -
    Bedrooms
    -
    -
    -
    ${bathrooms}
    -
    Bathrooms
    -
    -
    -
    ${squareFeet}
    -
    Square Feet
    -
    -
    -
    ${data.acres || "0.75"}
    -
    Acres
    -
    +
    5
    Bedrooms
    +
    6
    Bathrooms
    +
    6,200
    Square Feet
    +
    0.75
    Acres
    +

    Property Details

    -
    - Status - ${status} -
    -
    - Year Built - ${yearBuilt} -
    -
    - Type - ${propertyType} -
    -
    - Furnishing - ${furnishing} -
    -
    - Floor - ${ - data.Floor__c || data.floor || "N/A" - } -
    -
    - Maintenance Fee - ${ - data.Maintenance_Fee__c || - data.maintenanceFee || - "N/A" - } -
    -
    - Parking - ${parking} -
    -
    - Service Charge - ${ - data.Service_Charge__c || data.serviceCharge || "N/A" - } -
    +
    StatusFor Sale
    +
    Year Built2023
    +
    TypeSingle-Family Home
    +
    FurnishingPartially Furnished
    +
    Floor2 Levels
    +
    Maintenance Fee$1,200 / month
    +
    Parking3-Car Garage
    +
    Service ChargeIncluded
    +

    Amenities & Features

    -
    - ${amenitiesHTML} -
    +
      +
    • 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
    • +
    -
    - 05 -

    ${propertyName} - Floor Plan & Details

    + 04 +

    Floor Plan & Details

    Location & Nearby

    -
    - Schools ${schools} -
    -
    - Shopping ${shopping} -
    -
    - Hospitals ${hospitals} -
    -
    - Country Club ${countryClub} -
    -
    - Airport ${airport} -
    +
    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 ${petFriendly} -
    -
    - Smoking ${smoking} -
    -
    - Availability ${availability} -
    -
    - Utilities ${utilities} -
    +
    Pet-Friendly By Approval
    +
    Smoking Not Permitted
    +
    Availability Immediate
    +
    Utilities Not Included
    @@ -4549,33 +4924,23 @@ return result;
    Owner Information
    -
    ${ownerName}
    -

    ${ownerPhone}

    - +
    John & Jane Doe
    +

    (555) 111-2222

    +
    -
    Agent Information
    -
    ${agentName}
    -

    ${agentPhone}

    - +
    Agent Information
    +
    Olivia Sterling
    +

    (555) 987-6543

    +
    - - -
    -
    - 06 -

    ${propertyName} - Property Gallery

    - - -
    -
    + -`; + +`; } // Error handling methods @@ -4770,197 +5135,38 @@ return result; return true; } - // Modern implementation without deprecated execCommand + // ENHANCED BULLET FUNCTION handleBulletList() { - const editorContent = this.template.querySelector(".enhanced-editor-content"); + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); if (!editorContent) { this.showError("Editor not found"); return; } - this.insertList('ul'); - } - handleNumberedList() { - const editorContent = this.template.querySelector(".enhanced-editor-content"); - if (!editorContent) { - this.showError("Editor not found"); - return; - } - this.insertList('ol'); - } - - // New unified list insertion method - insertList(listType) { - const editorContent = this.template.querySelector(".enhanced-editor-content"); - const selection = window.getSelection(); editorContent.focus(); - - let range; - let selectedText = ''; - if (selection.rangeCount > 0) { - range = selection.getRangeAt(0); - selectedText = range.toString().trim(); - } else { - range = document.createRange(); - range.selectNodeContents(editorContent); - range.collapse(false); - selection.removeAllRanges(); - selection.addRange(range); - } - - const currentList = this.findParentList(range.commonAncestorContainer); - if (currentList && currentList.tagName.toLowerCase() === listType.toLowerCase()) { - this.convertListToParagraph(currentList); + // Use native command so selection toggles bullet list and supports nesting + document.execCommand("insertUnorderedList", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + // ENHANCED NUMBERED LIST FUNCTION + handleNumberedList() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); return; } - if (currentList && currentList.tagName.toLowerCase() !== listType.toLowerCase()) { - this.convertListType(currentList, listType); - return; - } - this.createNewList(listType, selectedText, range); + + editorContent.focus(); + // Use native command so selection toggles ordered list and supports nesting + document.execCommand("insertOrderedList", false, null); + editorContent.dispatchEvent(new Event("input", { bubbles: true })); } - // Helper method to find parent list element - findParentList(node) { - let current = node; - while (current && current !== document.body) { - if (current.nodeType === Node.ELEMENT_NODE) { - if (current.tagName === 'UL' || current.tagName === 'OL') { - return current; - } - if (current.tagName === 'LI') { - return current.parentElement; - } - } - current = current.parentElement; - } - return null; - } - - // Convert list back to paragraphs - convertListToParagraph(list) { - const listItems = Array.from(list.querySelectorAll('li')); - const fragment = document.createDocumentFragment(); - listItems.forEach(li => { - const p = document.createElement('p'); - p.innerHTML = li.innerHTML || 'List item'; - p.style.margin = '8px 0'; - fragment.appendChild(p); - }); - list.parentNode.replaceChild(fragment, list); - this.showSuccess("List converted to paragraphs"); - } - - // Convert one list type to another - convertListType(currentList, newListType) { - const newList = document.createElement(newListType); - const listItems = Array.from(currentList.querySelectorAll('li')); - listItems.forEach(li => { - const newLi = li.cloneNode(true); - newList.appendChild(newLi); - }); - this.styleList(newList); - currentList.parentNode.replaceChild(newList, currentList); - const listTypeName = newListType === 'ul' ? 'bullet' : 'numbered'; - this.showSuccess(`Converted to ${listTypeName} list`); - } - - // Create a new list - createNewList(listType, selectedText, range) { - const list = document.createElement(listType); - if (selectedText) { - const lines = selectedText.split('\n').filter(line => line.trim()); - if (lines.length > 1) { - lines.forEach(line => { - const li = document.createElement('li'); - li.textContent = line.trim(); - li.contentEditable = true; - list.appendChild(li); - }); - } else { - const li = document.createElement('li'); - li.textContent = selectedText || 'List item'; - li.contentEditable = true; - list.appendChild(li); - } - } else { - const li = document.createElement('li'); - li.textContent = 'List item'; - li.contentEditable = true; - list.appendChild(li); - } - this.styleList(list); - try { - range.deleteContents(); - range.insertNode(list); - const firstLi = list.querySelector('li'); - if (firstLi) { - const newRange = document.createRange(); - newRange.selectNodeContents(firstLi); - newRange.collapse(false); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(newRange); - firstLi.focus(); - } - const listTypeName = listType === 'ul' ? 'bullet' : 'numbered'; - this.showSuccess(`${listTypeName} list created`); - } catch (error) { - console.error('Error inserting list:', error); - this.showError('Failed to create list'); - } - } - - // Apply consistent styling to lists - styleList(list) { - if (list.tagName === 'UL') { - list.style.listStyleType = 'disc'; - } else if (list.tagName === 'OL') { - list.style.listStyleType = 'decimal'; - } - list.style.paddingLeft = '22px'; - list.style.margin = '0 0 8px 0'; - list.style.lineHeight = '1.6'; - const listItems = list.querySelectorAll('li'); - listItems.forEach(li => { - li.style.margin = '4px 0'; - li.style.paddingLeft = '4px'; - if (!li.hasAttribute('contenteditable')) { - li.contentEditable = true; - } - li.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.handleListItemEnter(e.target, list); - } - }); - }); - } - - // Handle Enter key in list items to create new items - handleListItemEnter(currentLi, list) { - const newLi = document.createElement('li'); - newLi.textContent = ''; - newLi.contentEditable = true; - newLi.style.margin = '4px 0'; - newLi.style.paddingLeft = '4px'; - newLi.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.handleListItemEnter(e.target, list); - } - }); - currentLi.parentNode.insertBefore(newLi, currentLi.nextSibling); - newLi.focus(); - const range = document.createRange(); - range.setStart(newLi, 0); - range.collapse(true); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - } - - // Keep the alias for backward compatibility + // Alias for numbered list handleNumberList() { this.handleNumberedList(); } @@ -6737,9 +6943,9 @@ return result; startTop = parseInt(element.style.top) || 0; if (editor) { -editor.initialScrollLeft = editor.scrollLeft; -editor.initialScrollTop = editor.scrollTop; -} + editor.initialScrollLeft = editor.scrollLeft; + editor.initialScrollTop = editor.scrollTop; + } e.preventDefault(); e.stopPropagation(); @@ -6779,20 +6985,20 @@ editor.initialScrollTop = editor.scrollTop; let newLeft = startLeft + deltaX; let newTop = startTop + deltaY; -// Account for editor scroll position for smooth dragging -if (editor) { -const currentScrollLeft = editor.scrollLeft; -const currentScrollTop = editor.scrollTop; - -// Adjust position based on scroll changes since drag started -const scrollDeltaX = currentScrollLeft - (editor.initialScrollLeft || 0); -const scrollDeltaY = currentScrollTop - (editor.initialScrollTop || 0); - -newLeft -= scrollDeltaX; -newTop -= scrollDeltaY; -} + // Account for editor scroll position for smooth dragging + if (editor) { + const currentScrollLeft = editor.scrollLeft; + const currentScrollTop = editor.scrollTop; + // Adjust position based on scroll changes since drag started + const scrollDeltaX = + currentScrollLeft - (editor.initialScrollLeft || 0); + const scrollDeltaY = + currentScrollTop - (editor.initialScrollTop || 0); + newLeft -= scrollDeltaX; + newTop -= scrollDeltaY; + } // Keep element within editor bounds - use scrollHeight for full template height const maxWidth = editor ? editor.clientWidth : editorRect.width; @@ -6824,11 +7030,6 @@ newTop -= scrollDeltaY; // Restore scrolling document.body.style.overflow = ""; - - // Ensure image containers return to in-flow after drag - if (element.classList && element.classList.contains("draggable-image-container")) { - element.style.position = "relative"; - } } }; @@ -8880,7 +9081,9 @@ newTop -= scrollDeltaY; // Replace all background-image URLs with the property's exterior image updatedHTML = updatedHTML.replace(backgroundImagePattern, (match) => { - return exteriorImageUrl ? `background-image: url('${exteriorImageUrl}')` : "background-image: none"; + return exteriorImageUrl + ? `background-image: url('${exteriorImageUrl}')` + : "background-image: none"; }); return updatedHTML; @@ -8913,11 +9116,14 @@ newTop -= scrollDeltaY; const currentStyle = element.getAttribute("style") || ""; const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; - const updatedStyle = currentStyle.replace(backgroundImagePattern, (match) => { - return exteriorImageUrl - ? `background-image: url('${exteriorImageUrl}')` - : "background-image: none"; - }); + const updatedStyle = currentStyle.replace( + backgroundImagePattern, + (match) => { + return exteriorImageUrl + ? `background-image: url('${exteriorImageUrl}')` + : "background-image: none"; + } + ); if (updatedStyle !== currentStyle) element.setAttribute("style", updatedStyle); }); @@ -10010,14 +10216,28 @@ newTop -= scrollDeltaY; this.highlightSelectedElement(container); return; } - // Wrap plain image and add handles; keep image position intact (no jump) + // Wrap plain image and add handles; preserve on-screen size and position + const editor = this.template.querySelector(".enhanced-editor-content"); + const rect = element.getBoundingClientRect(); + const editorRect = editor + ? editor.getBoundingClientRect() + : { left: 0, top: 0 }; const currentWidth = element.offsetWidth; const currentHeight = element.offsetHeight; const container = document.createElement("div"); container.className = "draggable-image-container"; - container.style.position = "relative"; // in-flow until drag begins + container.style.position = "absolute"; + container.style.left = + rect.left - editorRect.left + (editor ? editor.scrollLeft : 0) + "px"; + container.style.top = + rect.top - editorRect.top + (editor ? editor.scrollTop : 0) + "px"; + container.style.zIndex = + window.getComputedStyle(element).zIndex || "auto"; container.style.display = "inline-block"; + + // Move the image into container and preserve size + // Set container and image sizing container.style.width = currentWidth + "px"; container.style.height = currentHeight + "px"; element.style.width = "100%"; @@ -10025,8 +10245,11 @@ newTop -= scrollDeltaY; element.style.display = "block"; element.style.objectFit = "cover"; - // Insert container right before the image so layout doesn't shift + if (editor) { + editor.appendChild(container); + } else { element.parentNode.insertBefore(container, element); + } container.appendChild(element); container.classList.add("no-frame"); this.addResizeHandles(container); @@ -10152,4 +10375,4 @@ newTop -= scrollDeltaY; connectedCallback() { this.loadSavedTemplates(); } -} \ No newline at end of file +} diff --git a/template samples/real estate modern home-A3.html b/template samples/real estate modern home-A3.html new file mode 100644 index 0000000..d1cfe6f --- /dev/null +++ b/template samples/real estate modern home-A3.html @@ -0,0 +1,499 @@ + + + + + + Property Brochure - A3 Size + + + + + +
    +
    +
    +

    [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] +
    +
    +
    + + + diff --git a/template samples/real estate modern home-A4.html b/template samples/real estate modern home-A4.html new file mode 100644 index 0000000..bc09674 --- /dev/null +++ b/template samples/real estate modern home-A4.html @@ -0,0 +1,499 @@ + + + + + + Property Brochure - A4 Size + + + + + +
    +
    +
    +

    [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] +
    +
    +
    + + + diff --git a/template samples/the grand oak villa-A3.html b/template samples/the grand oak villa-A3.html new file mode 100644 index 0000000..5f2eaf5 --- /dev/null +++ b/template samples/the grand oak villa-A3.html @@ -0,0 +1,423 @@ + + + + + + Prestige Real Estate Brochure - 4 Page - A3 Size + + + + + + + + +
    +
    +
    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]
    +
    +
    + + + + diff --git a/template samples/the grand oak villa.html b/template samples/the grand oak villa-A4.html similarity index 96% rename from template samples/the grand oak villa.html rename to template samples/the grand oak villa-A4.html index be5a620..8dd13ee 100644 --- a/template samples/the grand oak villa.html +++ b/template samples/the grand oak villa-A4.html @@ -3,7 +3,7 @@ - Prestige Real Estate Brochure - 4 Page + Prestige Real Estate Brochure - 4 Page - A4 Size @@ -28,6 +28,28 @@ --padding-page: 60px; } + /* --- Print Media Queries for A4 --- */ + @media print { + @page { + size: A4; + margin: 0; + } + body { + margin: 0; + padding: 0; + background: white; + } + .brochure-page { + width: 210mm !important; + height: 297mm !important; + box-shadow: none !important; + page-break-after: always; + } + .brochure-page:last-child { + page-break-after: avoid; + } + } + /* --- GLOBAL & BODY STYLES --- */ body { font-family: var(--font-primary); @@ -398,4 +420,4 @@ - \ No newline at end of file + diff --git a/template samples/the serenity house-A3.html b/template samples/the serenity house-A3.html new file mode 100644 index 0000000..2bc43c6 --- /dev/null +++ b/template samples/the serenity house-A3.html @@ -0,0 +1,448 @@ + + + + + + Editorial Real Estate Brochure - Updated - A3 Size + + + + + + + + +
    +
    +
    +
    +
    +
    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

    + +
    +
    +
    +
    + + + diff --git a/template samples/the serenity.html b/template samples/the serenity house-A4.html similarity index 91% rename from template samples/the serenity.html rename to template samples/the serenity house-A4.html index 44492e4..a8ee29c 100644 --- a/template samples/the serenity.html +++ b/template samples/the serenity house-A4.html @@ -3,7 +3,7 @@ - Editorial Real Estate Brochure - Updated + Editorial Real Estate Brochure - Updated - A4 Size @@ -24,6 +24,28 @@ --font-sans: 'Lato', sans-serif; } + /* --- Print Media Queries for A4 --- */ + @media print { + @page { + size: A4; + margin: 0; + } + body { + margin: 0; + padding: 0; + background: white; + } + .brochure-page { + width: 210mm !important; + height: 297mm !important; + box-shadow: none !important; + page-break-after: always; + } + .brochure-page:last-child { + page-break-after: avoid; + } + } + /* --- GLOBAL & BODY STYLES --- */ body { font-family: var(--font-sans); @@ -321,7 +343,9 @@

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

    - [Description HTML] +

    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.

    @@ -332,7 +356,7 @@
    03

    Property Specifications

    -

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

    +

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

    @@ -421,4 +445,4 @@
    - \ No newline at end of file + diff --git a/template samples/the vertice-A3.html b/template samples/the vertice-A3.html new file mode 100644 index 0000000..42c3eec --- /dev/null +++ b/template samples/the vertice-A3.html @@ -0,0 +1,626 @@ + + + + + + Modern Urban Residences Brochure - Updated - A3 Size + + + + + + + + +
    +
    +
    +
    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
    • +
    +
    +
    +
    + THE VERTICE + Page 06 / 06 +
    +
    +
    + + + diff --git a/template samples/the_vertice.html b/template samples/the vertice-A4.html similarity index 80% rename from template samples/the_vertice.html rename to template samples/the vertice-A4.html index aacf05b..8d21337 100644 --- a/template samples/the_vertice.html +++ b/template samples/the vertice-A4.html @@ -3,7 +3,7 @@ - Modern Urban Residences Brochure - Updated + Modern Urban Residences Brochure - Updated - A4 Size @@ -25,6 +25,28 @@ --font-main: 'Inter', sans-serif; } + /* --- Print Media Queries for A4 --- */ + @media print { + @page { + size: A4; + margin: 0; + } + body { + margin: 0; + padding: 0; + background: white; + } + .brochure-page { + width: 210mm !important; + height: 297mm !important; + box-shadow: none !important; + page-break-after: always; + } + .brochure-page:last-child { + page-break-after: avoid; + } + } + /* --- GLOBAL & BODY STYLES --- */ body { font-family: var(--font-main); @@ -396,31 +418,32 @@
    -
    [Cover Subtitle]
    -

    [Property Name]

    -
    [Address]
    +
    An Urban Oasis
    +

    THE VERTICE

    +
    18 Skyline Avenue, Metropolis Centre, MC 90210
    -

    [Vision Heading]

    -
    [Vision HTML]
    +

    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.

    - [Property Name] + THE VERTICE Page 02 / 06
    @@ -433,14 +456,14 @@ A Canvas for Your Life
    - - - - - + + + + +
    - [Property Name] + THE VERTICE Page 03 / 06
    @@ -459,28 +482,28 @@

    Lifestyle Amenities

      -
    • [Amenity 1]
    • -
    • [Amenity 2]
    • -
    • [Amenity 3]
    • -
    • [Amenity 4]
    • -
    • [Amenity 5]
    • -
    • [Amenity 6]
    • -
    • [Amenity 7]
    • -
    • [Amenity 8]
    • +
    • Rooftop Infinity Pool
    • +
    • Fitness Center
    • +
    • Residents' Sky Lounge
    • +
    • Private Cinema Room
    • +
    • Wellness Spa & Sauna
    • +
    • Business Center
    • +
    • 24/7 Concierge
    • +
    • Secure Parking

    Key Specifications

    -
    Status [Status]
    -
    Property Type [Property Type]
    -
    Year Built [Year Built]
    -
    Technology [Technology]
    -
    Design [Design]
    +
    Status New Development
    +
    Property Type Condominium
    +
    Year Built 2025
    +
    Technology Integrated Smart Home
    +
    Design Sustainable & Eco-Friendly
    - [Property Name] + THE VERTICE Page 04 / 06
    @@ -496,67 +519,67 @@
    -

    [Floorplan 1 Title]

    +

    Two-Bedroom Residence

    -
    [Floorplan 1 Area]
    +
    1,450
    SQ. FT.
    -
    [Floorplan 1 Bedrooms]
    +
    2
    BEDROOMS
    -
    [Floorplan 1 Bathrooms]
    +
    2
    BATHROOMS
    -
    [Floorplan 1 Balcony]
    +
    1
    BALCONY
    -
    [Floorplan 1 Description HTML]
    +

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

    -

    [Floorplan 2 Title]

    +

    Three-Bedroom Penthouse

    -
    [Floorplan 2 Area]
    +
    3,200
    SQ. FT.
    -
    [Floorplan 2 Bedrooms]
    +
    3
    BEDROOMS
    -
    [Floorplan 2 Bathrooms]
    +
    3.5
    BATHROOMS
    -
    [Floorplan 2 Terrace]
    +
    1
    TERRACE
    -
    [Floorplan 2 Description HTML]
    +

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

    Additional Information

    -
    Pets
    [Pets]
    -
    Smoking
    [Smoking]
    -
    Availability
    [Availability]
    -
    Parking
    [Parking]
    -
    Security Deposit
    [Security Deposit]
    -
    Utilities
    [Utilities]
    +
    Pets
    Allowed (w/ restrictions)
    +
    Smoking
    In designated areas
    +
    Availability
    Q4 2025
    +
    Parking
    2 Spaces per Unit
    +
    Security Deposit
    2 Months
    +
    Utilities
    Sub-metered
    - [Property Name] + THE VERTICE Page 05 / 06
    @@ -571,33 +594,33 @@

    Schedule a Private Viewing

    -

    [Viewing CTA Text]

    +

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

    -
    [Contact Name]
    -
    [Contact Title]
    +
    Alexander Valentine
    +
    Sales Executive, Elysian Properties
    - [Contact Phone]
    - [Contact Email] + (555) 123-9876
    + alex.v@elysian.com

    Neighborhood Highlights

      -
    • Landmarks: [Landmarks]
    • -
    • Transportation: [Transportation]
    • -
    • Schools: [Schools]
    • -
    • Shopping: [Shopping]
    • -
    • Airport: [Airport]
    • +
    • 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 +