1// ââ Will It Be Bright â PDF Export ââ 2// Generates a branded PDF report using html2canvas + jsPDF 3// Agent mode adds branding, property-focused language, and QR code 4 5function getAgentInfo() { 6 try { 7 var stored = localStorage.getItem('wibb_agent_info'); 8 return stored ? JSON.parse(stored) : null; 9 } catch(e) { return null; } 10} 11 12function saveAgentInfo(info) { 13 try { localStorage.setItem('wibb_agent_info', JSON.stringify(info)); } catch(e) {} 14} 15 16function exportPdf(agentMode) { 17 var eventName = agentMode ? 'agent_report_export' : 'pdf_export'; 18 if (typeof plausible === 'function') plausible(eventName); 19 if (typeof html2canvas === 'undefined' || typeof jspdf === 'undefined') { 20 alert('PDF libraries still loading. Please try again in a moment.'); 21 return; 22 } 23 24 var agentInfo = agentMode ? getAgentInfo() : null; 25 var container = buildPdfLayout(agentInfo); 26 document.body.appendChild(container); 27 28 // Render compass SVG into pdf container 29 var compassEl = container.querySelector('.pdf-compass-svg'); 30 if (compassEl) renderPdfCompass(compassEl, STATE.bearing); 31 32 // Render sun bar into pdf container
33 var sunbarEl = container.querySelector('.pdf-sunbar'); 34 if (sunbarEl) renderPdfSunBar(sunbarEl, STATE.lat, STATE.lon, STATE.bearing, STATE.month); 35 36 // Render QR code if agent mode 37 var qrEl = container.querySelector('.pdf-qr'); 38 if (qrEl && typeof QRCode !== 'undefined') { 39 var shareUrl = 'https://willitbebright.com/?q=' + encodeURIComponent(STATE.display); 40 new QRCode(qrEl, { 41 text: shareUrl, 42 width: 80, 43 height: 80, 44 colorDark: '#2d2420', 45 colorLight: '#f5f0e8', 46 correctLevel: QRCode.CorrectLevel.M 47 }); 48 } 49 50 // Small delay to let QR render 51 setTimeout(function() { 52 html2canvas(container, { scale: 2, useCORS: true, backgroundColor: '#f5f0e8' }).then(function(canvas) { 53 document.body.removeChild(container); 54 var imgData = canvas.toDataURL('image/png'); 55 var pdf = new jspdf.jsPDF({ orientation: 'portrait', unit: 'pt', format: 'letter' }); 56 var pageW = pdf.internal.pageSize.getWidth(); 57 var pageH = pdf.internal.pageSize.getHeight(); 58 var imgW = pageW - 60; 59 var imgH = (canvas.height / canvas.width) * imgW; 60 if (imgH > pageH - 60) { 61 imgH = pageH - 60; 62 imgW = (canvas.width / canvas.height) * imgH; 63 } 64 var x = (pageW - imgW) / 2; 65 pdf.addImage(imgData, 'PNG', x, 30, imgW, imgH); 66 67 var slug = STATE.display.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); 68 var prefix = agentMode ? 'property-light-report-' : 'light-analysis-'; 69 pdf.save(prefix + slug + '.pdf'); 70 }).catch(function(err) { 71 if (container.parentNode) document.body.removeChild(container); 72 console.error('PDF generation failed:', err); 73 alert('PDF generation failed. Please try again.'); 74 }); 75 }, 200); 76} 77 78function renderPdfCompass(container, bearing) { 79 var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 80 svg.setAttribute('viewBox', '0 0 80 80'); 81 svg.setAttribute('width', '80'); 82 svg.setAttribute('height', '80'); 83 84 var labels = [ 85 { x: 40, y: 12, t: 'N' }, { x: 72, y: 44, t: 'E' }, 86 { x: 40, y: 76, t: 'S' }, { x: 8, y: 44, t: 'W' } 87 ]; 88 89 var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); 90 circle.setAttribute('cx', '40'); circle.setAttribute('cy', '40'); circle.setAttribute('r', '36'); 91 circle.setAttribute('fill', 'none'); circle.setAttribute('stroke', 'rgba(255,255,255,0.15)'); circle.setAttribute('stroke-width', '1'); 92 svg.appendChild(circle); 93 94 labels.forEach(function(l) { 95 var txt = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 96 txt.setAttribute('x', String(l.x)); txt.setAttribute('y', String(l.y)); txt.setAttribute('text-anchor', 'middle'); 97 txt.setAttribute('fill', 'rgba(255,255,255,0.5)'); txt.setAttribute('font-size', '8'); txt.setAttribute('font-family', 'monospace'); 98 txt.textContent = l.t; 99 svg.appendChild(txt); 100 }); 101 102 var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 103 g.setAttribute('transform', 'rotate(' + bearing + ', 40, 40)'); 104 var p1 = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 105 p1.setAttribute('points', '40,14 37,45 40,40 43,45'); p1.setAttribute('fill', '#c9a84c'); 106 var p2 = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 107 p2.setAttribute('points', '40,66 37,35 40,40 43,35'); p2.setAttribute('fill', 'rgba(255,255,255,0.25)'); 108 g.appendChild(p1); g.appendChild(p2); 109 svg.appendChild(g); 110 111 container.appendChild(svg); 112} 113 114function renderPdfSunBar(container, lat, lon, bearing, month) { 115 for (var h = 6; h <= 21; h += 0.5) { 116 var sun = getSunPosition(lat, lon, h, month); 117 var intensity = 0; 118 if (sun) { 119 var diff = Math.abs(((sun.az - bearing + 540) % 360) - 180); 120 var windowFov = diff < 60 ? 1 : diff < 90 ? 0.5 : diff < 120 ? 0.2 : 0; 121 var altF = Math.min(1, sun.alt / 35); 122 intensity = windowFov * altF; 123 } 124 var seg = document.createElement('div'); 125 seg.style.cssText = 'flex:1;height:28px;background:' + getSegColor(intensity) + ';'; 126 container.appendChild(seg); 127 } 128} 129 130function gradeColor(grade) { 131 var colors = { excellent: '#c9a84c', good: '#7a9e7e', fair: '#9e8a5a', poor: '#7a5a5a', minimal: '#6a7a88' }; 132 return colors[grade] || '#8a7a6a'; 133} 134 135function gradeTextColor(grade) { 136 return grade === 'excellent' ? '#2d2420' : '#ffffff'; 137} 138 139function buildSeasonalText(lat, lon, bearing) { 140 var delta = getSeasonalDelta(lat, lon, bearing); 141 if (delta.best.grade === delta.worst.grade) { 142 return 'Consistent ' + delta.best.grade + ' light across all seasons.'; 143 } 144 return 'Best: ' + delta.best.grade + ' light in ' + delta.best.
144season + 145 ' (' + delta.best.hours.toFixed(1) + ' hrs). Drops to ' + 146 delta.worst.grade + ' in ' + delta.worst.season + 147 ' (' + delta.worst.hours.toFixed(1) + ' hrs).'; 148} 149 150function buildPdfLayout(agentInfo) { 151 var isAgent = !!agentInfo; 152 var short = bearingToShort(STATE.bearing); 153 var dir = DIRS[short]; 154 var hours = calcDirectHours(STATE.lat, STATE.lon, STATE.bearing, STATE.month); 155 var rooms = ROOMS[short] || ROOMS['S']; 156 157 var el = document.createElement('div'); 158 el.style.cssText = 'position:fixed;left:-9999px;top:0;width:680px;padding:40px;background:#f5f0e8;font-family:Georgia,serif;color:#2d2420;z-index:-1;'; 159 160 // Build header 161 var header = document.createElement('div'); 162 header.style.cssText = 'border-bottom:2px solid #c9a84c;padding-bottom:16px;margin-bottom:24px;'; 163 164 if (isAgent) { 165 // Agent branding header 166 var agentRow = document.createElement('div'); 167 agentRow.style.cssText = 'display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;'; 168 var agentLeft = document.createElement('div'); 169 var agentName = document.createElement('div'); 170 agentName.style.cssText = 'font-size:14px;font-weight:600;color:#2d2420;'; 171 agentName.textContent = agentInfo.name; 172 agentLeft.appendChild(agentName); 173 if (agentInfo.brokerage) { 174 var brokerEl = document.createElement('div'); 175 brokerEl.style.cssText = 'font-size:11px;color:#8a7a6a;margin-top:2px;'; 176 brokerEl.textContent = agentInfo.brokerage; 177 agentLeft.appendChild(brokerEl); 178 } 179 if (agentInfo.contact) { 180 var contactEl = document.createElement('div'); 181 contactEl.style.cssText = 'font-size:10px;color:#8a7a6a;margin-top:2px;font-family:monospace;'; 182 contactEl.textContent = agentInfo.contact; 183 agentLeft.appendChild(contactEl); 184 } 185 agentRow.appendChild(agentLeft); 186 var agentRight = document.createElement('div'); 187 agentRight.style.cssText = 'text-align:right;'; 188 var reportLabel = document.createElement('div'); 189 reportLabel.style.cssText = 'font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#c9a84c;font-family:monospace;'; 190 reportLabel.textContent = 'Property Light Report'; 191 agentRight.appendChild(reportLabel); 192 agentRow.appendChild(agentRight); 193 header.appendChild(agentRow); 194 } 195 196 var eyebrow = document.createElement('div'); 197 eyebrow.style.cssText = 'font-size:11px;letter-spacing:3px;text-transform:uppercase;color:#e07b3a;margin-bottom:6px;font-family:monospace;'; 198 eyebrow.textContent = isAgent ? 'Natural Light Analysis' : 'Natural Light Analysis'; 199 header.appendChild(eyebrow); 200 var title = document.createElement('div'); 201 title.style.cssText = 'font-size:28px;font-weight:600;line-height:1.2;'; 202 title.textContent = 'Will it be '; 203 var em = document.createElement('span'); 204 em.style.cssText = 'color:#c9a84c;font-style:italic;'; 205 em.textContent = 'bright'; 206 title.appendChild(em); 207 title.appendChild(document.createTextNode('?')); 208 header.appendChild(title); 209 el.appendChild(header); 210 211 // Address 212 var addrDiv = document.createElement('div'); 213 addrDiv.style.cssText = 'font-size:16px;font-style:italic;color:#8a7a6a;padding-left:14px;border-left:3px solid #c9a84c;margin-bottom:24px;'; 214 addrDiv.textContent = STATE.display; 215 el.appendChild(addrDiv); 216 217 // Compass card 218 var compassCard = document.createElement('div'); 219 compassCard.style.cssText = 'background:#2d2420;color:#f5f0e8;padding:24px;margin-bottom:20px;display:flex;align-items:center;gap:24px;'; 220 221 var compassSvgWrap = document.createElement('div'); 222 compassSvgWrap.className = 'pdf-compass-svg'; 223 compassSvgWrap.style.cssText = 'width:80px;height:80px;flex-shrink:0;'; 224 compassCard.appendChild(compassSvgWrap); 225 226 var compassInfo = document.createElement('div'); 227 var badgeEl = document.createElement('div'); 228 badgeEl.style.cssText = 'display:inline-block;padding:3px 10px;font-size:12px;letter-spacing:1px;text-transform:uppercase;background:' + gradeColor(dir.grade) + ';color:' + gradeTextColor(dir.grade) + ';margin-bottom:8px;'; 229 badgeEl.textContent = dir.label; 230 compassInfo.appendChild(badgeEl); 231 232 var dirLabel = document.createElement('div'); 233 dirLabel.style.cssText = 'font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#c9a84c;margin-bottom:4px;'; 234 dirLabel.textContent = STATE.bearing.toFixed(0) + '\u00b0 \u2014 ' + short; 235 compassInfo.appendChild(dirLabel); 236 237 var dirHeading = document.createElement('div'); 238 dirHeading.style.cssText = 'font-size:20px;font-weight:600;margin-bottom:6px;'; 239 dirHeading.textContent = dir.long; 240 compassInfo.appendChild(dirHeading); 241 242 var hoursLabel = document.createElement('div'); 243 hoursLabel.style.cssText = 'font-size:12px;color:#b5a898;line-height:1.5;'; 244 hoursLabel.textContent = hours.toFixed(1) + ' hours of direct sun' + (isAgent ? ' for this property' : ''); 245 compassInfo.appendChild(hoursLabel); 246 compassCard.appendChild(compassInfo); 247 el.appendChild(compassCard); 248 249 // Sun bar section 250 var sunSection = document.createElement('div'); 251 sunSection.style.cssText = 'background:white;border:1px solid #e8dcc8;padding:16px;margin-bottom:20px;'; 252 var sunTitle = document.createElement('div'); 253 sunTitle.style.cssText = 'font-size:14px;font-weight:600;margin-bottom:12px;'; 254 sunTitle.textContent = 'Sunlight Timeline'; 255 sunSection.appendChild(sunTitle); 256 var sunbar = document.createElement('div');
257 sunbar.className = 'pdf-sunbar'; 258 sunbar.style.cssText = 'display:flex;gap:1px;margin-bottom:6px;'; 259 sunSection.appendChild(sunbar); 260 var sunLabels = document.createElement('div'); 261 sunLabels.style.cssText = 'display:flex;justify-content:space-between;font-size:10px;color:#8a7a6a;font-family:monospace;'; 262 ['6am','9am','12pm','3pm','6pm','9pm'].forEach(function(l) { 263 var s = document.createElement('span'); 264 s.textContent = l; 265 sunLabels.appendChild(s); 266 }); 267 sunSection.appendChild(sunLabels); 268 el.appendChild(sunSection); 269 270 // Room predictions 2x2 271 var roomGrid = document.createElement('div'); 272 roomGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px;'; 273 rooms.forEach(function(r) { 274 var roomCard = document.createElement('div'); 275 roomCard.style.cssText = 'background:white;border:1px solid #e8dcc8;padding:14px;'; 276 var rn = document.createElement('div'); 277 rn.style.cssText = 'font-size:10px;letter-spacing:2px;text-transform:uppercase;color:#8a7a6a;margin-bottom:4px;'; 278 rn.textContent = r.r; 279 roomCard.appendChild(rn); 280 var rl = document.createElement('div'); 281 rl.style.cssText = 'font-size:15px;font-weight:600;margin-bottom:4px;'; 282 rl.textContent = r.l; 283 roomCard.appendChild(rl); 284 var rd = document.createElement('div'); 285 rd.style.cssText = 'font-size:11px;color:#8a7a6a;line-height:1.5;'; 286 rd.textContent = r.d; 287 roomCard.appendChild(rd); 288 roomGrid.appendChild(roomCard); 289 }); 290 el.appendChild(roomGrid); 291 292 // Seasonal summary 293 var seasonDiv = document.createElement('div'); 294 seasonDiv.style.cssText = 'background:#edf3ea;border-left:3px solid #6a8a5a;padding:12px 16px;font-size:12px;color:#2d2420;line-height:1.6;margin-bottom:24px;'; 295 seasonDiv.textContent = buildSeasonalText(STATE.lat, STATE.lon, STATE.bearing); 296 el.appendChild(seasonDiv); 297 298 // QR code + footer 299 if (isAgent) { 300 var qrRow = document.createElement('div'); 301 qrRow.style.cssText = 'display:flex;align-items:center;gap:16px;margin-bottom:24px;padding:12px 16px;background:white;border:1px solid #e8dcc8;'; 302 var qrBox = document.createElement('div'); 303 qrBox.className = 'pdf-qr'; 304 qrBox.style.cssText = 'width:80px;height:80px;flex-shrink:0;'; 305 qrRow.appendChild(qrBox); 306 var qrText = document.createElement('div'); 307 var qrTitle = document.createElement('div'); 308 qrTitle.style.cssText = 'font-size:12px;font-weight:600;margin-bottom:4px;'; 309 qrTitle.textContent = 'View interactive analysis'; 310 qrText.appendChild(qrTitle); 311 var qrDesc = document.createElement('div'); 312 qrDesc.style.cssText = 'font-size:10px;color:#8a7a6a;line-height:1.5;'; 313 qrDesc.textContent = 'Scan to see the full interactive light analysis for this property, including seasonal comparisons and room-by-room predictions.'; 314 qrText.appendChild(qrDesc); 315 qrRow.appendChild(qrText); 316 el.appendChild(qrRow); 317 } 318 319 var footer = document.createElement('div'); 320 footer.style.cssText = 'display:flex;justify-content:space-between;font-size:10px;color:#b5a898;font-family:monospace;'; 321 var footerLeft = document.createElement('span'); 322 footerLeft.textContent = isAgent ? 'Prepared with willitbebright.com' : 'willitbebright.com'; 323 footer.appendChild(footerLeft); 324 var footerRight = document.createElement('span'); 325 footerRight.textContent = 'Generated ' + new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 326 footer.appendChild(footerRight); 327 el.appendChild(footer); 328 329 return el; 330}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.