PageSourceSearch

https://willitbebright.com/lib/pdf.js

js willitbebright.com collected 2026-10-02 01:00:15 UTC 14,750 bytes, 330 lines download raw bytes

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.