Property Listing Sheet Generator
Enter the details for the property below. This information will be used to generate the listing sheet preview.
Review the generated listing sheet below. Go back to the 'Property Details' tab to make changes.
Enter property details on the first tab to generate a preview.
Please fill in required fields (Address, Price, Beds, Baths) on the first tab to generate a preview.
'; downloadBtn.disabled = true; return; } downloadBtn.disabled = false; // Enable download button if we have data const formattedPrice = propertyData.price ? `$${parseFloat(propertyData.price).toLocaleString('en-US')}` : 'Price not set'; const formattedSqft = propertyData.sqft ? `${parseInt(propertyData.sqft).toLocaleString('en-US')} sq ft` : ''; let featuresHTML = ''; if (propertyData.features && propertyData.features.length > 0) { featuresHTML = `Key Features:
-
${propertyData.features.map(f => `
- ${escapeHtml(f)} `).join('')}
${escapeHtml(propertyData.address)}
${formattedPrice}
${propertyData.bedrooms} Bed
${propertyData.bathrooms} Bath
${formattedSqft ? ` ${formattedSqft}` : ''}
${escapeHtml(propertyData.description || 'No description provided.')}
${featuresHTML} `; } /** Reads form inputs and updates the state object AND the preview */ window.plsgUpdatePreview = () => { // Guard clauses for essential inputs if(!addressInput || !priceInput || !bedroomsInput || !bathroomsInput || !descriptionInput || !featuresInput || !sqftInput) return; propertyData.address = addressInput.value.trim(); propertyData.price = priceInput.value ? parseFloat(priceInput.value) : null; propertyData.sqft = sqftInput.value ? parseInt(sqftInput.value) : null; propertyData.bedrooms = bedroomsInput.value ? parseFloat(bedroomsInput.value) : null; // Use parseFloat for potential .5 baths propertyData.bathrooms = bathroomsInput.value ? parseFloat(bathroomsInput.value) : null; propertyData.description = descriptionInput.value.trim(); propertyData.features = featuresInput.value.trim().split('\n').map(f => f.trim()).filter(f => f); // Split by newline, trim, remove empty // Update the preview immediately if the preview tab is active if (currentTabId === 'plsg-tab-preview') { renderPreview(); } // Enable/disable download button based on required fields even if preview not visible if (!previewArea || !downloadBtn) return; const requiredFilled = propertyData.address && propertyData.price && propertyData.bedrooms !== null && propertyData.bathrooms !== null; downloadBtn.disabled = !requiredFilled; }; /** Switches tabs */ window.plsgShowTab = (tabId, element) => { if (!container || !tabLinks) return; container.querySelectorAll('.plsg-tab-content').forEach(tab => tab.classList.remove('plsg-active')); container.querySelectorAll('.plsg-tab-link').forEach(link => link.classList.remove('plsg-active')); const tabToShow = container.querySelector('#' + tabId); if (tabToShow) tabToShow.classList.add('plsg-active'); if (element) element.classList.add('plsg-active'); currentTabId = tabId; updateNavButtons(); // Update preview when switching TO the preview tab if (tabId === 'plsg-tab-preview') { plsgUpdatePreview(); // Make sure data is read from form first renderPreview(); } }; /** Handles nav buttons */ window.plsgNavigateTabs = (isNext) => { if (!container || !tabLinks) return; const targetTabId = isNext ? 'plsg-tab-preview' : 'plsg-tab-details'; const targetTabLink = container.querySelector(`.plsg-tab-link[onclick*="'${targetTabId}'"]`); if(targetTabLink) targetTabLink.click(); }; /** Special function for the button on the details tab */ window.plsgGoToPreviewTab = () => { plsgNavigateTabs(true); // Simulate clicking the "next" button }; /** Updates nav button states */ function updateNavButtons() { if (!prevBtn || !nextBtn) return; prevBtn.disabled = currentTabId === 'plsg-tab-details'; nextBtn.disabled = currentTabId === 'plsg-tab-preview'; } /** Downloads the listing as PDF */ function downloadPDF() { if (typeof jsPDF === 'undefined') { showMessage("PDF library not loaded.", true); return; } // Re-read data just in case something changed but wasn't saved to state correctly (though oninput should cover it) plsgUpdatePreview(); // Check if required data is present if (!propertyData.address || !propertyData.price || propertyData.bedrooms === null || propertyData.bathrooms === null) { showMessage("Cannot download. Please fill in required fields (Address, Price, Beds, Baths).", true); return; } showLoader(true); showMessage(null); try { const doc = new jsPDF({ orientation: 'p', // portrait unit: 'pt', format: 'letter' // Standard letter size }); const pageMargin = 50; const pageWidth = doc.internal.pageSize.getWidth(); const contentWidth = pageWidth - (pageMargin * 2); let y = pageMargin; const lineSpacing = 1.4; // --- Header --- doc.setFontSize(20); doc.setFont(undefined, 'bold'); // FIX: Use defined JS constant instead of CSS var doc.setTextColor(PRIMARY_COLOR); const addressLines = doc.splitTextToSize(propertyData.address, contentWidth); doc.text(addressLines, pageMargin, y); y += addressLines.length * 20 * lineSpacing * 0.8; doc.setFontSize(16); doc.setFont(undefined, 'normal'); // FIX: Use defined JS constant instead of CSS var doc.setTextColor(DARK_GRAY); const formattedPrice = `$${parseFloat(propertyData.price).toLocaleString('en-US')}`; doc.text(formattedPrice, pageMargin, y); y += 16 * lineSpacing * 1.2; // --- Details Line --- doc.setFontSize(11); // FIX: Use defined JS constant instead of CSS var doc.setTextColor(TEXT_COLOR); let detailsLine = `${propertyData.bedrooms} Bed | ${propertyData.bathrooms} Bath`; if (propertyData.sqft) { detailsLine += ` | ${parseInt(propertyData.sqft).toLocaleString('en-US')} sq ft`; } doc.text(detailsLine, pageMargin, y); y += 11 * lineSpacing * 1.5; // --- Divider --- // FIX: Use defined JS constant instead of CSS var doc.setDrawColor(MEDIUM_GRAY); doc.setLineWidth(0.5); doc.line(pageMargin, y, pageWidth - pageMargin, y); y += 11 * lineSpacing * 1.5; // --- Description --- if (propertyData.description) { doc.setFontSize(10); const descLines = doc.splitTextToSize(propertyData.description, contentWidth); // Check page break BEFORE description const descHeight = descLines.length * 10 * lineSpacing; if (y + descHeight > doc.internal.pageSize.getHeight() - pageMargin) { doc.addPage(); y = pageMargin; } doc.text(descLines, pageMargin, y); y += descHeight + 10 * lineSpacing * 1.5; } // --- Features --- if (propertyData.features && propertyData.features.length > 0) { // Check page break BEFORE features header if (y + 14 * lineSpacing > doc.internal.pageSize.getHeight() - pageMargin) { doc.addPage(); y = pageMargin; } doc.setFontSize(12); doc.setFont(undefined, 'bold'); // FIX: Use defined JS constant instead of CSS var doc.setTextColor(DARK_GRAY); doc.text("Key Features:", pageMargin, y); y += 12 * lineSpacing * 1.2; doc.setFontSize(10); doc.setFont(undefined, 'normal'); // FIX: Use defined JS constant instead of CSS var doc.setTextColor(TEXT_COLOR); // Use columns for features if many, or just list const featureSymbol = "\u2022"; // Bullet point propertyData.features.forEach(feature => { const featureLine = `${featureSymbol} ${feature}`; const splitFeature = doc.splitTextToSize(featureLine, contentWidth); // Use full width for single column const featureHeight = splitFeature.length * 10 * lineSpacing; if (y + featureHeight > doc.internal.pageSize.getHeight() - pageMargin) { doc.addPage(); y = pageMargin; // Optionally repeat "Key Features (cont.)" header? } doc.text(splitFeature, pageMargin + 10, y); // Indent features y += featureHeight; }); } doc.save("Property_Listing.pdf"); } catch (e) { console.error("Error generating PDF:", e); showMessage("An error occurred while generating the PDF.", true); } finally { showLoader(false); } } /** Basic HTML escaping */ function escapeHtml(unsafe) { if (typeof unsafe !== 'string') return unsafe ?? ''; // Handle null/undefined or numbers return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** Shows or hides loader */ function showLoader(show) { if (!loaderOverlay) return; loaderOverlay.style.display = show ? 'flex' : 'none'; } /** Displays messages globally */ function showMessage(message, isError = false) { if (!messageArea) return; if (!message) { messageArea.style.display = 'none'; return; } messageArea.textContent = message; messageArea.className = `mt-4 text-center font-medium ${isError ? 'text-red-600' : 'text-blue-600'}`; messageArea.style.display = 'block'; // Auto-hide message after a few seconds setTimeout(() => { if(messageArea && messageArea.textContent === message) { messageArea.style.display = 'none'; } }, 4000); } // === Event Listeners === if (downloadBtn) { downloadBtn.addEventListener('click', downloadPDF); } // Add input listeners to form elements for live update (already done with oninput attribute) // === Initial Render === renderPreview(); // Render initial state (likely empty preview) updateNavButtons(); });