Property Listing Sheet Generator

Property Listing Sheet Generator

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('')}
`; } previewArea.innerHTML = `

${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(); });
Scroll to Top