1// The Masthead â main.js 2 3// Date display in top bar 4(function() { 5 const el = document.getElementById('current-date'); 6 if (el) { 7 const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; 8 const months = ['January','February','March','April','May','June','July','August','September','October','November','December']; 9 const now = new Date(); 10 el.textContent = days[now.getDay()] + ', ' + now.getDate() + ' ' + months[now.getMonth()] + ' ' + now.getFullYear(); 11 } 12})(); 13 14// Mobile menu toggle 15(function() { 16 const btn = document.querySelector('.mobile-menu-btn'); 17 const nav = document.querySelector('.nav-list'); 18 if (btn && nav) { 19 btn.addEventListener('click', function() { 20 nav.classList.toggle('open'); 21 btn.textContent = nav.classList.contains('open') ? 'â' : 'â°'; 22 }); 23 } 24})(); 25 26// Business Voices avatar initials â when a Q&A post's author has no profile 27// photo, post.hbs renders their full name into the small circular avatar as 28// a fallback. A full name doesn't fit a 42px circle, so this shrinks it down 29// to two letters (first + last initial) once the page loads. 30(function() { 31 document.querySelectorAll('.avatar-initials').forEach(function(el) { 32 const name = (el.getAttribute('data-fullname') || el.textContent || '').trim(); 33 if (!name) return; 34 const parts = name.split(/\s+/); 35 const initials = parts[0].charAt(0) + (parts.length > 1 ? parts[parts.length - 1].charAt(0) : ''); 36 el.textContent = initials.toUpperCase(); 37 }); 38})(); 39 40// Ticker duplicate for seamless loop 41(function() { 42 const ticker = document.getElementById('ticker'); 43 if (ticker) { 44 const clone = ticker.innerHTML; 45 ticker.innerHTML = clone + clone; 46 } 47})(); 48 49// Newsco signup form â collects email, name, organisation, location, phone 50// (all but email + name are optional, by design) and posts to the Make.com 51// webhook, which tags the signup Business/Personal by email domain for 52// audience segmentation only (no domain is ever blocked), logs the row to 53// the audience sheet, creates the Ghost member, and triggers Ghost's native 54// magic-link sign-in email. See SIGNUP_AUTOMATION.md for the full pipeline. 55(function() { 56 const form = document.getElementById('newsco-signup-form'); 57 if (!form) return; 58 59 const WEBHOOK_URL = 'https://hook.eu1.make.com/i9alnipcf2x7jl5p8qq73oj15q37wmzm'; 60 61 const successEl = document.getElementById('signup-success'); 62 const errorEl = document.getElementById('signup-error'); 63 const submitBtn = form.querySelector('.signup-submit'); 64 65 form.addEventListener('submit', function(e) { 66 e.preventDefault(); 67 errorEl.style.display = 'none'; 68 successEl.style.display = 'none'; 69 70 const email = form.email.value.trim(); 71 const name = form.name.value.trim(); 72 if (!email || !name) { 73 errorEl.textContent = 'Please fill in your email and name.'; 74 errorEl.style.display = 'block'; 75 return; 76 } 77 78 const payload = { 79 email: email, 80 name: name, 81 organisation: form.organisation.value.trim(), 82 location: form.location.value.trim(), 83 phone: form.phone.value.trim(), 84 whatsapp_consent: form.whatsapp_consent.checked 85 }; 86 87 submitBtn.disabled = true; 88 submitBtn.textContent = 'Submittingâ¦'; 89 90 fetch(WEBHOOK_URL, { 91 method: 'POST', 92 headers: { 'Content-Type': 'application/json' }, 93 body: JSON.stringify(payload) 94 }).then(function(res) { 95 if (!res.ok) throw new Error('Request failed'); 96 successEl.style.display = 'block'; 97 form.reset(); 98 }).catch(function() { 99 errorEl.textContent = 'Something went wrong. Please try again in a moment.'; 100 errorEl.style.display = 'block'; 101 }).finally(function() { 102 submitBtn.disabled = false; 103 submitBtn.textContent = "Subscribe â it's free"; 104 }); 105 }); 106})(); 107 108// Press release submission form â collects registrant + release details, 109// validates official-email-only and phone format client-side, enforces a 110// 150-word cap per paragraph (4 paragraph boxes max), and posts to the 111// Make.com webhook which 112// handles email verification, the human review queue, and fake-release 113// flagging. See PR_SUBMISSION.md for the full pipeline. 114(function() { 115 const form = document.getElementById('pr-submit-form'); 116 if (!form) return; 117 118 const WEBHOOK_URL = 'https://hook.eu1.make.com/uz1lpr1lgy34sp3mu6hv2ibq19k1lnka'; 119 const PERSONAL_EMAIL_DOMAINS = ['gmail.com', 'yahoo.com', 'yahoo.co.in', 'hotmail.com', 'outlook.com', 'live.com', 'aol.com', 'icloud.com', 'protonmail.com', 'rediffmail.com']; 120 const MAX_WORDS_PER_PARA = 150; 121 const PARA_COUNT = 4; 122 const MAX_WORDS = MAX_WORDS_PER_PARA * PARA_COUNT; 123 const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
124 // Not every contact submits a professionally-shot headshot, so this is 125 // a floor, not a quality guarantee: it catches the worst case (a tiny 126 // thumbnail or screenshot-of-a-screenshot that would look blurry or 127 // pixelated once placed in a 280px-wide article slot or homepage card), 128 // without rejecting normal phone-camera photos. 129 const MIN_IMAGE_WIDTH = 500; 130 const MIN_IMAGE_HEIGHT = 400; 131 132 const emailInput = document.getElementById('pr-email'); 133 const emailError = document.getElementById('pr-email-error'); 134 const phoneInput = document.getElementById('pr-phone'); 135 const phoneError = document.getElementById('pr-phone-error'); 136 const paraInputs = []; 137 const paraCounters = []; 138 const paraErrors = []; 139 for (let i = 1; i <= PARA_COUNT; i++) { 140 paraInputs.push(document.getElementById('pr-body-' + i)); 141 paraCounters.push(document.getElementById('pr-body-' + i + '-counter')); 142 paraErrors.push(document.getElementById('pr-body-' + i + '-error')); 143 } 144 const bodyError = document.getElementById('pr-body-error'); 145 const wordCounter = document.getElementById('pr-word-counter'); 146 const sectorSelect = document.getElementById('pr-sector'); 147 const sectorError = document.getElementById('pr-sector-error'); 148 const sectorOtherRow = document.getElementById('pr-sector-other-row'); 149 const sectorOtherInput = document.getElementById('pr-sector-other'); 150 const sectorOtherError = document.getElementById('pr-sector-other-error'); 151 const imageInput = document.getElementById('pr-image'); 152 const imageError = document.getElementById('pr-image-error'); 153 const imagePreview = document.getElementById('pr-image-preview'); 154 const imagePreviewImg = document.getElementById('pr-image-preview-img'); 155 const successEl = document.getElementById('pr-success'); 156 const errorEl = document.getElementById('pr-error'); 157 const submitBtn = document.getElementById('pr-submit-btn'); 158 159 function countWords(text) { 160 return text.trim().length ? text.trim().split(/\s+/).length : 0; 161 } 162 163 // Converts the plain-text textarea value into structured HTML so the 164 // editorial draft created in Ghost has real paragraphs and bullet 165 // lists instead of one run-on block. Blank lines separate paragraphs; 166 // a block where every line starts with â¢, - or * becomes a <ul>. 167 function escapeHtml(s) { 168 return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 169 } 170 171 function formatBodyToHtml(text) { 172 const normalized = text.replace(/\r\n/g, '\n').trim(); 173 if (!normalized) return ''; 174 const bulletRe = /^[â¢\-\*]\s+/; 175 return normalized.split(/\n{2,}/).map(function(block) { 176 const lines = block.split('\n').map(function(l) { return l.trim(); }).filter(Boolean); 177 if (lines.length && lines.every(function(l) { return bulletRe.test(l); })) { 178 const items = lines.map(function(l) { 179 return '<li>' + escapeHtml(l.replace(bulletRe, '')) + '</li>'; 180 }).join(''); 181 return '<ul>' + items + '</ul>'; 182 } 183 return '<p>' + lines.map(escapeHtml).join('<br>') + '</p>'; 184 }).join(''); 185 } 186 187 function isOfficialEmail(email) { 188 const domain = email.split('@')[1]; 189 if (!domain) return false; 190 return !PERSONAL_EMAIL_DOMAINS.includes(domain.toLowerCase()); 191 } 192 193 function isValidIndianMobile(phone) { 194 const digits = phone.replace(/\D/g, ''); 195 return /^[6-9]\d{9}$/.test(digits.slice(-10)) && digits.length >= 10; 196 } 197 198 let imageBase64 = ''; 199 200 // Builds the combined plain-text and HTML body from the 4 paragraph 201 // boxes, skipping any left empty, so downstream payload shape 202 // (body_copy / body_copy_html / word_count) stays unchanged. 203 function getParaTexts() { 204 return paraInputs.map(function(input) { return input.value.trim(); }).filter(Boolean); 205 } 206 207 function updateParaCounter(index) { 208 const words = countWords(paraInputs[index].value); 209 paraCounters[index].textContent = words + ' / ' + MAX_WORDS_PER_PARA + ' words'; 210 paraCounters[index].classList.toggle('pr-word-over', words > MAX_WORDS_PER_PARA); 211 paraErrors[index].style.display = words > MAX_WORDS_PER_PARA ? 'block' : 'none'; 212 } 213 214 function updateTotalCounter() { 215 const totalWords = paraInputs.reduce(function(sum, input) { return sum + countWords(input.value); }, 0); 216 wordCounter.textContent = totalWords + ' / ' + MAX_WORDS + ' words total'; 217 wordCounter.classList.toggle('pr-word-over', totalWords > MAX_WORDS); 218 } 219
220 paraInputs.forEach(function(input, index) { 221 input.addEventListener('input', function() { 222 updateParaCounter(index); 223 updateTotalCounter(); 224 }); 225 }); 226 227 sectorSelect.addEventListener('change', function() { 228 const isOther = sectorSelect.value === 'other'; 229 sectorOtherRow.style.display = isOther ? 'flex' : 'none'; 230 sectorOtherInput.required = isOther; 231 if (!isOther) { 232 sectorOtherInput.value = ''; 233 sectorOtherError.style.display = 'none'; 234 } 235 }); 236 237 imageInput.addEventListener('change', function() { 238 imageError.style.display = 'none'; 239 imageBase64 = ''; 240 imagePreview.style.display = 'none'; 241 const file = imageInput.files[0]; 242 if (!file) return; 243 244 if (file.size > MAX_IMAGE_BYTES) { 245 imageError.textContent = 'Image must be under 2MB.'; 246 imageError.style.display = 'block'; 247 imageInput.value = ''; 248 return; 249 } 250 251 const reader = new FileReader(); 252 reader.onload = function() { 253 const dataUrl = reader.result; 254 // Check actual pixel dimensions before accepting â file size alone 255 // doesn't catch a small/blurry image (a heavily-compressed JPEG 256 // can be tiny in bytes but still low resolution). 257 const probe = new Image(); 258 probe.onload = function() { 259 if (probe.naturalWidth < MIN_IMAGE_WIDTH || probe.naturalHeight < MIN_IMAGE_HEIGHT) { 260 imageError.textContent = 'This image is too low-resolution (' + probe.naturalWidth + 'Ã' + probe.naturalHeight + 'px). Please upload a sharper photo, at least ' + MIN_IMAGE_WIDTH + 'Ã' + MIN_IMAGE_HEIGHT + 'px.'; 261 imageError.style.display = 'block'; 262 imageInput.value = ''; 263 imageBase64 = ''; 264 imagePreview.style.display = 'none'; 265 return; 266 } 267 imageBase64 = dataUrl; 268 imagePreviewImg.src = imageBase64; 269 imagePreview.style.display = 'block'; 270 }; 271 probe.onerror = function() { 272 imageError.textContent = 'Could not read this image. Please try a different file.'; 273 imageError.style.display = 'block'; 274 imageInput.value = ''; 275 }; 276 probe.src = dataUrl; 277 }; 278 reader.readAsDataURL(file); 279 }); 280 281 form.addEventListener('submit', function(e) { 282 e.preventDefault(); 283 errorEl.style.display = 'none'; 284 successEl.style.display = 'none'; 285 emailError.style.display = 'none'; 286 phoneError.style.display = 'none'; 287 bodyError.style.display = 'none'; 288 sectorError.style.display = 'none'; 289 sectorOtherError.style.display = 'none'; 290 291 const email = form.official_email.value.trim(); 292 const phone = form.phone.value.trim(); 293 const paraTexts = getParaTexts(); 294 const bodyCopy = paraTexts.join('\n\n'); 295 const bodyCopyHtml = paraTexts.map(formatBodyToHtml).join(''); 296 // Split into "intro" (first 2 paragraphs) and "rest" (remaining), so the 297 // spokesperson quote + photo can be inserted as the 3rd block in the 298 // published article â right after the intro and before the rest of the 299 // body â regardless of how many paragraphs (1-4) were submitted. 300 const bodyIntro = paraTexts.slice(0, 2).join('\n\n'); 301 const bodyRest = paraTexts.slice(2).join('\n\n'); 302 const words = paraInputs.reduce(function(sum, input) { return sum + countWords(input.value); }, 0); 303 const anyParaOverLimit = paraInputs.some(function(input) { return countWords(input.value) > MAX_WORDS_PER_PARA; }); 304 const sector = sectorSelect.value; 305 const sectorOther = form.sector_other.value.trim(); 306 307 let valid = true; 308 309 if (!isOfficialEmail(email)) { 310 emailError.style.display = 'block'; 311 valid = false; 312 } 313 if (!isValidIndianMobile(phone)) { 314 phoneError.style.display = 'block'; 315 valid = false;
316 } 317 if (!paraInputs[0].value.trim() || anyParaOverLimit) { 318 bodyError.style.display = 'block'; 319 valid = false; 320 } 321 if (!sector) { 322 sectorError.style.display = 'block'; 323 valid = false; 324 } 325 if (sector === 'other' && !sectorOther) { 326 sectorOtherError.style.display = 'block'; 327 valid = false; 328 } 329 if (!valid) return; 330 331 const payload = { 332 contact_name: form.contact_name.value.trim(), 333 organisation: form.organisation.value.trim(), 334 official_email: email, 335 contact_location: form.contact_location.value.trim(), 336 phone: phone, 337 release_date: form.release_date.value, 338 dateline: form.dateline.value.trim(), 339 headline: form.headline.value.trim(), 340 subheadline: form.subheadline.value.trim(), 341 body_copy: bodyCopy, 342 body_copy_html: bodyCopyHtml, 343 body_intro: bodyIntro, 344 body_rest: bodyRest, 345 word_count: words, 346 sector: sector, 347 sector_other: sector === 'other' ? sectorOther : '', 348 spokesperson_image: imageBase64, 349 spokesperson_name_title: form.spokesperson_name_title.value.trim(), 350 spokesperson_quote: form.spokesperson_quote.value.trim() 351 }; 352 353 submitBtn.disabled = true; 354 submitBtn.textContent = 'Submittingâ¦'; 355 356 fetch(WEBHOOK_URL, { 357 method: 'POST', 358 headers: { 'Content-Type': 'application/json' }, 359 body: JSON.stringify(payload) 360 }).then(function(res) { 361 if (!res.ok) throw new Error('Request failed'); 362 successEl.style.display = 'block'; 363 form.reset(); 364 paraCounters.forEach(function(counter, index) { 365 counter.textContent = '0 / ' + MAX_WORDS_PER_PARA + ' words'; 366 counter.classList.remove('pr-word-over'); 367 paraErrors[index].style.display = 'none'; 368 }); 369 wordCounter.textContent = '0 / ' + MAX_WORDS + ' words total'; 370 wordCounter.classList.remove('pr-word-over'); 371 sectorOtherRow.style.display = 'none'; 372 sectorOtherInput.required = false; 373 imagePreview.style.display = 'none'; 374 imageBase64 = ''; 375 // Redirect to the relevant sector tag page so the submitter 376 // lands on content that matches their release. Fall back to 377 // /tag/press-releases/ if they chose "Other". 378 var prRedirect = (sector && sector !== 'other') ? '/tag/' + sector + '/' : '/tag/press-releases/'; 379 setTimeout(function() { 380 window.location.href = prRedirect; 381 }, 5000); 382 }).catch(function() { 383 errorEl.textContent = 'Something went wrong. Please try again in a moment.'; 384 errorEl.style.display = 'block'; 385 }).finally(function() { 386 submitBtn.disabled = false; 387 submitBtn.textContent = 'Submit for Review'; 388 }); 389 }); 390})(); 391 392// OP-In article submission form â collects author + article content, validates 393// official email, phone, word limits (300 per part, 1200 total), and the 394// originality declaration. Posts to the Make.com OP-In webhook which runs 395// Copyscape plagiarism screening and AI grammar analysis before editorial review. 396(function() { 397 const form = document.getElementById('op-in-submit-form'); 398 if (!form) return; 399 400 const WEBHOOK_URL = 'https://hook.eu1.make.com/gngzm2ehd2y3v94xh6rfpvk5ctzl2j1u'; 401 const PERSONAL_EMAIL_DOMAINS = ['gmail.com','yahoo.com','yahoo.co.in','hotmail.com','outlook.com','live.com','aol.com','icloud.com','protonmail.com','rediffmail.com']; 402 const MAX_WORDS_PER_PARA = 200; 403 const MAX_WORDS_TOTAL = 1200; 404 const MAX_IMAGE_BYTES = 2 * 1024 * 1024; 405 const MIN_IMAGE_WIDTH = 800; 406 const MIN_IMAGE_HEIGHT = 800; 407 408 let paraCount = 0; 409 const paraContainer = document.getElementById('op-para-container'); 410 const totalCounter = document.getElementById('op-word-counter'); 411 const progressFill = document.getElementById('op-word-progress'); 412 const sectorSelect = document.getElementById('op-sector'); 413 const sectorOtherRow = document.getElementById('op-sector-other-row'); 414 const sectorOtherInput = document.getElementById('op-sector-other'); 415 const successEl = document.getElementById('op-success'); 416 const errorEl = document.getElementById('op-error'); 417 const submitBtn = document.getElementById('op-submit-btn'); 418 419 function countWords(text) { 420 return text.trim().length ? text.trim().split(/\s+/).length : 0; 421 } 422 423 function getTotalWords() { 424 var total = 0; 425 paraContainer.querySelectorAll('textarea').forEach(function(ta) { total += countWords(ta.value); }); 426 return total; 427 } 428 429 function updateTotalBar() { 430 var total = getTotalWords(); 431 var pct = Math.min(100, Math.round(total / MAX_WORDS_TOTAL * 100)); 432 totalCounter.textContent = total + ' / 1,200 words total'; 433 totalCounter.classList.toggle('pr-word-over', total > MAX_WORDS_TOTAL); 434 if (progressFill) { 435 progressFill.style.width = pct + '%'; 436 progressFill.classList.toggle('op-progress-warn', total > 1000 && total <= MAX_WORDS_TOTAL); 437 progressFill.classList.toggle('op-progress-over', total > MAX_WORDS_TOTAL); 438 } 439 } 440 441 function renumberParagraphs() { 442 paraContainer.querySelectorAll('.pr-body-para-group').forEach(function(g, i) { 443 var n = i + 1; 444 var lbl = g.querySelector('.op-para-label'); 445 var shInput = g.querySelector('.op-subheading-input'); 446 var shLabel = g.querySelector('.op-subheading-label'); 447 var ta = g.querySelector('textarea'); 448 var ctr = g.querySelector('.pr-word-counter'); 449 var err = g.querySelector('.pr-field-error'); 450 if (lbl) lbl.textContent = 'Paragraph ' + n + (i === 0 ? ' *' : ''); 451 if (shInput) { shInput.id = 'op-subheading-' + n; shInput.name = 'subheading_' + n; } 452 if (shLabel) shLabel.htmlFor = 'op-subheading-' + n; 453 if (ta) ta.id = 'op-body-' + n; 454 if (ctr) ctr.id = 'op-body-' + n + '-counter'; 455 if (err) { err.id = 'op-body-' + n + '-error'; err.textContent = 'Paragraph ' + n + ' exceeds 200 words.'; } 456 }); 457 paraCount = paraContainer.querySelectorAll('.pr-body-para-group').length; 458 } 459 460 function addParagraph(isFirst) { 461 paraCount++; 462 var n = paraCount; 463 var group = document.createElement('div'); 464 group.className = 'pr-body-para-group'; 465 466 var header = document.createElement('div'); 467 header.className = 'op-para-header'; 468 469 var lbl = document.createElement('span'); 470 lbl.className = 'op-para-label'; 471 lbl.textContent = 'Paragraph ' + n + (isFirst ? ' *' : ''); 472 header.appendChild(lbl); 473 474 if (!isFirst) { 475 var rmBtn = document.createElement('button'); 476 rmBtn.type = 'button'; 477 rmBtn.className = 'op-remove-para-btn'; 478 rmBtn.textContent = 'Remove'; 479 rmBtn.addEventListener('click', function() { 480 group.remove(); 481 renumberParagraphs(); 482 updateTotalBar(); 483 }); 484 header.appendChild(rmBtn); 485 } 486 487 /* Sub-heading field â optional, becomes <h3> in published article */ 488 var shLabel = document.createElement('label'); 489 shLabel.className = 'op-subheading-label'; 490 shLabel.htmlFor = 'op-subheading-' + n; 491 shLabel.textContent = 'Sub-heading (optional)'; 492 493 var shInput = document.createElement('input'); 494 shInput.type = 'text'; 495 shInput.id = 'op-subheading-' + n; 496 shInput.name = 'subheading_' + n; 497 shInput.className = 'pr-input op-subheading-input'; 498 shInput.maxLength = 120; 499 shInput.placeholder = 'Section heading for this paragraph â leave blank if none'; 500 501 var shHint = document.createElement('span'); 502 shHint.className = 'pr-hint op-subheading-hint'; 503 shHint.textContent = 'Will appear as a red sub-heading above this paragraph on the site.'; 504 505 /* Body textarea */ 506 var ta = document.createElement('textarea'); 507 ta.id = 'op-body-' + n; 508 ta.className = 'pr-input pr-textarea'; 509 ta.rows = 5; 510 ta.placeholder = 'Write this paragraph hereâ¦'; 511 if (isFirst) ta.required = true; 512 513 var ctr = document.createElement('span'); 514 ctr.className = 'pr-word-counter'; 515 ctr.id = 'op-body-' + n + '-counter'; 516 ctr.textContent = '0 / 200 words'; 517 518 var errSpan = document.createElement('span'); 519 errSpan.className = 'pr-field-error'; 520 errSpan.id = 'op-body-' + n + '-error'; 521 errSpan.style.display = 'none'; 522 errSpan.textContent = 'Paragraph ' + n + ' exceeds 200 words.'; 523 524 ta.addEventListener('input', function() { 525 var w = countWords(ta.value); 526 ctr.textContent = w + ' / 200 words'; 527 ctr.classList.toggle('pr-word-over', w > MAX_WORDS_PER_PARA); 528 errSpan.style.display = w > MAX_WORDS_PER_PARA ? 'block' : 'none'; 529 updateTotalBar(); 530 }); 531 532 group.appendChild(header); 533 group.appendChild(shLabel); 534 group.appendChild(shInput); 535 group.appendChild(shHint); 536 group.appendChild(ta); 537 group.appendChild(ctr); 538 group.appendChild(errSpan); 539 paraContainer.appendChild(group); 540 updateTotalBar(); 541 } 542 543 // Initialise with 4 paragraphs 544 addParagraph(true); 545 addParagraph(false); 546 addParagraph(false); 547 addParagraph(false); 548 549 var addBtn = document.getElementById('op-add-para-btn'); 550 if (addBtn) { addBtn.addEventListener('click', function() { addParagraph(false); }); } 551 552 function isOfficialEmail(email) { 553 var domain = (email.split('@')[1] || '').toLowerCase(); 554 return domain && !PERSONAL_EMAIL_DOMAINS.includes(domain); 555 } 556 557 function isValidIndianMobile(phone) { 558 var digits = phone.replace(/\D/g, ''); 559 return /^[6-9]\d{9}$/.test(digits.slice(-10)) && digits.length >= 10; 560 } 561 562 if (sectorSelect) { 563 sectorSelect.addEventListener('change', function() { 564 var isOther = sectorSelect.value === 'other'; 565 sectorOtherRow.style.display = isOther ? 'flex' : 'none'; 566 if (!isOther) { sectorOtherInput.value = ''; } 567 }); 568 } 569 570 var imageBase64 = ''; 571 var imageInput = document.getElementById('op-image'); 572 if (imageInput) { 573 imageInput.addEventListener('change', function() { 574 var errEl = document.getElementById('op-image-error'); 575 var preview = document.getElementById('op-image-preview'); 576 var previewImg = document.getElementById('op-image-preview-img'); 577 errEl.style.display = 'none'; imageBase64 = ''; preview.style.display = 'none'; 578 var file = imageInput.files[0]; 579 if (!file) return; 580 if (file.size > MAX_IMAGE_BYTES) { 581 errEl.textContent = 'Image must be under 2MB.'; 582 errEl.style.display = 'block'; imageInput.value = ''; return; 583 } 584 var reader = new FileReader(); 585 reader.onload = function() { 586 var dataUrl = reader.result; 587 var probe = new Image(); 588 probe.onload = function() {
589 if (probe.naturalWidth < MIN_IMAGE_WIDTH || probe.naturalHeight < MIN_IMAGE_HEIGHT) { 590 errEl.textContent = 'Image too small (' + probe.naturalWidth + '\xd7' + probe.naturalHeight + 'px). Minimum 800\xd7800px.'; 591 errEl.style.display = 'block'; imageInput.value = ''; imageBase64 = ''; return; 592 } 593 imageBase64 = dataUrl; previewImg.src = dataUrl; preview.style.display = 'block'; 594 }; 595 probe.onerror = function() { 596 errEl.textContent = 'Could not read image. Please try a different file.'; 597 errEl.style.display = 'block'; imageInput.value = ''; 598 }; 599 probe.src = dataUrl; 600 }; 601 reader.readAsDataURL(file); 602 }); 603 } 604 605 form.addEventListener('submit', function(e) { 606 e.preventDefault(); 607 if (errorEl) errorEl.style.display = 'none'; 608 if (successEl) successEl.style.display = 'none'; 609 var valid = true; 610 611 var emailInput = document.getElementById('op-email'); 612 var emailError = document.getElementById('op-email-error'); 613 if (emailInput && !isOfficialEmail(emailInput.value.trim())) { 614 emailError.style.display = 'block'; valid = false; 615 } else if (emailError) { emailError.style.display = 'none'; } 616 617 var phoneInput = document.getElementById('op-phone'); 618 var phoneError = document.getElementById('op-phone-error'); 619 if (phoneInput && !isValidIndianMobile(phoneInput.value.trim())) { 620 phoneError.style.display = 'block'; valid = false; 621 } else if (phoneError) { phoneError.style.display = 'none'; } 622 623 var bodyError = document.getElementById('op-body-error'); 624 var textareas = paraContainer.querySelectorAll('textarea'); 625 var partTexts = []; 626 var partSubheadings = []; 627 var bodyValid = true; 628 textareas.forEach(function(ta) { 629 var w = countWords(ta.value); 630 partTexts.push(ta.value.trim()); 631 var shInput = ta.parentNode.querySelector('.op-subheading-input'); 632 partSubheadings.push(shInput ? shInput.value.trim() : ''); 633 var errEl = ta.parentNode.querySelector('.pr-field-error'); 634 if (w > MAX_WORDS_PER_PARA) { if (errEl) errEl.style.display = 'block'; bodyValid = false; } 635 else { if (errEl) errEl.style.display = 'none'; } 636 }); 637 if (!partTexts.length || countWords(partTexts[0]) === 0) { bodyValid = false; } 638 if (getTotalWords() > MAX_WORDS_TOTAL) { bodyValid = false; } 639 if (!bodyValid) { if (bodyError) bodyError.style.display = 'block'; valid = false; } 640 else { if (bodyError) bodyError.style.display = 'none'; } 641 642 var sectorError = document.getElementById('op-sector-error'); 643 if (sectorSelect && !sectorSelect.value) { 644 sectorError.style.display = 'block'; valid = false; 645 } else if (sectorError) { sectorError.style.display = 'none'; } 646 if (sectorSelect && sectorSelect.value === 'other' && sectorOtherInput && !sectorOtherInput.value.trim()) { 647 document.getElementById('op-sector-other-error').style.display = 'block'; valid = false; 648 } else { var otherErr = document.getElementById('op-sector-other-error'); if (otherErr) otherErr.style.display = 'none'; } 649 650 var decl = document.getElementById('op-declaration'); 651 var declErr = document.getElementById('op-declaration-error'); 652 if (decl && !decl.checked) { declErr.style.display = 'block'; valid = false; } 653 else if (declErr) { declErr.style.display = 'none'; } 654 655 if (!valid) return; 656 657 var wordCount = getTotalWords(); 658 659 // Build the article HTML automatically: 660 // subheading present â <h3>heading</h3><p>body</p> 661 // no subheading â <p>body</p> 662 var bodyHtml = partTexts.filter(Boolean).map(function(text, i) { 663 var sh = partSubheadings[i] || ''; 664 var escaped = text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 665 var shHtml = sh ? '<h3>' + sh.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</h3>' : ''; 666 return shHtml + '<p>' + escaped + '</p>'; 667 }).join('\n'); 668 669 var payload = { 670 author_name: document.getElementById('op-name').value.trim(), 671 designation: document.getElementById('op-designation').value.trim(), 672 organisation: document.getElementById('op-org').value.trim(), 673 official_email: emailInput.value.trim(), 674 phone: phoneInput.value.trim(),
675 linkedin_url: (document.getElementById('op-linkedin').value || '').trim(), 676 headline: document.getElementById('op-headline').value.trim(), 677 subheadline: document.getElementById('op-subheadline').value.trim(), 678 sector: sectorSelect.value, 679 sector_other: (sectorOtherInput ? sectorOtherInput.value.trim() : ''), 680 body_html: bodyHtml, 681 body_combined: partTexts.filter(Boolean).join('\n\n'), 682 word_count: wordCount, 683 author_image: imageBase64, 684 declaration: true, 685 submitted_at: new Date().toISOString() 686 }; 687 partTexts.forEach(function(t, i) { 688 payload['body_part_' + (i + 1)] = t; 689 payload['subheading_' + (i + 1)] = partSubheadings[i] || ''; 690 }); 691 692 submitBtn.disabled = true; 693 submitBtn.textContent = 'Submittingâ¦'; 694 695 fetch(WEBHOOK_URL, { 696 method: 'POST', 697 headers: { 'Content-Type': 'application/json' }, 698 body: JSON.stringify(payload) 699 }).then(function(res) { 700 if (!res.ok) throw new Error('failed'); 701 successEl.style.display = 'block'; 702 form.style.display = 'none'; 703 // Redirect to the relevant sector tag page. Fall back to /tag/op-in/ 704 // if they chose "Other" or no sector was selected. 705 var opSector = sectorSelect ? sectorSelect.value : ''; 706 var opRedirect = (opSector && opSector !== 'other') ? '/tag/' + opSector + '/' : '/tag/op-in/'; 707 setTimeout(function() { window.location.href = opRedirect; }, 5000); 708 }).catch(function() { 709 errorEl.textContent = 'Something went wrong. Please try again in a moment.'; 710 errorEl.style.display = 'block'; 711 submitBtn.disabled = false; 712 submitBtn.textContent = 'Submit for Review'; 713 }); 714 }); 715})(); 716 717// Fixed masthead â logo + nav are position:fixed so they never move. 718// We dynamically measure the masthead height and apply it as body 719// padding-top so content starts below the header on all screen sizes. 720(function() { 721 const masthead = document.querySelector('.sticky-masthead'); 722 if (!masthead) return; 723 724 function applyPadding() { 725 document.body.style.paddingTop = masthead.offsetHeight + 'px'; 726 } 727 applyPadding(); 728 window.addEventListener('resize', applyPadding, { passive: true }); 729 730 window.addEventListener('scroll', function() { 731 const scrolled = window.scrollY > 80; 732 const wasScrolled = masthead.classList.contains('is-scrolled'); 733 masthead.classList.toggle('is-scrolled', scrolled); 734 // Recalculate padding when masthead height changes (scroll state change) 735 if (scrolled !== wasScrolled) applyPadding(); 736 }, { passive: true }); 737})(); 738 739// Segment-aware feature image handler. 740// Reads data-segment from the article element and applies the correct layout: 741// press-releases â Spokesperson Callout Block (portrait + blockquote side by side) 742// qa â NC5 Author Card at top (circular portrait) + Q&A label colouring 743// op-in â OP-In Author Byline at top (circular portrait) + bio at bottom 744// all others â float right from paragraph 1 745(function() { 746 var figure = document.getElementById('post-feature-image-inline'); 747 var body = document.querySelector('.post-body'); 748 var segmentEl = document.getElementById('article-segment'); 749 if (!body) return; 750 751 var segment = (segmentEl && segmentEl.dataset.segment) ? segmentEl.dataset.segment : ''; 752 753 // ââ PRESS RELEASE ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 754 if (segment === 'press-releases') { 755 var bq = body.querySelector('blockquote'); 756 if (figure && bq) { 757 var callout = document.createElement('div'); 758 callout.className = 'spokesperson-callout'; 759 760 // Image column 761 var imgCol = document.createElement('div'); 762 imgCol.className = 'sc-image'; 763 var img = figure.querySelector('img'); 764 if (img) imgCol.appendChild(img.cloneNode(true)); 765 var figcap = figure.querySelector('figcaption');
766 if (figcap) { 767 var cap = document.createElement('p'); 768 cap.className = 'sc-caption'; 769 cap.innerHTML = figcap.innerHTML; 770 imgCol.appendChild(cap); 771 } 772 773 // Quote column 774 var quoteCol = document.createElement('div'); 775 quoteCol.className = 'sc-quote'; 776 quoteCol.appendChild(bq.cloneNode(true)); 777 778 callout.appendChild(imgCol); 779 callout.appendChild(quoteCol); 780 781 bq.parentNode.replaceChild(callout, bq); 782 figure.remove(); 783 } 784 return; 785 } 786 787 // ââ NC5 (QA) âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 788 if (segment === 'nc5') { 789 if (figure) { 790 var card = document.createElement('div'); 791 card.className = 'nc5-author-card'; 792 793 var ncImg = figure.querySelector('img'); 794 if (ncImg) { 795 var ncImgClone = ncImg.cloneNode(true); 796 ncImgClone.className = 'nc5-portrait'; 797 card.appendChild(ncImgClone); 798 } 799 800 var info = document.createElement('div'); 801 info.className = 'nc5-author-info'; 802 // Name and title come from the feature image caption. 803 // Editors should set it as "Interviewee Name | Title, Organisation" 804 // e.g. "Neeraj Saxena | CEO & M.D., Auxilo Finserve" 805 var figcapNC = figure.querySelector('figcaption'); 806 var capText = figcapNC ? figcapNC.textContent.trim() : ''; 807 var capParts = capText.split('|'); 808 var name = capParts[0] ? capParts[0].trim() : ''; 809 var bio = capParts[1] ? capParts[1].trim() : ''; 810 info.innerHTML = (name ? '<h3>' + name + '</h3>' : '') + 811 (bio ? '<p>' + bio + '</p>' : ''); 812 card.appendChild(info); 813 814 body.insertAdjacentElement('beforebegin', card); 815 figure.remove(); 816 } 817 818 // Colour NC: questions and speaker answers 819 body.querySelectorAll('p').forEach(function(p) { 820 var fec = p.firstElementChild; 821 if (fec && fec.tagName === 'STRONG') { 822 var label = fec.textContent.trim(); 823 if (label === 'NC:') { 824 p.classList.add('nc5-q'); 825 } else if (label.slice(-1) === ':') { 826 p.classList.add('nc5-a'); 827 } 828 } 829 }); 830 return; 831 } 832 833 // ââ OP-IN âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 834 if (segment === 'op-in') { 835 // Primary: read author data from hidden #opin-meta div injected by Make.com 836 var metaDiv = document.getElementById('opin-meta'); 837 var opAuthor = metaDiv ? (metaDiv.getAttribute('data-name') || '').trim() : ''; 838 var opDesig = metaDiv ? (metaDiv.getAttribute('data-designation') || '').trim() : ''; 839 var opOrg = metaDiv ? (metaDiv.getAttribute('data-org') || '').trim() : ''; 840 var opDesigOrg = [opDesig, opOrg].filter(Boolean).join(', '); 841 var opTitle = opDesigOrg ? opDesigOrg + ' | Views are personal.' : 'Views are personal.'; 842 843 // Fallback: if no meta div, try figcaption pipe-format 844 if (!opAuthor && figure) { 845 var figcapOp = figure.querySelector('figcaption'); 846 var capRaw = figcapOp ? figcapOp.textContent.trim() : ''; 847 var capParts = capRaw.split('|'); 848 opAuthor = capParts[0] ? capParts[0].trim() : ''; 849 opTitle = capParts[1] ? capParts[1].trim() : opTitle; 850 } 851 852 // Last resort: Ghost author name (but never "newsdesk") 853 if (!opAuthor) { 854 var metaName = document.querySelector('.author-name'); 855 var fbName = metaName ? metaName.textContent.trim() : ''; 856 opAuthor = (fbName && fbName.toLowerCase() !== 'newsdesk') ? fbName : ''; 857 } 858 859 var byline = document.createElement('div'); 860 byline.className = 'opin-byline'; 861 862 // Photo: from feature image figure if present 863 if (figure) {
864 var opImg = figure.querySelector('img'); 865 if (opImg) byline.appendChild(opImg.cloneNode(true)); 866 figure.remove(); 867 } 868 869 var opText = document.createElement('div'); 870 opText.className = 'opin-byline-text'; 871 opText.innerHTML = (opAuthor ? '<strong>' + opAuthor + '</strong>' : '') + 872 '<span>' + opTitle + '</span>'; 873 byline.appendChild(opText); 874 body.insertAdjacentElement('beforebegin', byline); 875 if (metaDiv) metaDiv.remove(); 876 877 // Hide the Ghost "newsdesk" meta author row â byline card replaces it 878 var authorInfo = document.querySelector('.post-author-info'); 879 if (authorInfo) authorInfo.style.display = 'none'; 880 881 // Style the existing author box as a bottom bio card 882 var authorBox = document.querySelector('.author-box'); 883 if (authorBox) authorBox.classList.add('opin-author-bio'); 884 return; 885 } 886 887 // ââ ALL OTHER SEGMENTS â float right from paragraph 1 ââââââââââââââââââââ 888 if (!figure) return; 889 // If the article uses an nc5-leader-card (spokesperson bio block), remove 890 // the feature image entirely â the headshot is the visual anchor. 891 if (body.querySelector('.nc5-leader-card')) { 892 figure.remove(); 893 return; 894 } 895 var paras = body.querySelectorAll('p'); 896 if (paras.length >= 1) { 897 paras[0].insertAdjacentElement('beforebegin', figure); 898 } 899})(); 900 901// Quick Read card â removed per editorial decision (all segments). 902(function() { 903 var content = document.querySelector('.gh-content'); 904 if (!content) return; 905 content.querySelectorAll('p').forEach(function(p) { 906 var strong = p.querySelector('strong'); 907 if (!strong || strong.textContent.trim() !== 'Quick Read:') return; 908 var nextEl = p.nextElementSibling; 909 p.style.display = 'none'; 910 if (nextEl && nextEl.tagName === 'UL') nextEl.style.display = 'none'; 911 }); 912})(); 913 914// NC5 article submission form â reporters fill in leader profile, 5 Q&A pairs 915// and a Newsco Takeaway. Submits directly to Ghost Admin API (no Make.com). 916// To set up: Ghost Admin â Settings â Integrations â Add custom integration 917// â name "NC5 Submit" â copy the Admin API Key â paste below. 918(function() { 919 var form = document.getElementById('nc5-submit-form'); 920 if (!form) return; 921 922 // Ghost Admin API key (id:secret) â from Ghost Admin â Settings â Integrations 923 var GHOST_ADMIN_KEY = '6a355ce2ad17f25da61c88ac:32b8d5bb808fad825eb90e592e6d2da8a04d07d49ec6e5ba3f547b0a7b323591'; 924 var GHOST_API_URL = '/ghost/api/admin/posts/?source=html'; 925 var MAX_IMG_BYTES = 2 * 1024 * 1024; 926 927 var successEl = document.getElementById('nc5-success'); 928 var errorEl = document.getElementById('nc5-error'); 929 var submitBtn = document.getElementById('nc5-submit-btn'); 930 var photoInput = document.getElementById('nc5-photo'); 931 var photoError = document.getElementById('nc5-photo-error'); 932 var photoPreview = document.getElementById('nc5-photo-preview'); 933 var photoPreviewImg= document.getElementById('nc5-photo-preview-img'); 934 var photoBase64 = ''; 935 var photoFile = null; // actual File object â uploaded to Ghost media library on submit 936 937 // Photo upload â preview only; actual upload happens at submit time 938 photoInput.addEventListener('change', function() { 939 var file = photoInput.files[0]; 940 photoError.style.display = 'none'; 941 photoPreview.style.display = 'none'; 942 photoBase64 = ''; 943 photoFile = null; 944 if (!file) return; 945 if (file.size > MAX_IMG_BYTES) { 946 photoError.textContent = 'Image must be under 2MB.'; 947 photoError.style.display = 'block'; 948 photoInput.value = ''; 949 return; 950 } 951 photoFile = file; 952 var reader = new FileReader(); 953 reader.onload = function(ev) { 954 photoBase64 = ev.target.result; 955 photoPreviewImg.src = photoBase64; 956 photoPreview.style.display = 'block'; 957 };
958 reader.readAsDataURL(file); 959 }); 960 961 // Upload a File to Ghost media library; returns Promise<url string> 962 function uploadImageToGhost(file, token) { 963 var fd = new FormData(); 964 fd.append('file', file, file.name); 965 fd.append('purpose', 'image'); 966 return fetch('/ghost/api/admin/images/upload/', { 967 method: 'POST', 968 headers: { 'Authorization': 'Ghost ' + token }, 969 body: fd 970 }).then(function(res) { 971 if (!res.ok) throw new Error('Image upload failed'); 972 return res.json(); 973 }).then(function(data) { 974 return data.images[0].url; 975 }); 976 } 977 978 function esc(str) { 979 return String(str) 980 .replace(/&/g, '&') 981 .replace(/</g, '<') 982 .replace(/>/g, '>') 983 .replace(/"/g, '"'); 984 } 985 986 function toParas(str) { 987 return esc(str).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>'); 988 } 989 990 function autoInitials(name) { 991 var parts = name.trim().split(/\s+/).filter(Boolean); 992 if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); 993 if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); 994 return 'NC'; 995 } 996 997 function buildNc5Html(d) { 998 var firstName = d.leader_name.trim().split(/\s+/)[0]; 999 1000 var photoHtml = d.photo 1001 ? '<img src="' + d.photo + '" class="nc5-leader-photo" alt="' + esc(d.leader_name) + '">' 1002 : '<div class="nc5-leader-photo-placeholder">' + autoInitials(d.leader_name) + '</div>'; 1003 1004 var takeawayHtml = '<div class="nc5-takeaway">\n' + 1005 ' <p>' + esc(d.takeaway) + '</p>\n' + 1006 '</div>'; 1007 1008 var qaBlocks = [1,2,3,4,5].map(function(i) { 1009 var block = '<div class="nc5-qa">\n' + 1010 ' <div class="nc5-question">\n' + 1011 ' <span class="nc5-q-num">' + i + '</span>\n' + 1012 ' <span class="nc5-q-sep"></span>\n' + 1013 ' <p class="nc5-q-text"><strong>' + esc(d['q' + i]) + '</strong></p>\n' + 1014 ' </div>\n' + 1015 ' <div class="nc5-answer">\n' + 1016 ' <p><strong>' + esc(firstName) + ':</strong> ' + toParas(d['a' + i]) + '</p>\n' + 1017 ' </div>\n' + 1018 '</div>'; 1019 return i === 2 ? block + '\n' + takeawayHtml : block; 1020 }).join('\n'); 1021 1022 // Wrap in Ghost HTML card markers so the API stores the HTML exactly 1023 // as-is (raw HTML card), preserving all nc5-* classes without Ghost's 1024 // Lexical parser converting or stripping them. 1025 return '<!--kg-card-begin: html-->\n' + 1026 '<div class="nc5-leader-card">\n' + 1027 ' ' + photoHtml + '\n' + 1028 ' <div class="nc5-leader-info">\n' + 1029 ' <p class="nc5-leader-name">' + esc(d.leader_name) + '</p>\n' + 1030 ' <p class="nc5-leader-title">' + esc(d.leader_title_org) + '</p>\n' + 1031 ' <p class="nc5-leader-bio">' + esc(d.leader_bio) + '</p>\n' + 1032 ' </div>\n' + 1033 '</div>\n' + 1034 '<div class="nc5-divider">\n' + 1035 ' <span class="nc5-divider-line"></span>\n' + 1036 ' <span class="nc5-divider-line"></span>\n' + 1037 '</div>\n' + 1038 qaBlocks + '\n' + 1039 '<!--kg-card-end: html-->'; 1040 } 1041 1042 form.addEventListener('submit', function(e) { 1043 e.preventDefault(); 1044 errorEl.style.display = 'none'; 1045 successEl.style.display = 'none'; 1046 1047 var d = { 1048 title: document.getElementById('nc5-title').value.trim() + ' â ' + form.leader_name.value.trim(), 1049 reporter_name: 'newsco', 1050 leader_name: form.leader_name.value.trim(), 1051 leader_title_org: form.leader_title_org.value.trim(), 1052 leader_bio: form.leader_bio.value.trim(), 1053 photo: photoBase64, 1054 q1: form.q1.value.trim(), a1: form.a1.value.trim(), 1055 q2: form.q2.value.trim(), a2: form.a2.value.trim(), 1056 q3: form.q3.value.trim(), a3: form.a3.value.trim(), 1057 q4: form.q4.value.trim(), a4: form.a4.value.trim(), 1058 q5: form.q5.value.trim(), a5: form.a5.value.trim(), 1059 takeaway: form.takeaway.value.trim() 1060 }; 1061
1062 var required = ['title','leader_name','leader_title_org','leader_bio', 1063 'q1','a1','q2','a2','q3','a3','q4','a4','q5','a5','takeaway']; 1064 for (var i = 0; i < required.length; i++) { 1065 if (!d[required[i]]) { 1066 errorEl.textContent = 'Please fill in all required fields.'; 1067 errorEl.style.display = 'block'; 1068 return; 1069 } 1070 } 1071 1072 submitBtn.disabled = true; 1073 submitBtn.textContent = 'Creating draftâ¦'; 1074 1075 // 1. Get JWT, 2. Upload photo (if any) to Ghost media, 3. Create draft post 1076 ghostJWT(GHOST_ADMIN_KEY).then(function(token) { 1077 var imgPromise = photoFile 1078 ? uploadImageToGhost(photoFile, token).then(function(url) { d.photo = url; }) 1079 : Promise.resolve(); 1080 return imgPromise.then(function() { 1081 d.html = buildNc5Html(d); 1082 return fetch(GHOST_API_URL, { 1083 method: 'POST', 1084 headers: { 1085 'Content-Type': 'application/json', 1086 'Authorization': 'Ghost ' + token 1087 }, 1088 body: JSON.stringify({ 1089 posts: [{ 1090 title: d.title, 1091 html: d.html, 1092 status: 'draft', 1093 tags: [{ slug: 'nc5' }] 1094 }] 1095 }) 1096 }); 1097 }); 1098 }).then(function(res) { 1099 if (!res.ok) return res.json().then(function(e) { throw new Error(JSON.stringify(e)); }); 1100 return res.json(); 1101 }).then(function() { 1102 successEl.style.display = 'block'; 1103 form.reset(); 1104 photoBase64 = ''; 1105 photoFile = null; 1106 photoPreview.style.display = 'none'; 1107 window.scrollTo({ top: 0, behavior: 'smooth' }); 1108 }).catch(function(err) { 1109 console.error('NC5 submit error:', err); 1110 errorEl.textContent = 'Something went wrong. Please try again in a moment.'; 1111 errorEl.style.display = 'block'; 1112 }).finally(function() { 1113 submitBtn.disabled = false; 1114 submitBtn.textContent = 'Create Draft â'; 1115 }); 1116 }); 1117 1118 // Generate a Ghost Admin API JWT (HS256) from an "id:hexsecret" key. 1119 function ghostJWT(apiKey) { 1120 var parts = apiKey.split(':'); 1121 var id = parts[0]; 1122 var secret = parts[1]; 1123 var iat = Math.floor(Date.now() / 1000); 1124 1125 function b64url(str) { 1126 return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 1127 } 1128 var header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: id })); 1129 var payload = b64url(JSON.stringify({ iat: iat, exp: iat + 300, aud: '/admin/' })); 1130 var unsigned = header + '.' + payload; 1131 1132 // Decode hex secret â Uint8Array 1133 var secretBytes = new Uint8Array(secret.match(/.{2}/g).map(function(b) { return parseInt(b, 16); })); 1134 1135 return crypto.subtle.importKey( 1136 'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] 1137 ).then(function(key) { 1138 return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(unsigned)); 1139 }).then(function(sig) { 1140 var sigB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sig))) 1141 .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 1142 return unsigned + '.' + sigB64; 1143 }); 1144 } 1145})(); 1146 1147// IN CONVERSATION article submission form â reporters fill in guest profile, 1148// intro, unlimited Q&A pairs, and a pull quote. Creates Ghost draft automatically. 1149(function() { 1150 var form = document.getElementById('or-submit-form'); 1151 if (!form) return; 1152 1153 var GHOST_ADMIN_KEY = '6a355ce2ad17f25da61c88ac:32b8d5bb808fad825eb90e592e6d2da8a04d07d49ec6e5ba3f547b0a7b323591'; 1154 var GHOST_API_URL = '/ghost/api/admin/posts/?source=html'; 1155 var MAX_IMG_BYTES = 2 * 1024 * 1024; 1156 1157 var successEl = document.getElementById('or-success'); 1158 var errorEl = document.getElementById('or-error'); 1159 var submitBtn = document.getElementById('or-submit-btn'); 1160 var photoInput = document.getElementById('or-photo'); 1161 var photoError = document.getElementById('or-photo-error'); 1162 var photoPreview = document.getElementById('or-photo-preview'); 1163 var photoPreviewImg = document.getElementById('or-photo-preview-img'); 1164 var photoFile = null; 1165 var photoBase64 = ''; 1166 var qaList = document.getElementById('or-qa-list'); 1167 var addQaBtn = document.getElementById('or-add-qa'); 1168 var qaPairCount = 7; 1169 1170 // Photo preview 1171 photoInput.addEventListener('change', function() {
1172 photoError.style.display = 'none'; 1173 photoPreview.style.display = 'none'; 1174 photoBase64 = ''; 1175 photoFile = null; 1176 var file = photoInput.files[0]; 1177 if (!file) return; 1178 if (file.size > MAX_IMG_BYTES) { 1179 photoError.textContent = 'Image must be under 2MB.'; 1180 photoError.style.display = 'block'; 1181 photoInput.value = ''; 1182 return; 1183 } 1184 photoFile = file; 1185 var reader = new FileReader(); 1186 reader.onload = function(ev) { 1187 photoBase64 = ev.target.result; 1188 photoPreviewImg.src = photoBase64; 1189 photoPreview.style.display = 'block'; 1190 }; 1191 reader.readAsDataURL(file); 1192 }); 1193 1194 // Add another Q&A pair dynamically 1195 addQaBtn.addEventListener('click', function() { 1196 qaPairCount++; 1197 var n = qaPairCount; 1198 var div = document.createElement('div'); 1199 div.className = 'pr-body-para-group or-qa-pair'; 1200 div.dataset.num = n; 1201 div.innerHTML = 1202 '<label>Question ' + n + '</label>' + 1203 '<textarea name="q' + n + '" class="pr-input pr-textarea or-q" rows="2"></textarea>' + 1204 '<label class="nc5-answer-label">Answer ' + n + '</label>' + 1205 '<textarea name="a' + n + '" class="pr-input pr-textarea or-a" rows="4"></textarea>'; 1206 qaList.appendChild(div); 1207 div.querySelector('textarea').focus(); 1208 }); 1209 1210 function esc(str) { 1211 return String(str) 1212 .replace(/&/g, '&').replace(/</g, '<') 1213 .replace(/>/g, '>').replace(/"/g, '"'); 1214 } 1215 1216 function toParas(str) { 1217 return esc(str).replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>'); 1218 } 1219 1220 function ghostJWT(apiKey) { 1221 var parts = apiKey.split(':'); 1222 var id = parts[0]; 1223 var secret = parts[1]; 1224 var iat = Math.floor(Date.now() / 1000); 1225 function b64url(str) { 1226 return btoa(str).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 1227 } 1228 var header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT', kid: id })); 1229 var payload = b64url(JSON.stringify({ iat: iat, exp: iat + 300, aud: '/admin/' })); 1230 var unsigned = header + '.' + payload; 1231 var secretBytes = new Uint8Array(secret.match(/.{2}/g).map(function(b) { return parseInt(b, 16); })); 1232 return crypto.subtle.importKey( 1233 'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] 1234 ).then(function(key) { 1235 return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(unsigned)); 1236 }).then(function(sig) { 1237 var sigB64 = btoa(String.fromCharCode.apply(null, new Uint8Array(sig))) 1238 .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 1239 return unsigned + '.' + sigB64; 1240 }); 1241 } 1242 1243 function uploadImageToGhost(file, token) { 1244 var fd = new FormData(); 1245 fd.append('file', file, file.name); 1246 fd.append('purpose', 'image'); 1247 return fetch('/ghost/api/admin/images/upload/', { 1248 method: 'POST', 1249 headers: { 'Authorization': 'Ghost ' + token }, 1250 body: fd 1251 }).then(function(res) { 1252 if (!res.ok) throw new Error('Image upload failed'); 1253 return res.json(); 1254 }).then(function(data) { return data.images[0].url; }); 1255 } 1256 1257 function buildOrHtml(d) { 1258 var firstName = d.guest_name.trim().split(/\s+/)[0]; 1259 1260 var photoHtml = d.photo 1261 ? '<img src="' + d.photo + '" class="nc5-leader-photo" alt="' + esc(d.guest_name) + '">' 1262 : '<div class="nc5-leader-photo-placeholder">' + 1263 (function(name) { 1264 var p = name.trim().split(/\s+/).filter(Boolean); 1265 if (p.length >= 2) return (p[0][0] + p[p.length-1][0]).toUpperCase(); 1266 return p[0] ? p[0].slice(0,2).toUpperCase() : 'IC'; 1267 })(d.guest_name) + '</div>'; 1268 1269 var takeawayHtml = 1270 '<div class="nc5-takeaway ic-pullquote">\n' + 1271 ' <p class="nc5-takeaway-label">In Their Words</p>\n' + 1272 ' <p>“' + esc(d.quote) + '”</p>\n' + 1273 ' <span class="nc5-takeaway-attr">— ' + esc(d.guest_name) + ', ' + esc(d.guest_title) + ', ' + esc(d.guest_company) + '</span>\n' + 1274 '</div>'; 1275 1276 // Insert pull quote after Q3 (or after last Q if fewer than 3) 1277 var insertAfter = Math.min(2, d.pairs.length - 1); 1278 var qaBlocks = d.pairs.map(function(pair, idx) { 1279 var num = String(idx + 1).padStart(2, '0'); 1280 var block = '<div class="nc5-qa">\n' + 1281 ' <div class="nc5-question">\n' +
1282 ' <span class="nc5-q-num">' + num + '</span>\n' + 1283 ' <span class="nc5-q-sep"></span>\n' + 1284 ' <p class="nc5-q-text"><strong>' + esc(pair.q) + '</strong></p>\n' + 1285 ' </div>\n' + 1286 ' <div class="nc5-answer">\n' + 1287 ' <p><strong>' + esc(firstName) + ':</strong> ' + toParas(pair.a) + '</p>\n' + 1288 ' </div>\n' + 1289 '</div>'; 1290 if (idx === insertAfter) { block += '\n' + takeawayHtml; } 1291 return block; 1292 }).join('\n'); 1293 1294 return '<!--kg-card-begin: html-->\n' + 1295 '<div class="ic-article">\n' + 1296 '<div class="nc5-divider ic-section-header">\n' + 1297 ' <span class="nc5-divider-line"></span>\n' + 1298 ' <span class="nc5-divider-label ic-divider-label">Inside Business</span>\n' + 1299 ' <span class="nc5-divider-line"></span>\n' + 1300 '</div>\n' + 1301 '<div class="ic-leader-wrap">\n' + 1302 '<div class="nc5-leader-card">\n' + 1303 ' ' + photoHtml + '\n' + 1304 ' <div class="nc5-leader-info">\n' + 1305 ' <p class="nc5-leader-name">' + esc(d.guest_name) + '</p>\n' + 1306 ' <p class="nc5-leader-title">' + esc(d.guest_title) + ' · ' + esc(d.guest_company) + '</p>\n' + 1307 ' </div>\n' + 1308 '</div>\n' + 1309 '<p class="ic-intro">' + esc(d.intro) + '</p>\n' + 1310 '</div>\n' + 1311 qaBlocks + '\n' + 1312 '</div>\n' + 1313 '<!--kg-card-end: html-->'; 1314 } 1315 1316 form.addEventListener('submit', function(e) { 1317 e.preventDefault(); 1318 errorEl.style.display = 'none'; 1319 successEl.style.display = 'none'; 1320 1321 // Collect all Q&A pairs 1322 var pairs = []; 1323 var qaPairs = qaList.querySelectorAll('.or-qa-pair'); 1324 var allFilled = true; 1325 qaPairs.forEach(function(pair) { 1326 var q = pair.querySelector('.or-q').value.trim(); 1327 var a = pair.querySelector('.or-a').value.trim(); 1328 if (q || a) { 1329 if (!q || !a) { allFilled = false; } 1330 else { pairs.push({ q: q, a: a }); } 1331 } 1332 }); 1333 1334 var d = { 1335 title: form.title.value.trim(), 1336 guest_name: form.guest_name.value.trim(), 1337 guest_title: form.guest_title.value.trim(), 1338 guest_company:form.guest_company.value.trim(), 1339 1340 intro: form.intro.value.trim(), 1341 quote: form.quote.value.trim(), 1342 photo: photoBase64, 1343 pairs: pairs 1344 }; 1345 1346 var missing = !d.title || !d.guest_name || !d.guest_title || !d.guest_company || 1347 !d.intro || !d.quote || pairs.length === 0 || !allFilled; 1348 if (missing) { 1349 errorEl.textContent = 'Please fill in all required fields. Every question must have an answer.'; 1350 errorEl.style.display = 'block'; 1351 return; 1352 } 1353 1354 submitBtn.disabled = true; 1355 submitBtn.textContent = 'Creating draftâ¦'; 1356 1357 ghostJWT(GHOST_ADMIN_KEY).then(function(token) { 1358 var imgPromise = photoFile 1359 ? uploadImageToGhost(photoFile, token).then(function(url) { d.photo = url; }) 1360 : Promise.resolve(); 1361 return imgPromise.then(function() { 1362 d.html = buildOrHtml(d); 1363 return fetch(GHOST_API_URL, { 1364 method: 'POST', 1365 headers: { 'Content-Type': 'application/json', 'Authorization': 'Ghost ' + token }, 1366 body: JSON.stringify({ 1367 posts: [{ 1368 title: d.title, 1369 html: d.html, 1370 status: 'draft', 1371 tags: [{ slug: 'inside-business' }] 1372 }] 1373 }) 1374 }); 1375 }); 1376 }).then(function(res) { 1377 if (!res.ok) return res.json().then(function(e) { throw new Error(JSON.stringify(e)); }); 1378 return res.json(); 1379 }).then(function() { 1380 successEl.style.display = 'block'; 1381 form.reset(); 1382 photoBase64 = ''; photoFile = null; 1383 photoPreview.style.display = 'none'; 1384 // Reset dynamic Q&A pairs back to 7 1385 var extra = qaList.querySelectorAll('.or-qa-pair'); 1386 extra.forEach(function(p, i) { if (i >= 7) p.remove(); }); 1387 qaPairCount = 7; 1388 window.scrollTo({ top: 0, behavior: 'smooth' }); 1389 }).catch(function(err) { 1390 console.error('Inside Business submit error:', err); 1391 errorEl.textContent = 'Something went wrong. Please try again in a moment.'; 1392 errorEl.style.display = 'block'; 1393 }).finally(function() { 1394 submitBtn.disabled = false;
1395 submitBtn.textContent = 'Create Draft â'; 1396 }); 1397 }); 1398})(); 1399 1400// HTML card swipe hint. 1401// CSS (main.css) makes .kg-html-card the scroll container (overflow-x:auto). 1402// body has no overflow-x so iOS Safari routes horizontal touch events here. 1403// This JS adds a "swipe â" visual hint on cards that actually overflow. 1404// Double-rAF waits for layout to fully settle before measuring. 1405(function() { 1406 function initCard(card) { 1407 requestAnimationFrame(function() { 1408 requestAnimationFrame(function() { 1409 if (card.scrollWidth <= card.clientWidth + 2) return; 1410 card.style.position = 'relative'; 1411 var hint = document.createElement('div'); 1412 hint.className = 'kg-html-card-hint'; 1413 hint.textContent = 'swipe â'; 1414 card.appendChild(hint); 1415 card.addEventListener('scroll', function() { 1416 hint.style.display = 'none'; 1417 }, { passive: true, once: true }); 1418 }); 1419 }); 1420 } 1421 document.querySelectorAll('.kg-html-card').forEach(initCard); 1422})(); 1423 1424// Tap-to-enlarge lightbox for article images. 1425// Any image inside .gh-content can be tapped/clicked to view full size. 1426// Excludes author avatars and emoji/icon images. 1427(function() { 1428 var content = document.querySelector('.gh-content'); 1429 if (!content) return; 1430 1431 // Build overlay 1432 var overlay = document.createElement('div'); 1433 overlay.id = 'img-lightbox'; 1434 var closeBtn = document.createElement('span'); 1435 closeBtn.id = 'img-lightbox-close'; 1436 closeBtn.textContent = 'â'; 1437 var bigImg = document.createElement('img'); 1438 overlay.appendChild(closeBtn); 1439 overlay.appendChild(bigImg); 1440 document.body.appendChild(overlay); 1441 1442 function open(src) { 1443 bigImg.src = src; 1444 overlay.classList.add('open'); 1445 document.body.style.overflow = 'hidden'; 1446 } 1447 function close() { 1448 overlay.classList.remove('open'); 1449 document.body.style.overflow = ''; 1450 } 1451 1452 overlay.addEventListener('click', close); 1453 bigImg.addEventListener('click', function(e) { e.stopPropagation(); }); 1454 closeBtn.addEventListener('click', close); 1455 document.addEventListener('keydown', function(e) { if (e.key === 'Escape') close(); }); 1456 1457 // Wire up all content images (exclude small avatars / icons) 1458 content.querySelectorAll('img').forEach(function(el) { 1459 if (el.naturalWidth && el.naturalWidth < 80) return; // skip tiny icons 1460 el.style.cursor = 'zoom-in'; 1461 el.addEventListener('click', function() { open(el.src); }); 1462 }); 1463})(); 1464 1465// Copy-intercept â appends source URL when a reader copies text from any 1466// article page. Shows a brief toast notice. Applies site-wide on post pages. 1467(function() { 1468 var body = document.querySelector('.post-body, .gh-content'); 1469 if (!body) return; 1470 1471 // Build a small toast notice element 1472 var notice = document.createElement('div'); 1473 notice.className = 'copy-notice'; 1474 notice.textContent = '© newsco.in â source link added to your clipboard'; 1475 document.body.appendChild(notice); 1476 1477 var hideTimer; 1478 function showNotice() { 1479 notice.classList.add('show'); 1480 clearTimeout(hideTimer); 1481 hideTimer = setTimeout(function() { notice.classList.remove('show'); }, 2800); 1482 } 1483 1484 document.addEventListener('copy', function(e) { 1485 var selection = window.getSelection(); 1486 if (!selection || selection.isCollapsed) return; 1487 var text = selection.toString(); 1488 if (!text || text.trim().length < 10) return; // ignore tiny selections 1489 1490 var url = window.location.href; 1491 var sourceNote = '\n\nSource: ' + url + '\n© newsco.in â All rights reserved.'; 1492 1493 try { 1494 e.clipboardData.setData('text/plain', text + sourceNote); 1495 e.clipboardData.setData('text/html', 1496 text + 1497 '<br><br><small>Source: <a href="' + url + '">' + url + '</a><br>' + 1498 '© newsco.in — All rights reserved.</small>' 1499 ); 1500 e.preventDefault(); 1501 showNotice(); 1502 } catch (err) { 1503 // If clipboard API fails, let the copy proceed without interception 1504 } 1505 }); 1506})(); 1507 1508// In-article ad slot â inserted ~60% through the article body. 1509// Runs on every post page, regardless of segment/tag (incl. Press Releases), 1510// since all posts share post.hbs. 1511(function() { 1512 const body = document.querySelector('.post-body'); 1513 if (!body) return; 1514 const children = Array.prototype.slice.call(body.children); 1515 if (children.length < 2) return; 1516 1517 const totalHeight = body.scrollHeight; 1518 const targetHeight = totalHeight * 0.6;
1519 let cumulative = 0; 1520 let insertBeforeEl = null; 1521 1522 for (let i = 0; i < children.length; i++) { 1523 cumulative += children[i].offsetHeight; 1524 if (cumulative >= targetHeight) { 1525 insertBeforeEl = children[i + 1] || null; 1526 break; 1527 } 1528 } 1529 1530 const ad = document.createElement('div'); 1531 ad.className = 'ad-zone ad-in-article'; 1532 ad.innerHTML = 1533 '<div class="ad-placeholder">' + 1534 '<span>ADVERTISEMENT</span>' + 1535 '<span>In-article Banner 728 x 90</span>' + 1536 '<span>60% scroll-depth slot</span>' + 1537 '</div>'; 1538 1539 if (insertBeforeEl) { 1540 body.insertBefore(ad, insertBeforeEl); 1541 } else { 1542 body.appendChild(ad); 1543 } 1544})();
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.