1let translations=window.messages||{}; 2// let lang; 3let langId; 4 5function setTextContentTranslationByElement(element, key, prefix, suffix){if (element) {const text = translations[key];if (text)element.textContent = (prefix || "") + text + (suffix || "");else console.log('Translation not found: ' + key);}} 6function setTextContentTranslationById(id, key, prefix){const element = document.getElementById(id);if (element === null) return;const text = translations[key];if (text !== undefined) {if (prefix)element.textContent = prefix + text;else element.textContent = text;} else {console.log('Translation not found: ' + key);}} 7function setPlaceholderTranslationById(id, key, suffix) {const element = document.getElementById(id);if (element === null) return;const text = translations[key];if (text !== undefined) {if (suffix)element.setAttribute('placeholder', text + suffix);else element.setAttribute('placeholder', text);} else {console.log('Translation not found: ' + key);}} 8function setTextContentTranslationByQuery(query, key, prefix) {const element = document.querySelector(query);if (element === null) return;const text = translations[key];if (text !== undefined){if (prefix)element.textContent = prefix + text;else element.textContent = text;} else {console.log('Translation not found: ' + key);}} 9 10var background1 = "#0d6625", background2 = "#4a6767", color1 = "#faf7f7", color2 = "#faf7f7"; 11// let languages = []; 12const currencyMap = { 'USD': '$', 'EUR': 'â¬', 'GBP': '£', 'TR': 'âº' }; 13const endpoint = window.location.href.split("/").slice(0, -4).join("/") + '/rest'; 14//const endpoint = "http://localhost:8080/com.westcoast.revenueflex.web/rest"; 15//const endpoint = 'https://revenueflex.com/rest'; 16const pageUrl = window.location.href; 17const page = pageUrl.split('?')[0].substring(pageUrl.lastIndexOf('/') + 1); 18 19function alog(strng) { 20 console.log("%cFCCMP.%c: " + strng, "background:" + background1 + ";color:" + color1 + ";border-radius:3px;font-weight:bold;padding-left:3px;padding-right:3px", 21 "background:" + background2 + ";color:" + color2 + ";font-weight:bold;"); 22} 23 24// Per-region site-data (device storage) consent config. 25// `legallyRequired` means the toggle is forced on and cannot be turned off â 26// GDPR / Quebec Law 25 / KVKK all legally mandate opt-in for device storage. 27const SITE_DATA_REGIONS = [ 28 { region: 'eu', legallyRequired: true }, 29 { region: 'ca', legallyRequired: true }, 30 { region: 'tr', legallyRequired: true }, 31 { region: 'br', legallyRequired: true }, 32 { region: 'za', legallyRequired: true }, 33 { region: 'jp', legallyRequired: true }, 34 { region: 'ch', legallyRequired: true }, 35 { region: 'au', legallyRequired: true }, 36 { region: 'kr', legallyRequired: true }, 37 { region: 'th', legallyRequired: true }, 38 { region: 'in', legallyRequired: true }, 39 { region: 'us', legallyRequired: false }, 40 { region: 'others', legallyRequired: false } 41]; 42 43function collectSiteDataConsent() { 44 const out = {}; 45 SITE_DATA_REGIONS.forEach(function(r) { 46 const enable = document.getElementById('site_data_enable_' + r.region); 47 if (!enable) return; 48 // Publishers may override legally-required regions â the checkbox value is 49 // honoured verbatim. The CMP displays a "legally required" badge as guidance, 50 // but the user has the final say. 51 // No `title` â the Essential group has a fixed, localized label. 52 out[r.region] = { enabled: enable.checked ? 1 : 0 }; 53 }); 54 const qc = quebecValueToSave('site_data_enable_qc'); 55 if (qc !== undefined) out.qc = { enabled: qc }; 56 return out; 57} 58 59function isAnySiteDataEnabled() { 60 syncQuebecRows(); 61 for (var i = 0; i < SITE_DATA_REGIONS.length; i++) { 62 var r = SITE_DATA_REGIONS[i]; 63 var el = document.getElementById('site_data_enable_' + r.region); 64 if (el && el.checked) return true; 65 } 66 var qc = document.getElementById('site_data_enable_qc'); 67 return !!(qc && qc.checked); 68} 69 70// J129: Quebec (Law 25) has its own rows, and they FOLLOW the Canada rows until the publisher changes one: while it 71// follows, the Quebec checkbox mirrors Canada's and a save leaves Quebec out, so the server keeps judging Quebec visitors 72// by the Canada row (Utils.regionFlag / siteDataRow), as for every project before Quebec was split out. Only a click on a 73// Quebec checkbox sets it apart (a programmatic change â Select all, Configure â never does). 74const QUEBEC_ROWS = [ 75 { qc: 'advertising_qc', ca: 'advertising_ca' }, 76 { qc: 'site_data_enable_qc', ca: 'site_data_enable_ca' } 77]; 78 79function quebecFollowsCanada(qcId) { 80 const el = document.getElementById(qcId); 81 return !el || el.dataset.followsCanada !== '0'; 82} 83 84function setQuebecFollows(qcId, follows) { 85 const el = document.getElementById(qcId); 86 if (!el) return; 87 el.dataset.followsCanada = follows ? '1' : '0'; 88 const hint = document.getElementById(qcId + '_follows'); 89 if (hint) hint.classList.toggle('d-none', !follows); 90} 91
92function syncQuebecRows() { 93 QUEBEC_ROWS.forEach(function(r) { 94 const qc = document.getElementById(r.qc), ca = document.getElementById(r.ca); 95 if (qc && ca && quebecFollowsCanada(r.qc)) qc.checked = ca.checked; 96 }); 97} 98 99// advertising = the loaded cmpAdvertisingSettings: a stored Quebec value sets the row apart, an absent one follows Canada. 100function loadQuebecRows(advertising) { 101 advertising = advertising || {}; 102 const siteDataQc = (advertising.siteDataConsent || {}).qc; 103 const stored = { 104 advertising_qc: Object.prototype.hasOwnProperty.call(advertising, 'qc') ? advertising.qc : undefined, 105 site_data_enable_qc: siteDataQc && Object.prototype.hasOwnProperty.call(siteDataQc, 'enabled') ? siteDataQc.enabled : undefined 106 }; 107 QUEBEC_ROWS.forEach(function(r) { 108 const el = document.getElementById(r.qc); 109 if (!el) return; 110 const v = stored[r.qc]; 111 setQuebecFollows(r.qc, v === undefined || v === null); 112 if (v !== undefined && v !== null) el.checked = !!v; 113 }); 114 syncQuebecRows(); 115} 116 117// 0/1 for a Quebec row set apart, undefined while it follows Canada (left out of the save). 118function quebecValueToSave(qcId) { 119 syncQuebecRows(); 120 const el = document.getElementById(qcId); 121 if (!el || quebecFollowsCanada(qcId)) return undefined; 122 return el.checked ? 1 : 0; 123} 124 125function addQuebecAdvertising(advertisingSettings) { 126 const qc = quebecValueToSave('advertising_qc'); 127 if (qc !== undefined) advertisingSettings.qc = qc; 128 return advertisingSettings; 129} 130 131document.addEventListener('change', function(e) { 132 const id = e.target && e.target.id; 133 if (!id) return; 134 QUEBEC_ROWS.forEach(function(r) { 135 if (id === r.qc && e.isTrusted) setQuebecFollows(r.qc, false); 136 else if (id === r.ca) syncQuebecRows(); 137 }); 138}); 139 140function loadSiteDataConsent(siteData) { 141 if (!siteData) siteData = {}; 142 SITE_DATA_REGIONS.forEach(function(r) { 143 const row = siteData[r.region] || {}; 144 const enable = document.getElementById('site_data_enable_' + r.region); 145 // Legally-required default = on; user may uncheck on subsequent visits. 146 // Only apply the default when the stored value is undefined. 147 if (enable) { 148 if ('enabled' in row) enable.checked = row.enabled !== 0; 149 else enable.checked = !!r.legallyRequired; 150 } 151 }); 152} 153 154// Canonical list of US-state-law GPP sections exposed in the Advertising tab. 155// Each entry: { key: JSON key under cmpSectionPreferences, prefix: DOM id prefix, withTargetedAds: whether the section shows a targeted-ads control }. 156// California doesn't ship a targeted-ads control (its CCPA/uscav1 behaviour bakes that into the single sale/share toggle), 157// so it's the only section with withTargetedAds=false. 158const US_SECTION_PREFS = [ 159 { key: 'usNat', prefix: 'us_nat_pref', withTargetedAds: true }, 160 { key: 'usCa', prefix: 'us_ca_pref', withTargetedAds: false }, 161 { key: 'usVa', prefix: 'us_va_pref', withTargetedAds: true }, 162 { key: 'usCo', prefix: 'us_co_pref', withTargetedAds: true }, 163 { key: 'usUt', prefix: 'us_ut_pref', withTargetedAds: true }, 164 { key: 'usCt', prefix: 'us_ct_pref', withTargetedAds: true }, 165 { key: 'usTx', prefix: 'us_tx_pref', withTargetedAds: true }, 166 { key: 'usOr', prefix: 'us_or_pref', withTargetedAds: true }, 167 { key: 'usMt', prefix: 'us_mt_pref', withTargetedAds: true }, 168 { key: 'usDe', prefix: 'us_de_pref', withTargetedAds: true }, 169 { key: 'usIa', prefix: 'us_ia_pref', withTargetedAds: true }, 170 { key: 'usNh', prefix: 'us_nh_pref', withTargetedAds: true }, 171 { key: 'usNj', prefix: 'us_nj_pref', withTargetedAds: true }, 172 { key: 'usTn', prefix: 'us_tn_pref', withTargetedAds: true }, 173 { key: 'usMn', prefix: 'us_mn_pref', withTargetedAds: true }, 174 { key: 'usNe', prefix: 'us_ne_pref', withTargetedAds: true }, 175 { key: 'usFl', prefix: 'us_fl_pref', withTargetedAds: true } 176]; 177 178// Build the full cmpSectionPreferences object from the form DOM. 179// Returns an object like { usNat: {...}, usCa: {...}, usVa: {...}, ... }. 180// Missing/hidden elements are silently skipped so legacy forms that don't render every state are safe. 181function collectUsSectionPreferences() { 182 const out = {}; 183 US_SECTION_PREFS.forEach(function(sec) { 184 const personal = document.getElementById(sec.prefix + '_personal_data'); 185 const sensitive = document.getElementById(sec.prefix + '_sensitive_data'); 186 if (!personal || !sensitive) return; 187 const obj = { 188 simplificationPersonalData: parseInt(personal.value), 189 sensitiveData: parseInt(sensitive.value) 190 }; 191 if (sec.withTargetedAds) { 192 const targeted = document.getElementById(sec.prefix + '_targeted_ads'); 193 if (targeted) obj.targetedAdvertising = parseInt(targeted.value); 194 } 195 out[sec.key] = obj; 196 }); 197 return out; 198} 199 200// Load values from a previously-stored cmpSectionPreferences object into the form. 201function loadUsSectionPreferences(sectionPrefs) { 202 if (!sectionPrefs) return; 203 US_SECTION_PREFS.forEach(function(sec) { 204 const prefs = sectionPrefs[sec.key]; 205 if (!prefs) return; 206 const personal = document.getElementById(sec.prefix + '_personal_data'); 207 const sensitive = document.getElementById(sec.prefix + '_sensitive_data'); 208 if (personal && 'simplificationPersonalData' in prefs) personal.value = prefs['simplificationPersonalData']; 209 if (sensitive && 'sensitiveData' in prefs) sensitive.value = prefs['sensitiveData']; 210 if (sec.withTargetedAds && 'targetedAdvertising' in prefs) { 211 const targeted = document.getElementById(sec.prefix + '_targeted_ads'); 212 if (targeted) targeted.value = prefs['targetedAdvertising']; 213 } 214 }); 215} 216 217// Canonical region codes â kept in sync with the server's 218// Utils.isSiteDataConsentLegallyRequired / getAdvertisingSettings region list. 219const ALL_REGION_CODES = ['eu','us','ca','tr','br','za','jp','ch','au','kr','th','in','others']; 220 221// Quebec (J129) is not in ALL_REGION_CODES: the defaults below leave it out so it follows Canada. The Configure modal 222// still moves its checkboxes with the group (a following one ends up equal to Canada's either way). 223const CFG_TCF_REGIONS = ALL_REGION_CODES.map(function(r){ return 'advertising_' + r; }).concat(['advertising_qc']); 224const CFG_REG_REGIONS = ['regulation_enable_us']; 225const CFG_GCM_REGIONS = ALL_REGION_CODES.map(function(r){ return 'site_data_enable_' + r; }).concat(['site_data_enable_qc']); 226 227// Shared payload builder: every TCF region and every site-data region set to
228// enabled. Used by the initial wizard and by the Configure modal's 229// "enable group" transition so the defaults stay in one place. 230function buildAllRegionsConsentDefaults() { 231 const out = { siteDataConsent: {} }; 232 ALL_REGION_CODES.forEach(function(r) { 233 out[r] = 1; 234 out.siteDataConsent[r] = { enabled: 1 }; 235 }); 236 return out; 237} 238const CFG_GCM_SUB_CHECKBOXES = ['gcm_url_passthrough','gcm_ads_data_redaction','gcm_google_signals','gcm_ad_personalization_signals','gcm_restricted_data_processing']; 239 240function anyChecked(ids) { 241 return ids.some(function(id){ const el = document.getElementById(id); return el && el.checked; }); 242} 243 244function setupConsentConfigureModal() { 245 const modal = document.getElementById('consentConfigureModal'); 246 if (!modal) return; 247 const cfgTcf = document.getElementById('cfg_enable_tcf'); 248 const cfgReg = document.getElementById('cfg_enable_regulations'); 249 const cfgGcm = document.getElementById('cfg_enable_gcm'); 250 251 // Sections stay visible at all times â the Configure modal only toggles the 252 // underlying per-region checkboxes. Hiding sections confused publishers who 253 // wanted to see "what am I about to save" after clicking Apply. 254 255 let snapshot = null; 256 257 function takeSnapshot() { 258 const s = { checkboxes: {}, values: {} }; 259 CFG_TCF_REGIONS.concat(CFG_REG_REGIONS, CFG_GCM_REGIONS, CFG_GCM_SUB_CHECKBOXES) 260 .forEach(function(id){ const el = document.getElementById(id); if (el) s.checkboxes[id] = el.checked; }); 261 const valIds = ['gcm_send_page_view','gcm_wait_for_update']; 262 US_SECTION_PREFS.forEach(function(sec){ 263 valIds.push(sec.prefix + '_personal_data'); 264 valIds.push(sec.prefix + '_sensitive_data'); 265 if (sec.withTargetedAds) valIds.push(sec.prefix + '_targeted_ads'); 266 }); 267 valIds.forEach(function(id){ const el = document.getElementById(id); if (el) s.values[id] = el.value; }); 268 return s; 269 } 270 271 function restoreSnapshot(s) { 272 if (!s) return; 273 Object.keys(s.checkboxes).forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = s.checkboxes[id]; }); 274 Object.keys(s.values).forEach(function(id){ const el = document.getElementById(id); if (el) el.value = s.values[id]; }); 275 } 276 277 modal.addEventListener('show.bs.modal', function(){ 278 syncQuebecRows(); 279 snapshot = takeSnapshot(); 280 // Master = ON only when every region in the group is checked. Pre-checked 281 // legally-required regions (EU/CA/TR for site data) mean the group is 282 // partial by default â that reads as "off" in the master so clicking it 283 // cleanly cascades to "enable everyone" on Apply. 284 cfgTcf.checked = CFG_TCF_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; }); 285 cfgReg.checked = CFG_REG_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; }); 286 cfgGcm.checked = CFG_GCM_REGIONS.every(function(id){ const el = document.getElementById(id); return el && el.checked; }); 287 }); 288 289 modal.addEventListener('hidden.bs.modal', function(){ 290 if (snapshot) { 291 restoreSnapshot(snapshot); 292 snapshot = null; 293 } 294 }); 295 296 const clearAllBtn = document.getElementById('consent_configure_clear_all'); 297 if (clearAllBtn) clearAllBtn.addEventListener('click', function(){ 298 cfgTcf.checked = false; cfgReg.checked = false; cfgGcm.checked = false; 299 CFG_TCF_REGIONS.concat(CFG_REG_REGIONS, CFG_GCM_REGIONS).forEach(function(id){ 300 const el = document.getElementById(id); if (el) el.checked = false; 301 }); 302 CFG_GCM_SUB_CHECKBOXES.forEach(function(id){ 303 const el = document.getElementById(id); if (el) el.checked = true; 304 }); 305 const spv = document.getElementById('gcm_send_page_view'); if (spv) spv.value = 'true'; 306 const wfu = document.getElementById('gcm_wait_for_update'); if (wfu) wfu.value = '1000'; 307 US_SECTION_PREFS.forEach(function(sec){ 308 const p = document.getElementById(sec.prefix + '_personal_data'); if (p) p.value = '0'; 309 const s = document.getElementById(sec.prefix + '_sensitive_data'); if (s) s.value = '0'; 310 if (sec.withTargetedAds) { 311 const t = document.getElementById(sec.prefix + '_targeted_ads'); if (t) t.value = '1'; 312 } 313 }); 314 }); 315 316 const applyBtn = document.getElementById('consent_configure_apply');
317 if (applyBtn) applyBtn.addEventListener('click', function(){ 318 // Master semantics on Apply: 319 // master ON + subs not all on â enable every region in the group 320 // master OFF + any sub on â disable every region in the group 321 // otherwise â no change (already matches intent) 322 // This lets a partially-populated group (e.g. EU/CA/TR pre-checked for 323 // site data) still respond when the user clicks the master â previously 324 // the handler bailed because "at least one" was already checked. 325 function applyMasterToGroup(master, ids, onFreshEnable) { 326 const allChecked = ids.every(function(id){ const el = document.getElementById(id); return el && el.checked; }); 327 const anyChecked = ids.some(function(id){ const el = document.getElementById(id); return el && el.checked; }); 328 if (master.checked && !allChecked) { 329 ids.forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = true; }); 330 if (onFreshEnable && !anyChecked) onFreshEnable(); 331 } else if (!master.checked && anyChecked) { 332 ids.forEach(function(id){ const el = document.getElementById(id); if (el) el.checked = false; }); 333 } 334 } 335 applyMasterToGroup(cfgTcf, CFG_TCF_REGIONS); 336 applyMasterToGroup(cfgReg, CFG_REG_REGIONS, function(){ 337 // Fresh enable: seed every US state/national section with display-friendly 338 // combo values so the visitor actually sees opt-out rows. Publisher can 339 // refine per-state afterwards in the US Section Preferences panel. 340 US_SECTION_PREFS.forEach(function(sec){ 341 const p = document.getElementById(sec.prefix + '_personal_data'); if (p) p.value = '0'; 342 const s = document.getElementById(sec.prefix + '_sensitive_data'); if (s) s.value = '1'; 343 if (sec.withTargetedAds) { 344 const t = document.getElementById(sec.prefix + '_targeted_ads'); if (t) t.value = '1'; 345 } 346 }); 347 }); 348 applyMasterToGroup(cfgGcm, CFG_GCM_REGIONS); 349 350 snapshot = null; 351 const instance = bootstrap.Modal.getInstance(modal) || new bootstrap.Modal(modal); 352 instance.hide(); 353 354 // Persist immediately â Apply is the commit point for the whole 355 // Consent & Privacy configuration. Triggering the advertising form's 356 // submit handler reuses the existing save pipeline (collectSiteDataConsent, 357 // section preferences, etc.) so publishers don't have to find the 358 // page-level Save button afterwards. requestSubmit() is preferred 359 // (runs native validation first); fall back to a dispatched submit 360 // event on older browsers. 361 var advertisingFormEl = document.getElementById('advertising_settings_form'); 362 if (advertisingFormEl) { 363 if (typeof advertisingFormEl.requestSubmit === 'function') { 364 advertisingFormEl.requestSubmit(); 365 } else { 366 advertisingFormEl.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 367 } 368 } 369 }); 370} 371 372async function onLoad(){ 373 374 //removePlaceholders(); 375 console.log('document load handler'); 376 377 // const pageUrl = window.location.href; 378 // const page = pageUrl.split('?')[0].substring(pageUrl.lastIndexOf('/') + 1); 379 const page = new URL(window.location.href).pathname.split('/').pop(); 380 381 addLogoutHandler(); 382 // addSubscriptionRequiredMessage(); 383 // loadTemplates(); 384 // loadTemplateSettings(); 385 //setCancelBtnClicking(page); 386 displayPanelLanguageSelector(); 387 let alertDisplaying = true; 388 389 switch (page) { 390 case 'websites.jsp': 391 alog("Jump to websites 1"); 392 await loadWebsites(); 393 setupDeletedDomainsToggle(); 394 break; 395 case 'admin_users.jsp': 396 loadAdminUsers(); 397 break; 398 case 'website_settings.jsp': 399 alog("Jump to website settings 2"); 400 await loadWebsiteSettings(); 401 break; 402 case 'user_settings.jsp': 403 alog("Jump to user settings 3"); 404 await loadUserSettings(document.getElementById('user_settings_form')); 405 document.getElementById('formAccountSettings').addEventListener('submit', updatePassword); 406 break; 407 case 'dashboard.jsp': 408 alog("Jump to dashboard 4"); 409 await loadDashboard(); 410 break; 411 case 'consent_logs.jsp': 412 alog("Jump to consent log"); 413 await loadConsentLogs(); 414 break; 415 case 'subscriptions.jsp': 416 alog("Jump to subscriptions 5"); 417 await loadSubscriptions(); 418 break; 419 case 'payment.jsp': 420 alog("Jump to payment 6"); 421 loadPayment(); 422 break; 423 case 'payment_result.jsp': 424 alog("Jump to payment result 7"); 425 loadPaymentResult(); 426 break; 427 case 'payments.jsp': 428 alog("Jump to payments 8"); 429 await loadPayments(); 430 break; 431 case 'contact_us.jsp': 432 alog("Jump to contact form 9"); 433 loadContactUs(); 434 break; 435 case 'reseller.jsp': 436 alog("Jump to resellers 10"); 437 loadResellers(); 438 break; 439 case 'reseller_user.jsp': 440 alog("Jump to reseller user 11"); 441 loadResellerUser(); 442 break; 443 case 'templates.jsp': 444 alog("Jump to templates 12"); 445 await loadTemplates(); 446 break; 447 case 'template_settings.jsp': 448 alog("Jump to template settings 13"); 449 await loadTemplateSettings(); 450 // after load so the masters reflect the persisted checkbox states 451 setupSelectAllToggles(); 452 break; 453 case 'initial_wizard.jsp': 454 alog("Jump to initial wizard 14"); 455 await loadInitialWizard(); 456 alertDisplaying = false;
457 break; 458 case 'document_generator.jsp': 459 alog("Jump to document generator 15"); 460 await loadDocumentGenerator(); 461 break; 462 case 'cookie_manager.jsp': 463 alog("Jump to cookie manager"); 464 await loadCookieManager(); 465 break; 466 default: 467 alog("Unidentified page"); 468 return; 469 } 470 471 setTooltips(); 472 if (alertDisplaying) 473 checkAlertIsRequired(); 474 475} 476 477if (document.readyState === "complete")onLoad(); 478else document.addEventListener('DOMContentLoaded', onLoad ); 479 480//function removePlaceholders(){ 481// return; 482// var inputs = document.querySelectorAll('span[data-placeholder-1]'); 483// 484// if (!inputs) 485// return; 486// 487// inputs.forEach(iii => { 488// iii.setAttribute('data-placeholder-1',''); 489// 490// /* 491// var input=iii; 492// if (!input.placeholder) 493// input.placeholder = ''; 494// 495// // it will reset placeholder value on blur by dataset variable 496// input.addEventListener('blur', (e) => { 497// if (input.dataset.placeholder && input.dataset.placeholder.length>0 && input.placeholder === "" && (!input.value || input.value.length == 0 )) 498// input.placeholder = input.dataset.placeholder; 499// }); 500// 501// // it will remove placeholder value on focus and store with dataset variable 502// input.addEventListener('focus', e => { 503// if (input.placeholder.length>0){ 504// input.dataset.placeholder = input.placeholder; 505// input.placeholder = ''; 506// } 507// }); 508// 509// function ef(input){ 510// if ( !input.value || input.value.length == 0 ){ 511// if (input.placeholder.length>0){ 512// input.dataset.placeholder = input.placeholder; 513// input.placeholder = ''; 514// } 515// } 516// else { 517// console.log('4 ' + input.value); 518// if (input.placeholder.length>0){ 519// console.log('5 ' + input.value); 520// input.dataset.placeholder = input.placeholder; 521// input.placeholder = ''; 522// } 523// } 524// } 525// 526// input.addEventListener('change', (e) => { 527// //if (document.activeElement == input) 528// // return; 529// ef(input); 530// }); 531// 532// ef(input); 533// */ 534// }); 535// 536// //setTimeout(removePlaceholders,1000); 537//} 538 539function checkIfLoginRequired() { 540 if (getSid() == null && !document.getElementById("login_form")) { 541 alog("Redirect to login"); 542 window.location.href = 'login.jsp?redirect=' + encodeURIComponent(window.location.href); 543 return 1; 544 } 545 return 0; 546} 547 548function displayPanelLanguageSelector() { 549 let navbarLanguageSelector = document.getElementById('navbar_language_selector'); 550 if (!navbarLanguageSelector) { 551 const languageSelectorContainer = document.createElement('div'); 552 languageSelectorContainer.style.position = "absolute"; 553 languageSelectorContainer.style.top = "10px"; 554 languageSelectorContainer.style.right = "10px"; 555 languageSelectorContainer.style.padding = "10px"; 556 languageSelectorContainer.style.borderRadius = "6px"; 557 558 const body = document.querySelector('body'); 559 body.insertBefore(languageSelectorContainer, body.firstChild); 560 561 navbarLanguageSelector = document.createElement('div'); 562 navbarLanguageSelector.classList.add('nav-item', 'dropdown', 'me-3', 'd-none'); 563 navbarLanguageSelector.id = 'navbar_language_selector'; 564 languageSelectorContainer.appendChild(navbarLanguageSelector); 565 566 const btn = document.createElement('button'); 567 btn.classList.add('navbar-language-selector-btn', 'btn', 'nav-link', 'dropdown-toggle'); 568 btn.type = 'button'; 569 btn.setAttribute('data-bs-toggle', 'dropdown'); 570 btn.setAttribute('aria-expanded', 'false'); 571 navbarLanguageSelector.appendChild(btn); 572 } 573 574 // const params = new URLSearchParams(window.location.search); 575 // let lang = params.get('lang') || getCookie('lang') || "en"; 576 let selectedLanguage = languages.find(l => l.bundle === lang); 577 if (!selectedLanguage) { 578 if (lang.includes('_')) 579 lang = lang.split('_')[0]; 580 else if (lang.includes('-')) 581 lang = lang.split('-')[0]; 582 583 selectedLanguage = languages.find(l => l.bundle.split('_')[0] === lang); 584 } 585 586 if (!selectedLanguage) { 587 console.error("Language not found: " + lang); 588 return; 589 } 590 591 langId = selectedLanguage.id; 592 593 const btn = navbarLanguageSelector.querySelector('.navbar-language-selector-btn'); 594 btn.textContent = "ð " + selectedLanguage.localName; 595 596 const dropdown = document.createElement('ul'); 597 dropdown.classList.add('dropdown-menu', 'dropdown-menu-end', 'dropdown-menu-scrollable'); 598 navbarLanguageSelector.appendChild(dropdown); 599 navbarLanguageSelector.classList.remove('d-none'); 600 601 const langCookie = getCookie('lang'); 602 if (langCookie == null || langCookie !== lang) 603 createCookie('lang', lang); 604
605 languages.forEach(option => { 606 const li = document.createElement('li'); 607 const link = document.createElement('a'); 608 link.classList.add('dropdown-item'); 609 link.href = '#'; 610 link.addEventListener('click', function(ev) { 611 ev.preventDefault(); 612 const url = new URL(window.location.href); 613 url.searchParams.set('lang', option.bundle); 614 window.location.href = url.toString(); 615 }); 616 // //<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/6.7.0/css/flag-icons.min.css"> 617 // const flag = document.createElement('span'); 618 // flag.classList.add('fi', 'fi-' + option.bundle); 619 // flag.setAttribute('aria-hidden', 'true'); 620 // link.appendChild(flag); 621 // const span = document.createElement('span'); 622 // span.textContent = option.name; 623 // span.classList.add('ms-2'); 624 // link.appendChild(span); 625 link.textContent = option.name; 626 627 if (option.bundle === selectedLanguage.bundle) 628 link.classList.add('fw-bold'); 629 630 li.appendChild(link); 631 dropdown.appendChild(li); 632 }); 633} 634 635function addLogoutHandler(){ 636 const logoutlink = document.getElementById('logout_btn'); 637 if (logoutlink) 638 logoutlink.addEventListener('click', event => { 639 event.preventDefault(); 640 logout(); 641 }); 642} 643 644function logout() { 645 //if (event) 646 // event.preventDefault(); 647 648 const sid = getSid(); 649 // console.dir("sid: " + sid); 650 //if (sid == null) 651 // return; 652 653 fetch(endpoint + "/logout/" + sid).then(async response => { 654 if (response.ok) { 655 const responseBody = await response.json(); 656 if (responseBody.errorCode === 0) { 657 // console.log("Logout successful"); 658 deleteCookie('sid'); 659 deleteCookie('name'); 660 deleteCookie('email'); 661 deleteCookie('domainCounts'); 662 clearLocalStorage(); 663 alog("Jump to login 10"); 664 window.location.href = '../views/login.jsp'; 665 } else { 666 console.log('Logout Error Code: ' + responseBody.errorCode); 667 } 668 } else { 669 console.error('Logout failed: ' + JSON.stringify(response)); 670 } 671 }).catch(err => { 672 console.log(err); 673 }); 674 675 return false; 676} 677 678function checkAlertIsRequired() { 679 if (trialRemainDayCount > 0 && trialRemainDayCount <= 7) 680 displayAlert((translations['trialEndMsg'] || 'Your trial ends in [DayCount] days. Consider upgrading your subscription.').replace('[DayCount]', trialRemainDayCount), 'warning'); 681 682 if (!isReseller && usedDomainCount > allowedDomainCount) { 683 const msg = (translations['usingDomainCountExceedMsg'] || 'You have added [usedDomainCount] domains but your plan supports [allowedDomainCount].') 684 .replace('[usedDomainCount]', usedDomainCount).replace('[allowedDomainCount]', allowedDomainCount); 685 // Extra domains are allowed; the user can also drop back under the plan limit 686 // by rejecting the ones they don't need on the domain management page. 687 const manageLinkLabel = translations['manageDomainsLink'] || 'Manage your domains'; 688 const manageLink = '<a href="websites.jsp" class="alert-link">' + manageLinkLabel + '</a>'; 689 const hint = (translations['manageDomainsHint'] || "Don't need them all? You can [manageDomainsLink] to reject any extra domains.") 690 .replace('[manageDomainsLink]', manageLink); 691 // sticky: stays until the user closes it (plan-limit info must not vanish on its own) 692 displayAlert(msg + '<br>' + hint, 'danger', true); 693 } 694 695 // if (exceededPageViewLimit) { 696 // displayAlert(translations['pageviewLimitMsg']); 697 // } 698} 699 700/* TEMPLATES */ 701async function loadTemplates() { 702 const projectsTable = document.getElementById('templates_table'); 703 if (!projectsTable || checkIfLoginRequired()) 704 return; 705 706 if (isReseller) 707 displayUsedDomainCount(); 708 709 const data = { sid: getSid(), projectTypeId: 2 }; 710 const response = await postRequest(endpoint + '/project/list', data); 711 if (response.ok) { 712 const responseBody = await response.json(); 713 if (responseBody.statusCode === 0) { 714 const projects = responseBody.contents; 715 if (projects && !projects.length) 716 return showTemplateCreateLink('templates_table_container', !0); 717 718 document.getElementById('templates_table').style.display = ''; 719 720 const tableBody = projectsTable.querySelector('tbody');
721 projects.forEach(project => { 722 // if (project.projectTypeId === 2) { 723 // const data = { sid: getSid(), project: project.id }; 724 // const projectResponse = await postRequest(endpoint + '/project/get', data); 725 // const projectResponseBody = await projectResponse.json(); 726 // debugger; 727 728 const row = document.createElement('tr'); 729 // row.classList.add('gy-3') 730 731 var nameCell = document.createElement('td'); 732 nameCell.textContent = project.projectName; 733 row.appendChild(nameCell); 734 735 // const nameCell = document.createElement('td'); 736 // const nameLink = document.createElement('a'); 737 // nameLink.textContent = project.projectName; 738 // nameLink.href = "template_settings.jsp?id=" + project.id; 739 // nameLink.classList.add("name-link", "align-middle"); 740 // nameCell.appendChild(nameLink); 741 // row.appendChild(nameCell); 742 743 const websitesCell = document.createElement('td'); 744 // websitesCell.textContent = project.domains.map(function (d) { 745 // return d.domainName; 746 // }).join(', '); 747 748 for (let i = 0; i < project.domains.length; i++) { 749 const websiteLink = document.createElement('a'); 750 const domain = project.domains[i]; 751 websiteLink.textContent = domain.domainName; 752 websiteLink.href = "website_settings.jsp?id=" + domain.id + "&pp=t"; 753 websiteLink.classList.add("name-link", "align-middle"); 754 websitesCell.appendChild(websiteLink); 755 756 if (i < project.domains.length - 1) { 757 const comma = document.createElement('span'); 758 comma.textContent = ', '; 759 websitesCell.appendChild(comma); 760 } 761 } 762 row.appendChild(websitesCell); 763 764 const enabledCell = document.createElement('td'); 765 enabledCell.classList.add('text-center'); 766 const enabledIcon = document.createElement('i'); 767 enabledIcon.classList.add('fa', project.cmpEnabled ? 'fa-check' : 'fa-times'); 768 // enabledIcon.classList.add(project.enabled ? 'true' : 'false'); 769 enabledIcon.classList.add(project.cmpEnabled ? 'text-success' : 'text-danger'); 770 enabledCell.appendChild(enabledIcon); 771 // enabledCell.classList.add("align-middle"); 772 row.appendChild(enabledCell); 773 774 const actionCell = document.createElement('td'); 775 actionCell.classList.add('text-center'); 776 777 const settingsButton = document.createElement('button'); 778 settingsButton.textContent = translations['settings']; 779 settingsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm'); 780 settingsButton.addEventListener('click', () => { 781 alog("Jump to project settings " + project.id + " 11"); 782 window.location.href = "template_settings.jsp?id=" + project.id; 783 }); 784 actionCell.appendChild(settingsButton); 785 786 const scriptButton = document.createElement('button'); 787 setTextContentTranslationByElement(scriptButton, 'pageCode'); 788 scriptButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 789 scriptButton.setAttribute('data-bs-toggle', 'modal'); 790 scriptButton.setAttribute('data-bs-target', '#scriptModal'); 791 actionCell.appendChild(scriptButton); 792 scriptButton.addEventListener('click', async () => { 793 const contentElement = document.getElementById('script_content_area'); 794 const copyButton = document.getElementById('copy_btn'); 795 const copyInfo = document.getElementById('copy-info'); 796 contentElement.style.display="none"; 797 contentElement.textContent = ""; 798 copyButton.style.display="none"; 799 copyInfo.innerHTML = ""; 800 if (project.domains.length){ 801 copyInfo.innerHTML = "<div class='loader'></div>"; 802 const data = { sid: getSid(), projectId: project.id }; 803 const response = await postRequest(endpoint + '/scriptcode', data); 804 if (response && response.ok) { 805 const responseBody = await response.json(); 806 if (responseBody.statusCode === 0) { 807 contentElement.textContent = responseBody.content; 808 contentElement.style.display=""; 809 copyButton.style.display=""; 810 copyInfo.innerHTML = translations["copyScriptLabel"]; 811 812 } else { 813 console.log('Script Error Code: ' + responseBody.statusCode); 814 copyInfo.innerHTML = translations["serverError"]; 815 } 816 }else { 817 console.log('Script Error Code: ' + responseBody.statusCode); 818 copyInfo.innerHTML = translations["serverError"]; 819 } 820 } 821 else 822 copyInfo.innerHTML = translations["noWebsitesForTemplate"]; 823 }); 824 825 const statButton = document.createElement('button'); 826 statButton.textContent = translations['stats']; 827 statButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 828 actionCell.appendChild(statButton); 829 statButton.addEventListener('click', async () => { 830 alog("Jump to dashboard " + project.id + " 12"); 831 window.location.href = "dashboard.jsp?id=" + project.id; 832 }); 833 834 /*const cmButton = document.createElement('button'); 835 setTextContentTranslationByElement(cmButton, 'cookieDisplayCode'); 836 cmButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 837 cmButton.setAttribute('data-bs-toggle', 'modal');
838 cmButton.setAttribute('data-bs-target', '#scriptModal'); 839 actionCell.appendChild(cmButton); 840 cmButton.addEventListener('click', async () => { 841 const contentElement = document.getElementById('script_content_area'); 842 const copyButton = document.getElementById('copy_btn'); 843 const copyInfo = document.getElementById('copy-info'); 844 contentElement.style.display="none"; 845 contentElement.textContent = ""; 846 copyButton.style.display="none"; 847 copyInfo.innerHTML = ""; 848 if (project.domains.length){ 849 copyInfo.innerHTML = "<div class='loader'></div>"; 850 const data = { sid: getSid(), projectId: project.id }; 851 const response = await postRequest(endpoint + '/scriptcode', data); 852 if (response && response.ok) { 853 const responseBody = await response.json(); 854 if (responseBody.statusCode === 0) { 855 contentElement.textContent = responseBody.cookiesScriptCode; 856 contentElement.style.display=""; 857 copyButton.style.display=""; 858 copyInfo.innerHTML = translations["copyScriptLabel"]; 859 860 } else { 861 console.log('Script Error Code: ' + responseBody.statusCode); 862 copyInfo.innerHTML = translations["serverError"]; 863 } 864 }else { 865 console.log('Script Error Code: ' + responseBody.statusCode); 866 copyInfo.innerHTML = translations["serverError"]; 867 } 868 } 869 else 870 copyInfo.innerHTML = translations["noWebsitesForTemplate"]; 871 });*/ 872 873 if (responseBody.ul1) { 874 const deleteButton = document.createElement('button'); 875 deleteButton.textContent = translations['delete'] || 'Delete'; 876 deleteButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 877 actionCell.appendChild(deleteButton); 878 deleteButton.addEventListener('click', async () => { 879 const data2 = { sid: getSid(), projectTypeId: 2, projectId: project.id }; 880 const response2 = await postRequest(endpoint + '/project/delete', data2); 881 if (response2.ok) { 882 const responseBody2 = await response2.json(); 883 if (responseBody2.statusCode === 0) 884 tableBody.deleteRow(row.rowIndex - 1); 885 } 886 }); 887 } 888 889 //const paymentsButton = document.createElement('button'); 890 //paymentsButton.textContent = 'Payments'; 891 //paymentsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 892 //actionCell.appendChild(paymentsButton); 893 //paymentsButton.addEventListener('click', async () => { 894 // alog("Jump to payments " + project.id + " 13"); 895 // window.location.href = "payments.jsp?id=" + project.id; 896 //}); 897 898 row.appendChild(actionCell); 899 900 tableBody.appendChild(row); 901 // } 902 }); 903 } else if (responseBody.statusCode === 3) { 904 deleteCookie('sid'); 905 deleteCookie('name'); 906 deleteCookie('email'); 907 clearLocalStorage(); 908 alog("Jump to login 14"); 909 window.location.href = '../views/login.jsp'; 910 } 911 912 } // else { 913 // showAlert("The page could not be loaded!", "error"); 914 // } 915 916 createCopyToClipboard('copy_btn', 'script_content_area'); 917} 918 919function createCopyToClipboard(btnid, contentid, displayinfo, tooltip) { 920 const copyBtn = document.getElementById(btnid); 921 // let tooltip = displayinfo ? null : new bootstrap.Tooltip(copyBtn); 922 copyBtn.addEventListener('click', function() { 923 const contentElement = document.getElementById(contentid); 924 if (contentElement) { 925 if (displayinfo) { 926 navigator.clipboard.writeText(contentElement.textContent).then(() => { 927 console.log("Copied the script to clipboard!"); 928 }); 929 var di = document.getElementById(displayinfo); 930 if (di) { 931 di.innerHTML = translations["copyCompleted"]; 932 } 933 } else if (tooltip) { 934 const btnText = copyBtn.getAttribute('data-bs-original-title'); 935 navigator.clipboard.writeText(contentElement.textContent).then(() => { 936 tooltip.dispose(); 937 copyBtn.setAttribute('title', "â " + (translations['copied'] || "Copied")); 938 tooltip = new bootstrap.Tooltip(copyBtn); 939 tooltip.show(); 940 941 setTimeout(() => { 942 tooltip.dispose(); 943 copyBtn.setAttribute('title', btnText); 944 tooltip = new bootstrap.Tooltip(copyBtn); 945 }, 2000); 946 }); 947 } else { 948 const btnText = copyBtn.textContent; 949 navigator.clipboard.writeText(contentElement.textContent).then(() => { 950 copyBtn.textContent = "â " + (translations['copied'] || "Copied"); 951 952 setTimeout(() => { 953 copyBtn.textContent = btnText; 954 }, 2000); 955 }); 956 } 957 958 // contentElement.focus(); 959 // contentElement.setSelectionRange(0, 99999); 960 // setTimeout(function() { 961 // contentElement.setSelectionRange(0, 0); 962 // }, 200); 963 } 964 }); 965 966 // if (!displayinfo) { 967 // const tooltip = new bootstrap.Tooltip(copyBtn); 968 // 969 // copyBtn.addEventListener('click', () => { 970 // // Metni kopyala 971 // const copyText = document.getElementById('copyText'); 972 // navigator.clipboard.writeText(copyText.value).then(() => { 973 // // Tooltip mesajını deÄiÅtir 974 // copyBtn.setAttribute('data-bs-original-title', 'Copied'); 975 // tooltip.show(); 976 // 977 // // Bir süre sonra tekrar "Copy" mesajına dön 978 // setTimeout(() => { 979 // copyBtn.setAttribute('data-bs-original-title', 'Copy'); 980 // }, 1500); 981 // }); 982 // }); 983 // } 984} 985 986function addLanguageSelector(primaryLanguage, availableLanguages) { 987 const langselector = document.getElementById('select_language');
988 availableLanguages.forEach(option1 => { 989 const optionx = document.createElement('option'); 990 optionx.value = option1.shortCode; 991 optionx.text = option1.name; 992 //console.dir(primaryLanguage + " " + option1.shortCode); 993 if (primaryLanguage == option1.shortCode) { 994 optionx.selected = "selected"; 995 optionx.checked = true; 996 langselector.value = optionx.shortCode; 997 } 998 langselector.appendChild(optionx); 999 }); 1000} 1001 1002 1003//////////////////////////////////////////////////////////////////////////////////////// 1004//////////////////////////////////////////////////////////////////////////////////////// 1005//////////////////////////////////////////////////////////////////////////////////////// 1006//////////////////////////////////////////////////////////////////////////////////////// 1007//////////////////////////////////////////////////////////////////////////////////////// 1008//////////////////////////////////////////////////////////////////////////////////////// 1009//////////////////////////////////////////////////////////////////////////////////////// 1010//////////////////////////////////////////////////////////////////////////////////////// 1011//////////////////////////////////////////////////////////////////////////////////////// 1012//////////////////////////////////////////////////////////////////////////////////////// 1013//////////////////////////////////////////////////////////////////////////////////////// 1014//////////////////////////////////////////////////////////////////////////////////////// 1015//////////////////////////////////////////////////////////////////////////////////////// 1016//////////////////////////////////////////////////////////////////////////////////////// 1017//////////////////////////////////////////////////////////////////////////////////////// 1018//////////////////////////////////////////////////////////////////////////////////////// 1019//////////////////////////////////////////////////////////////////////////////////////// 1020//////////////////////////////////////////////////////////////////////////////////////// 1021 1022 1023 1024/* TEMPLATE SETTINGS */ 1025 1026async function loadTemplateSettings() { 1027 const projectSettingsForm = document.getElementById('project_settings_form'); 1028 if (!projectSettingsForm || checkIfLoginRequired()) 1029 return; 1030 1031 const advertisingForm = document.getElementById('advertising_settings_form'); 1032 const layoutForm = document.getElementById('layout_properties_form'); 1033 const allForms = [projectSettingsForm, advertisingForm, layoutForm]; 1034 const creatingForms = [projectSettingsForm, advertisingForm]; 1035 1036 const params = new URLSearchParams(window.location.search); 1037 const projectId = params.get("id"); 1038 var projectAccessKey = ''; 1039 var serverAvailableLanguages = null; 1040 1041 // Activate tab from URL hash (e.g. #advertising, #layout, #geopreview) or ?tab= param 1042 var initialTab = window.location.hash ? window.location.hash.substring(1) : params.get("tab"); 1043 if (initialTab) { 1044 var tabTriggerEl = document.querySelector('#settings_tabs button[data-bs-target="#' + initialTab + '"]'); 1045 if (tabTriggerEl) { 1046 var tab = new bootstrap.Tab(tabTriggerEl); 1047 tab.show(); 1048 } 1049 } 1050 1051 // Update URL hash when tabs change 1052 document.querySelectorAll('#settings_tabs button[data-bs-toggle="tab"]').forEach(function(btn) { 1053 btn.addEventListener('shown.bs.tab', function() { 1054 var target = btn.getAttribute('data-bs-target'); 1055 if (target) history.replaceState(null, '', target); 1056 }); 1057 }); 1058 1059 const enabledCheckbox = document.getElementById('consent_enabled'); 1060 enabledCheckbox.addEventListener('change', function() { 1061 const statusElement = document.getElementById('status_text'); 1062 if (enabledCheckbox.checked) { 1063 statusElement.classList.remove('text-danger'); 1064 statusElement.classList.add('text-success'); 1065 setTextContentTranslationByElement(statusElement, 'enabled'); 1066 } else { 1067 statusElement.classList.remove('text-success'); 1068 statusElement.classList.add('text-danger'); 1069 setTextContentTranslationByElement(statusElement, 'projectDisabledMessage'); 1070 } 1071 }); 1072 1073 1074 const usCheckbox = document.getElementById('advertising_us'); 1075 1076 1077 const layoutPropertiesEnabled = document.getElementById('layout_properties_enabled'); 1078 layoutPropertiesEnabled?.addEventListener('change', function () { 1079 const expandableContainer = document.getElementById('layout_properties_container'); 1080 // expandableContainer.classList.toggle('expanded'); 1081 for (const layoutPropertyContainer of expandableContainer.children) { 1082 for (const layoutPropertyElement of layoutPropertyContainer.children) { 1083 layoutPropertyElement.disabled = !layoutPropertiesEnabled.checked; 1084 // layoutPropertyElement.classList.toggle("text-muted"); 1085 if (layoutPropertiesEnabled.checked) 1086 layoutPropertyElement.classList.remove("text-muted"); 1087 else 1088 layoutPropertyElement.classList.add("text-muted"); 1089 } 1090 } 1091 }); 1092 1093 const statusSection = document.getElementById('status_section'); 1094 const gcmUrlPassthrough = document.getElementById('gcm_url_passthrough'); 1095 const gcmAdsDataRedaction = document.getElementById('gcm_ads_data_redaction'); 1096 const gcmGoogleSignals = document.getElementById('gcm_google_signals'); 1097 const gcmAdPersonalizationSignals = document.getElementById('gcm_ad_personalization_signals'); 1098 const gcmRestrictedDataProcessing = document.getElementById('gcm_restricted_data_processing'); 1099 const gcmSendPageView = document.getElementById('gcm_send_page_view'); 1100 const gcmWaitForUpdate = document.getElementById('gcm_wait_for_update'); 1101 // const additionalSection = document.getElementById('additional_section'); 1102 const contentElement = document.getElementById('page_script'); 1103 1104 //console.dir(projectId); 1105 if (projectId == null) { //Create new project 1106 addLanguageSelector("en", availConsentLanguages); 1107 // layoutPropertiesEnabled?.dispatchEvent(new Event('change')); 1108 1109 for (let i = 0; i < creatingForms.length; i++) { 1110 const buttonsContainer = document.createElement('div');
1111 buttonsContainer.classList.add('mt-2', 'd-flex', 'justify-content-end'); 1112 if (i < creatingForms.length - 1) { 1113 buttonsContainer.innerHTML = '<button type="button" class="btn btn-primary btn-next w-lg mx-2">' + (translations['next'] || "Next") + '</button>'; 1114 // if (i > 0) 1115 // buttonsContainer.innerHTML += '<button type="button" class="btn btn-back w-sm" onclick="">' + (translations['back'] || "Back") + '</button>'; 1116 } else { 1117 const createButton = document.createElement('button'); 1118 createButton.type = 'submit'; 1119 createButton.classList.add('btn', 'btn-success', 'w-lg', 'mx-2'); 1120 createButton.textContent = translations['create'] || "Create"; 1121 buttonsContainer.appendChild(createButton); 1122 createButton.addEventListener('click', async () => { 1123 createButton.disabled = true; 1124 createButton.textContent = (translations['creating'] || "Creating") + '...'; 1125 await createTemplateSettings(); 1126 }); 1127 // buttonsContainer.innerHTML = '<button type="button" class="btn btn-success w-lg mx-2">' + (translations['create'] || "Create") + '</button>'; 1128 // buttonsContainer.innerHTML += '<button type="button" class="btn btn-back w-sm" onclick="">' + (translations['back'] || "Back") + '</button>'; 1129 } 1130 creatingForms[i].appendChild(buttonsContainer); 1131 } 1132 } else { //Edit an existing project 1133 document.getElementById('layout_properties_section').classList.remove('d-none'); 1134 1135 const data = { sid: getSid(), project: projectId }; 1136 const response = await postRequest(endpoint + '/project/get', data); 1137 if (response.ok) { 1138 const responseBody = await response.json(); 1139 if (responseBody.statusCode === 0) { 1140 projectSettingsForm.elements['name'].value = responseBody.project.projectName; 1141 const config = responseBody.config; 1142 projectAccessKey = config.accessKey; 1143 projectSettingsForm.elements['consent_enabled'].checked = config.cmpEnabled; 1144 enabledCheckbox.dispatchEvent(new Event('change')); 1145 // projectSettingsForm.elements['gdpr_applies_globally'].checked = config.cmpGdprAppliesGlobally; 1146 1147 addLanguageSelector(config.primaryLanguage, responseBody.availableLanguages); 1148 if (responseBody.availableLanguages) 1149 serverAvailableLanguages = responseBody.availableLanguages; 1150 projectSettingsForm.elements['use_client_info_for_language'].checked = config.useClientLanguage; 1151 1152 //Google / Microsoft Consent Mode â enablement is derived from per-region 1153 //site-data toggles; the sub-settings live in a collapsible body that 1154 //defaults to collapsed and is toggled by the user on demand. 1155 gcmUrlPassthrough.checked = config.cmpConsentModeV2.urlPassthrough; 1156 gcmAdsDataRedaction.checked = config.cmpConsentModeV2.adsDataRedaction; 1157 gcmGoogleSignals.checked = config.cmpConsentModeV2.googleSignals; 1158 gcmAdPersonalizationSignals.checked = config.cmpConsentModeV2.adPersonalizationSignals; 1159 gcmRestrictedDataProcessing.checked = config.cmpConsentModeV2.restrictedDataProcessing; 1160 gcmSendPageView.value = config.cmpConsentModeV2.sendPageView; 1161 gcmWaitForUpdate.value = config.cmpConsentModeV2.waitForUpdate; 1162 1163 projectSettingsForm.elements['auto_blocker'].checked = config.cmpAutoBlockerEnabled === "1"; 1164 projectSettingsForm.elements['privacy_button'].checked = config.cmpDisplayConsentButton; 1165 if (projectSettingsForm.elements['cmp_show_reject_all_home']) 1166 projectSettingsForm.elements['cmp_show_reject_all_home'].checked = config.cmpShowRejectAllHome !== false; 1167 if (projectSettingsForm.elements['cmp_hide_empty_cookie_categories']) 1168 projectSettingsForm.elements['cmp_hide_empty_cookie_categories'].checked = !!config.cmpHideEmptyCookieCategories; 1169 // Default ON â matches the CMP_EMBED_PLACEHOLDER backend default. 1170 if (projectSettingsForm.elements['cmp_embed_placeholder']) 1171 projectSettingsForm.elements['cmp_embed_placeholder'].checked = config.cmpEmbedPlaceholder !== false; 1172 // Opt-in (default OFF) â matches the CMP_REGATE_ON_WITHDRAWAL backend default. 1173 // Note the polarity: `!!` not `!== false`, or an absent value would read as ON. 1174 if (projectSettingsForm.elements['cmp_regate_on_withdrawal']) 1175 projectSettingsForm.elements['cmp_regate_on_withdrawal'].checked = !!config.cmpRegateO
1175nWithdrawal; 1176 // Opt-in (default OFF) â matches the CMP_ACCESSIBILITY_WIDGET backend 1177 // default. `!!` not `!== false`, same polarity reasoning as above. 1178 if (projectSettingsForm.elements['cmp_accessibility_widget']) 1179 projectSettingsForm.elements['cmp_accessibility_widget'].checked = !!config.cmpAccessibilityWidget; 1180 1181 // Banner copy experiment. The server always sends a NORMALIZED definition, 1182 // so anything it could not validate arrives as {} and the form simply shows 1183 // a stopped test â the panel never has to re-implement the whitelist. 1184 fcLoadBannerText(config.cmpBannerText); 1185 fcLoadExperiment(config.cmpExperiment); 1186 // Results for the last 30 days, alongside the editor: a merchant deciding 1187 // whether to stop a test should not have to go and find the numbers. 1188 if (projectId) { 1189 const expEnd = new Date(); 1190 const expStart = new Date(expEnd.getTime() - 29 * 24 * 3600 * 1000); 1191 // Local date parts, NOT toISOString(): that returns UTC, so east of UTC 1192 // every boundary shifts a day back â the same bug fixed in the trend 1193 // chart (verified there under Auckland and Los Angeles). 1194 const pad = (v) => String(v).padStart(2, '0'); 1195 const iso = (d) => d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); 1196 loadExperimentReport(projectId, iso(expStart), iso(expEnd)); 1197 } 1198 // Privacy requests: the lookup is on demand, so only the button is wired. 1199 const privacyBtn = document.getElementById('privacy_lookup_btn'); 1200 if (privacyBtn && !privacyBtn.dataset.fcBound) { 1201 privacyBtn.dataset.fcBound = '1'; 1202 privacyBtn.addEventListener('click', fcPrivacyLookup); 1203 const privacyInput = document.getElementById('privacy_client_key'); 1204 if (privacyInput) 1205 privacyInput.addEventListener('keydown', function (e) { 1206 if (e.key === 'Enter') { e.preventDefault(); fcPrivacyLookup(); } 1207 }); 1208 } 1209 // Opt-in (default off) â matches the CMP_MONTHLY_SCAN_REPORT backend default. 1210 if (projectSettingsForm.elements['cmp_monthly_scan_report']) 1211 projectSettingsForm.elements['cmp_monthly_scan_report'].checked = !!config.cmpMonthlyScanReport; 1212 1213 contentElement.textContent = responseBody.pageCode; 1214 1215 1216 advertisingForm.elements['advertising_eu'].checked = config.cmpAdvertisingSettings.eu; 1217 advertisingForm.elements['advertising_us'].checked = config.cmpAdvertisingSettings.us; 1218 usCheckbox.dispatchEvent(new Event('change')); 1219 advertisingForm.elements['advertising_ca'].checked = config.cmpAdvertisingSettings.ca; 1220 ['tr','br','za','jp','ch','au','kr','th','in'].forEach(function(r){ 1221 const el = advertisingForm.elements['advertising_' + r]; 1222 if (el) el.checked = !!config.cmpAdvertisingSettings[r]; 1223 }); 1224 advertisingForm.elements['advertising_others'].checked = config.cmpAdvertisingSettings.others; 1225 1226 const regulations = (config.cmpAdvertisingSettings && config.cmpAdvertisingSettings.regulations) || {}; 1227 const regUsToggle = document.getElementById('regulation_enable_us'); 1228 if (regUsToggle) regUsToggle.checked = regulations.us !== 0; 1229 1230 loadSiteDataConsent(config.cmpAdvertisingSettings && config.cmpAdvertisingSettings.siteDataConsent); 1231 loadQuebecRows(config.cmpAdvertisingSettings); 1232 1233 loadUsSectionPreferences(config['cmpSectionPreferences']); 1234 1235 1236 const customCss = config.customCss; 1237 layoutPropertiesEnabled.checked = customCss.enabled; 1238 layoutPropertiesEnabled.dispatchEvent(new Event('change')); 1239 layoutForm.elements['font_family'].value = customCss.fontFamily || ""; 1240 layoutForm.elements['font_size'].value = customCss.fontSize || ""; 1241 layoutForm.elements['custom_css'].value = customCss.css || ""; 1242 layoutForm.elements['custom_logo'].value = customCss.customLogo || ""; 1243 layoutForm.elements['custom_icon'].value = customCss.customIcon || ""; 1244 layoutForm.elements['home_modal_theme'].value = customCss.homeModalTheme || customCss.homeModalTheme || "default"; 1245 layoutForm.elements['home_modal_location'].value = customCss.homeModalLocation || customCss.homeModalPlace || "center"; 1246 layoutForm.elements['page_interaction'].checked = customCss.allowPageInteraction || false; 1247 1248 await previewChanged(); 1249 const previewFrame = document.getElementById('preview_frame'); 1250 // previewFrame.onload = function () { 1251 // const screenWidth = screen.width; 1252 // // const docWidth = document.documentElement.offsetWidth; 1253 // // const homeModal = previewFrame.contentWindow.document.querySelector('#rfCmpHomeModal'); 1254 // // const homeModalDialog = homeModal.querySelector('.rf-cmp-modal-dialog'); 1255 // // const homeModalContent = homeModal.querySelector('.rf-cmp-modal-content'); 1256 // // const homeModalContentWidth = homeModalContent.offsetWidth; 1257 // // const maxWidth = 640; 1258 // // // const maxWidth = window.getComputedStyle(homeModalDialog).getPropertyValue('max-width'); 1259 // const previewWidth = previewFrame.offsetWidth; 1260 // // const previewBodyWidth = previewFrame.contentWindow.document.body.offsetWidth; 1261 // 1262 // // const ratio = maxWidth / screenWidth; 1263 // // previewFrame.contentWindow.document.body.style.zoom = ratio; 1264 // 1265 // const ratio = previewWidth / screenWidth; 1266 // previewFrame.style.zoom = ratio; 1267 // } 1268 1269 // previewFrame.style.zoom = previewFrame.offsetWidth / screen.width; 1270 // Populate domain selects and load preview 1271 var projectDomains = []; 1272 var fallbackDomain = window.location.hostname; 1273 1274 function populateDomainSelect(selectId, domains) { 1275 var sel = document.getElementById(selectId); 1276 if (!sel) return; 1277 sel.innerHTML = ''; 1278 if (!domains || domains.length === 0) { 1279 var opt = document.createElement('option'); 1280 opt.value = '0'; 1281 opt.textContent = fallbackDomain; 1282 sel.appendChild(opt); 1283 } else { 1284 domains.forEach(function(d) { 1285 var opt = document.createElement('option'); 1286 opt.value = d.id || '0'; 1287 opt.textContent = d.domainName; 1288 sel.appendChild(opt); 1289 }); 1290 } 1291 } 1292 1293 function getPreviewUrl(domainId) { 1294 return "../views/consent_preview.jsp?pid=" + projectId + "&key=" + config.accessKey + "&domainId=" + domainId + "&rand=" + Math.random(); 1295 } 1296 1297 // Load domains for this project from the domain list API
1298 postRequest(endpoint + '/project-domain/list', { sid: getSid(), projectTypeId: 2 }) 1299 .then(function(r) { return r.json(); }) 1300 .then(function(body) { 1301 if (body.statusCode === 0 && body.domains) { 1302 // Filter domains belonging to this project 1303 projectDomains = body.domains.filter(function(d) { 1304 return d.project && String(d.project.id) === String(projectId); 1305 }); 1306 // If no domains for this project, use all user domains 1307 if (projectDomains.length === 0) 1308 projectDomains = body.domains; 1309 } 1310 populateDomainSelect('preview_domain_select', projectDomains); 1311 populateDomainSelect('geo_preview_domain_select', projectDomains); 1312 var sel = document.getElementById('preview_domain_select'); 1313 if (sel) previewFrame.src = getPreviewUrl(sel.value); 1314 }).catch(function() { 1315 populateDomainSelect('preview_domain_select', []); 1316 populateDomainSelect('geo_preview_domain_select', []); 1317 previewFrame.src = getPreviewUrl('0'); 1318 }); 1319 1320 var previewDomainSelect = document.getElementById('preview_domain_select'); 1321 if (previewDomainSelect) { 1322 previewDomainSelect.onchange = function() { 1323 previewFrame.src = getPreviewUrl(previewDomainSelect.value); 1324 }; 1325 } 1326 1327 var geoDomainSelect = document.getElementById('geo_preview_domain_select'); 1328 if (geoDomainSelect) { 1329 geoDomainSelect.onchange = function() { 1330 var geoFrame = document.getElementById('geo_preview_frame'); 1331 if (geoFrame && geoFrame.src) { 1332 var url = new URL(geoFrame.src, window.location.href); 1333 url.searchParams.set('domainId', geoDomainSelect.value); 1334 url.searchParams.set('rand', Math.random()); 1335 geoFrame.src = url.toString(); 1336 } 1337 }; 1338 } 1339 1340 1341 const previewNewTabBtn = document.getElementById('btn_preview_new_tab'); 1342 previewNewTabBtn.onclick = function (event) { 1343 event.preventDefault(); 1344 window.open(previewFrame.src, '_blank'); 1345 }; 1346 1347 allForms.forEach(form => { 1348 const buttonsContainer = document.createElement('div'); 1349 buttonsContainer.classList.add('d-flex', 'justify-content-end', 'mb-5', 'mb-md-0'); 1350 buttonsContainer.innerHTML = '<button type="submit" class="btn btn-success w-lg mx-2">' + (translations['save'] || "Save") + '</button>'; 1351 buttonsContainer.innerHTML += '<button type="button" class="btn btn-cancel w-sm" onclick="history.back()">' + (translations['cancel'] || "Cancel") + '</button>'; 1352 form.appendChild(buttonsContainer); 1353 }); 1354 } else { 1355 console.log('Loading Error Code: ' + responseBody.statusCode); 1356 showAlert("The page could not be loaded!", "error"); 1357 } 1358 1359 Array.prototype.forEach.call(document.getElementsByClassName('btn-success'), function(element) { 1360 element.removeAttribute('disabled'); 1361 }); 1362 } 1363 } 1364 1365 //General Tab 1366 // const gcmSubmenu = document.getElementById('gcm_submenu'); 1367 // if (projectSettingsForm.elements['google_consent_mode'].checked) 1368 // gcmSubmenu.classList.add('expanded'); 1369 1370 // Google/Microsoft Consent Mode sub-settings live inside a collapsible body; 1371 // the header toggles the body's `expanded` class. Default is collapsed â these 1372 // are advanced flags most publishers leave at their sensible defaults. 1373 const gcmSubmenuHeader = document.getElementById('gcm_submenu_header'); 1374 const gcmSubmenuBody = document.getElementById('gcm_submenu_body'); 1375 const gcmSubmenuChevron = document.getElementById('gcm_submenu_chevron'); 1376 if (gcmSubmenuHeader && gcmSubmenuBody && gcmSubmenuChevron) { 1377 gcmSubmenuHeader.addEventListener('click', function() { 1378 gcmSubmenuBody.classList.toggle('expanded'); 1379 gcmSubmenuChevron.innerHTML = gcmSubmenuBody.classList.contains('expanded') 1380 ? '<i class="fa-solid fa-chevron-down"></i>' 1381 : '<i class="fa-solid fa-chevron-right"></i>'; 1382 }); 1383 } 1384 1385 const triggerCustomization = document.getElementById('trigger_customization'); 1386 const chevronParent = triggerCustomization.lastElementChild; 1387 triggerCustomization.onclick = function() { 1388 const nextElement = this.nextElementSibling; 1389 nextElement.classList.toggle('expanded'); 1390 1391 if (nextElement.classList.contains('expanded')) 1392 chevronParent.innerHTML = '<i class="fa-solid fa-chevron-down"></i>'; 1393 else 1394 chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>'; 1395 } 1396 1397 const privacyHeader = document.getElementById('privacy_trigger_header'); 1398 const privacyBody = document.getElementById('privacy_trigger_body'); 1399 const privacyChevron = document.getElementById('privacy_trigger_chevron'); 1400 if (privacyHeader && privacyBody && privacyChevron) { 1401 privacyHeader.addEventListener('click', function() { 1402 privacyBody.classList.toggle('expanded'); 1403 privacyChevron.innerHTML = privacyBody.classList.contains('expanded') 1404 ? '<i class="fa-solid fa-chevron-down"></i>' 1405 : '<i class="fa-solid fa-chevron-right"></i>'; 1406 }); 1407 } 1408 1409 function wireCollapsible(headerId, bodyId, chevronId) { 1410 const header = document.getElementById(headerId); 1411 const body = document.getElementById(bodyId); 1412 const chevron = document.getElementById(chevronId);
1413 if (!header || !body || !chevron) return; 1414 header.addEventListener('click', function() { 1415 body.classList.toggle('expanded'); 1416 chevron.innerHTML = body.classList.contains('expanded') 1417 ? '<i class="fa-solid fa-chevron-down"></i>' 1418 : '<i class="fa-solid fa-chevron-right"></i>'; 1419 }); 1420 } 1421 1422 wireCollapsible('section_preferences_header', 'section_preferences_body', 'section_preferences_chevron'); 1423 wireCollapsible('gcm_section_header', 'gcm_section_body', 'gcm_section_chevron'); 1424 wireCollapsible('advertising_section_header', 'advertising_section_body', 'advertising_section_chevron'); 1425 1426 1427 //Advertising Tab 1428 1429 1430 //Layout Tab 1431 async function previewChanged(event) { 1432 const cmpDisplayPrivacyButton = document.getElementById('privacy_button').checked; 1433 1434 const previewSettings = { 1435 useClientLanguage: document.getElementById('use_client_info_for_language').checked, 1436 primaryLanguage: document.getElementById('select_language').value, 1437 cmpConsentModeV2: { 1438 enabled: isAnySiteDataEnabled(), 1439 urlPassthrough: document.getElementById('gcm_url_passthrough').checked, 1440 adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked, 1441 googleSignals: document.getElementById('gcm_google_signals').checked, 1442 adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked, 1443 restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked, 1444 sendPageView: document.getElementById('gcm_send_page_view').value !== 'false', 1445 waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000 1446 }, 1447 cmpAutoBlockerEnabled: document.getElementById('auto_blocker').checked, 1448 cmpDisplayConsentButton: cmpDisplayPrivacyButton, 1449 cmpShowRejectAllHome: document.getElementById('cmp_show_reject_all_home') 1450 ? document.getElementById('cmp_show_reject_all_home').checked : true, 1451 cmpHideEmptyCookieCategories: !!(document.getElementById('cmp_hide_empty_cookie_categories') 1452 && document.getElementById('cmp_hide_empty_cookie_categories').checked), 1453 1454 enabled: document.getElementById('layout_properties_enabled').checked, 1455 fontFamily: document.getElementById('font_family').value, 1456 fontSize: document.getElementById('font_size').value, 1457 css: document.getElementById('custom_css').value, 1458 customLogo: document.getElementById('custom_logo').value, 1459 customIcon: document.getElementById('custom_icon').value, 1460 homeModalTheme: document.getElementById('home_modal_theme').value, 1461 homeModalLocation: document.getElementById('home_modal_location').value, 1462 allowPageInteraction: document.getElementById('page_interaction').checked 1463 }; 1464 1465 const data = {sid: getSid(), project: projectId, previewSettings: previewSettings}; 1466 const response = await postRequest(endpoint + '/project/preview', data); 1467 if (response.ok) { 1468 const responseBody = await response.json(); 1469 if (responseBody.statusCode === 0 && event) { 1470 const previewFrame = document.getElementById('preview_frame'); 1471 previewFrame.contentWindow.location.reload(); 1472 } 1473 } 1474 } 1475 1476 if (projectId) { 1477 layoutPropertiesEnabled.addEventListener('change', previewChanged); 1478 document.getElementById('font_family').addEventListener('change', previewChanged); 1479 document.getElementById('font_size').addEventListener('change', previewChanged); 1480 document.getElementById('custom_css').addEventListener('change', previewChanged); 1481 document.getElementById('custom_logo').addEventListener('change', previewChanged); 1482 document.getElementById('custom_icon').addEventListener('change', previewChanged); 1483 document.getElementById('home_modal_theme').addEventListener('change', previewChanged); 1484 document.getElementById('home_modal_location').addEventListener('change', previewChanged); 1485 document.getElementById('page_interaction').addEventListener('change', previewChanged); 1486 } 1487 1488 // ===================== Geo Preview Tab ===================== 1489 if (projectId) { 1490 const geoGroupSelect = document.getElementById('geo_preview_group'); 1491 const geoCountrySelect = document.getElementById('geo_preview_country'); 1492 const geoStateSelect = document.getElementById('geo_preview_state'); 1493 const geoLangSelect = document.getElementById('geo_preview_lang'); 1494 const geoCountryContainer = document.getElementById('geo_preview_country_container'); 1495 const geoStateContainer = document.getElementById('geo_preview_state_container'); 1496 const geoInfoCard = document.getElementById('geo_preview_info'); 1497 const geoInfoContent = document.getElementById('geo_preview_info_content');
1498 const geoPreviewFrame = document.getElementById('geo_preview_frame'); 1499 let geoPreviewLoaded = false; 1500 1501 // Available CMP languages (populated from server response) 1502 const GEO_LANGUAGES = [ 1503 {code:'en',name:'English'},{code:'fr',name:'Français'},{code:'de',name:'Deutsch'}, 1504 {code:'es',name:'Español'},{code:'it',name:'Italiano'},{code:'pt',name:'Português'}, 1505 {code:'nl',name:'Nederlands'},{code:'pl',name:'Polski'},{code:'cs',name:'ÄeÅ¡tina'}, 1506 {code:'da',name:'Dansk'},{code:'sv',name:'Svenska'},{code:'fi',name:'Suomi'}, 1507 {code:'el',name:'Îλληνικά'},{code:'hu',name:'Magyar'},{code:'ro',name:'RomânÄ'}, 1508 {code:'sk',name:'SlovenÄina'},{code:'sl',name:'SlovenÅ¡Äina'},{code:'bg',name:'ÐÑлгаÑÑки'}, 1509 {code:'hr',name:'Hrvatski'},{code:'et',name:'Eesti'},{code:'lv',name:'LatvieÅ¡u'}, 1510 {code:'lt',name:'Lietuvių'},{code:'mt',name:'Malti'},{code:'ga',name:'Gaeilge'}, 1511 {code:'tr',name:'Türkçe'},{code:'ja',name:'æ¥æ¬èª'},{code:'zh',name:'䏿'}, 1512 {code:'ko',name:'íêµì´'},{code:'ar',name:'Ø§ÙØ¹Ø±Ø¨ÙØ©'},{code:'he',name:'×¢×ר×ת'}, 1513 {code:'ru',name:'Ð ÑÑÑкий'},{code:'uk',name:'УкÑаÑнÑÑка'},{code:'no',name:'Norsk'} 1514 ]; 1515 1516 function populateGeoLanguages() { 1517 var langs = serverAvailableLanguages || GEO_LANGUAGES; 1518 geoLangSelect.innerHTML = '<option value="">' + (translations['geoPreviewLangAuto'] || 'Auto (use browser default)') + '</option>';
1519 langs.forEach(function(l) { 1520 var code = l.code || l.shortCode || l; 1521 var name = l.name || code; 1522 var opt = document.createElement('option'); 1523 opt.value = code; 1524 opt.textContent = name + ' (' + code + ')'; 1525 geoLangSelect.appendChild(opt); 1526 }); 1527 } 1528 populateGeoLanguages(); 1529 1530 // Region/country definitions 1531 const EU_COUNTRIES = { 1532 'at':'Austria','be':'Belgium','bg':'Bulgaria','hr':'Croatia','cy':'Cyprus', 1533 'cz':'Czechia','dk':'Denmark','ee':'Estonia','fi':'Finland','fr':'France', 1534 'de':'Germany','gr':'Greece','hu':'Hungary','ie':'Ireland','it':'Italy', 1535 'lv':'Latvia','lt':'Lithuania','lu':'Luxembourg','mt':'Malta','nl':'Netherlands', 1536 'pl':'Poland','pt':'Portugal','ro':'Romania','sk':'Slovakia','si':'Slovenia', 1537 'es':'Spain','se':'Sweden','gb':'United Kingdom','no':'Norway','is':'Iceland', 1538 'li':'Liechtenstein','ch':'Switzerland' 1539 }; 1540 const US_STATES = { 1541 'ca':'California','va':'Virginia','co':'Colorado','ct':'Connecticut', 1542 'ut':'Utah','tx':'Texas','or':'Oregon','mt':'Montana','de':'Delaware', 1543 'ia':'Iowa','nh':'New Hampshire','nj':'New Jersey','tn':'Tennessee', 1544 'mn':'Minnesota','md':'Maryland','ky':'Kentucky','ri':'Rhode Island', 1545 'fl':'Florida','ne':'Nebraska','in':'Indiana','ny':'New York','il':'Illinois', 1546 'pa':'Pennsylvania','oh':'Ohio','ga':'Georgia','nc':'North Carolina', 1547 'mi':'Michigan','wa':'Washington','az':'Arizona','ma':'Massachusetts' 1548 }; 1549 // States with dedicated server-side implementation (state-specific generator 1550 // and/or GPP section). California gets CCPA-specific purposes (generateCcpaPurposes, 1551 // gppUsCa section); the rest share generateUsNatPurposes + gppUsNat. The 1552 // "Other" entry falls through to usnat on the server (stateCode doesn't match 1553 // any state flag â isUsNat=true). 1554 const US_STATES_IMPLEMENTED = { 1555 '': 'US National (usnat â MSPA catch-all)', 1556 'ca': 'California (CCPA)', 1557 'va': 'Virginia (VCDPA)', 1558 'co': 'Colorado (CPA)', 1559 'ut': 'Utah (UCPA)', 1560 'ct': 'Connecticut (CTDPA)', 1561 'tx': 'Texas (TDPSA)', 1562 'or': 'Oregon (OCPA)', 1563 'mt': 'Montana (MCDPA)', 1564 'de': 'Delaware (DPDPA)', 1565 'ia': 'Iowa (ICDPA)', 1566 'nh': 'New Hampshire (SB255)', 1567 'nj': 'New Jersey (NJDPA)', 1568 'tn': 'Tennessee (TIPA)', 1569 'mn': 'Minnesota (MCDPA)', 1570 'ne': 'Nebraska (NDPA)', 1571 'fl': 'Florida (FDBR)', 1572 'other': 'Other US state (falls through to usnat)' 1573 }; 1574 const US_STATES_IMPLEMENTED_ORDER = ['', 'ca', 'va', 'co', 'ut', 'ct', 'tx', 'or', 'mt', 'de', 'ia', 'nh', 'nj', 'tn', 'mn', 'ne', 'fl', 'other']; 1575 const GPP_OPT_OUT_STATES = ['ca','va','co','ct','ut','tx','or','mt','de','ia','nh','nj','tn','mn','md','ky','ri','fl','ne','in']; 1576 1577 // Regions that produce identical TCF EU v2 encoding when advertising is 1578 // enabled. Canada is excluded â it adds the GPP CA section and gets its 1579 // own group. US is state-aware and stays separate too. 1580 const TCF_REGION_COUNTRIES = { 1581 'eu': EU_COUNTRIES, 1582 'br': {'br':'Brazil'}, 1583 'za': {'za':'South Africa'}, 1584 'jp': {'jp':'Japan'}, 1585 'ch': {'ch':'Switzerland'}, 1586 'au': {'au':'Australia'}, 1587 'kr': {'kr':'South Korea'}, 1588 'th': {'th':'Thailand'}, 1589 'in': {'in':'India'}, 1590 'tr': {'tr':'Turkey'} 1591 }; 1592 const TCF_REGION_LABELS = { 1593 'eu':'EU/EEA','br':'Brazil','za':'South Africa','jp':'Japan', 1594 'ch':'Switzerland','au':'Australia','kr':'South Korea','th':'Thailand', 1595 'in':'India','tr':'Turkey' 1596 }; 1597 // Sample countries for the "others" catch-all bucket. 1598 const OTHERS_SAMPLE_COUNTRIES = { 1599 'mx':'Mexico','sg':'Singapore','ng':'Nigeria','ar':'Argentina','id':'Indonesia' 1600 }; 1601 1602 // Build regulation groups based on publisher's advertising settings. 1603 // Regions with identical regulation (same TCF EU v2 encoding) are merged 1604 // into a single group â the country dropdown picks a specific country. 1605 function buildGeoGroups() { 1606 function advChecked(code) { 1607 var el = document.getElementById('advertising_' + code); 1608 return !!(el && el.checked); 1609 } 1610 function siteDataChecked(code) { 1611 var el = document.getElementById('site_data_enable_' + code); 1612 return !!(el && el.checked); 1613 } 1614
1615 syncQuebecRows(); 1616 const advUs = advChecked('us'); 1617 const advCa = advChecked('ca'); 1618 const advQc = advChecked('qc'); 1619 const advOthers = advChecked('others'); 1620 const gcmCa = siteDataChecked('ca'); 1621 const gcmQc = siteDataChecked('qc'); 1622 const gcmOthers = siteDataChecked('others'); 1623 1624 const groups = []; 1625 1626 // Short translation lookup: panel translations live on `translations` 1627 // (populated from /consent/panel/tj/*.json). Every user-facing 1628 // geo-preview string goes through t() so localisation picks it up; 1629 // the hard-coded English fallback survives if the translation file 1630 // is missing a key. 1631 function t(key, fallback) { 1632 return (typeof translations !== "undefined" && translations && translations[key]) || fallback; 1633 } 1634 1635 // 1. TCF EU v2 groups, split by geography. All five produce identical 1636 // TCF EU v2 encoding on the server but a narrower country picker per 1637 // group keeps the preview usable â a combined group with 30+ 1638 // countries was cumbersome and (when the publisher hadn't saved 1639 // advertising changes) masked which specific region's setting was 1640 // driving the missing purposes. Each sub-group shows only countries 1641 // whose advertising flag is actually on. 1642 function tcfSubgroup(id, label, description, countries, defaultCountry) { 1643 if (!Object.keys(countries).length) return; 1644 groups.push({ 1645 id: id, 1646 label: label, 1647 countries: countries, 1648 info: '<b>' + t('geoPreviewTcfEuV2Header', 'TCF EU v2') + ' â ' + description + '</b><br>' 1649 + t('geoPreviewTcfInfoBlockingModal', 'Blocking modal, explicit consent required.') + '<br>' 1650 + t('geoPreviewTcfInfoIdentical', 'Produces a tcfeuv2 GPP section; every country in this group encodes identically.'), 1651 defaultCountry: countries[defaultCountry] ? defaultCountry : Object.keys(countries)[0], 1652 defaultState: '' 1653 }); 1654 } 1655 1656 // 1a. Europe/EEA & Switzerland â GDPR + FADP. EU_COUNTRIES already 1657 // includes Switzerland, so advertising_eu=1 covers the whole bloc. 1658 // If only advertising_ch=1 (Switzerland standalone) we still show 1659 // just the CH row. 1660 var euGroupCountries = {}; 1661 if (advChecked('eu')) Object.assign(euGroupCountries, EU_COUNTRIES); 1662 else if (advChecked('ch')) Object.assign(euGroupCountries, {'ch':'Switzerland'}); 1663 tcfSubgroup('tcf_eu', 1664 t('geoPreviewGroupEu', 'Europe/EEA & Switzerland'), 1665 t('geoPreviewGroupEuDesc', 'GDPR / FADP'), 1666 euGroupCountries, 'de'); 1667 1668 // 1b. Asia-Pacific â Japan, South Korea, Thailand, India, Australia. 1669 var apacCountries = {}; 1670 if (advChecked('jp')) apacCountries['jp'] = 'Japan'; 1671 if (advChecked('kr')) apacCountries['kr'] = 'South Korea'; 1672 if (advChecked('th')) apacCountries['th'] = 'Thailand'; 1673 if (advChecked('in')) apacCountries['in'] = 'India'; 1674 if (advChecked('au')) apacCountries['au'] = 'Australia'; 1675 tcfSubgroup('tcf_apac', 1676 t('geoPreviewGroupApac', 'Asia-Pacific'), 1677 t('geoPreviewGroupApacDesc', 'APPI / PIPA / PDPA / DPDP / Privacy Act'), 1678 apacCountries, 'jp'); 1679 1680 // 1c. Americas â Brazil plus sample Latin-American catch-all 1681 // countries when advertising_others=1. 1682 var americasCountries = {}; 1683 if (advChecked('br')) americasCountries['br'] = 'Brazil'; 1684 if (advOthers) { americasCountries['mx'] = 'Mexico'; americasCountries['ar'] = 'Argentina'; } 1685 tcfSubgroup('tcf_americas', 1686 t('geoPreviewGroupAmericas', 'Americas'), 1687 t('geoPreviewGroupAmericasDesc', 'LGPD + publisher-enabled global TCF'), 1688 americasCountries, 'br'); 1689 1690 // 1d. Africa, Middle East & Turkey â South Africa, Turkey, plus a 1691 // sample African catch-all when advertising_others=1. 1692 var meaCountries = {}; 1693 if (advChecked('za')) meaCountries['za'] = 'South Africa'; 1694 if (advChecked('tr')) meaCountries['tr'] = 'Turkey'; 1695 if (advOthers) meaCountries['ng'] = 'Nigeria'; 1696 tcfSubgroup('tcf_africa_me', 1697 t('geoPreviewGroupAfricaMe', 'Africa, Middle East & Turkey'), 1698 t('geoPreviewGroupAfricaMeDesc', 'POPIA / KVKK + publisher-enabled global TCF'), 1699 meaCountries, 'za'); 1700 1701 // 1e. Rest of World â only the "others" catch-all countries that 1702 // aren't already covered by one of the regional buckets above. 1703 var rowCountries = {}; 1704 if (advOthers) { rowCountries['sg'] = 'Singapore'; rowCountries['id'] = 'Indonesia'; } 1705 tcfSubgroup('tcf_row', 1706 t('geoPreviewGroupRow', 'Rest of World'), 1707 t('geoPreviewGroupRowDesc', 'Publisher-enabled global TCF'), 1708 rowCountries, 'sg'); 1709 1710 // 2. United States â state-aware, distinct from every other region. 1711 groups.push({ 1712 id: 'us', 1713 label: advUs 1714 ? t('geoPreviewGroupUsDual', 'United States (TCF + GPP)') 1715 : t('geoPreviewGroupUsGpp', 'United States (GPP opt-outs)'), 1716 countries: {'us': t('geoPreviewCountryUs', 'United States')},
1717 states: US_STATES_IMPLEMENTED, 1718 stateOrder: US_STATES_IMPLEMENTED_ORDER, 1719 info: advUs 1720 ? '<b>' + t('geoPreviewUsDualHeader', 'TCF EU v2 + GPP US sections') + '</b> â ' 1721 + t('geoPreviewUsDualInfo', 'Blocking TCF modal on auto-show plus GPP opt-outs accessible via Manage Privacy Settings.') 1722 + '<br>' + t('geoPreviewUsRouting', 'California â gppUsCa. VA/CO/UT/CT/usnat/Other â gppUsNat.') 1723 : '<b>' + t('geoPreviewUsGppHeader', 'GPP US state opt-outs') + '</b> â ' 1724 + t('geoPreviewUsGppInfo', 'Non-blocking, opt-out model. California uses the gppUsCa section (CCPA); every other implemented state and the "Other" catch-all encode via gppUsNat.'), 1725 defaultCountry: 'us', defaultState: '' 1726 }); 1727 1728 // 3. Canada â TCF + GPP CA only when advertising_ca=1 (unique regulation). 1729 if (advCa) { 1730 groups.push({ 1731 id: 'ca_tcf', 1732 label: t('geoPreviewGroupCanada', 'Canada â TCF + GPP TCF-CA'), 1733 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1734 info: '<b>' + t('geoPreviewCanadaHeader', 'TCF EU v2 + GPP TCF-CA (Section 5)') + '</b> â ' 1735 + t('geoPreviewCanadaInfo', 'Blocking modal.') 1736 + '<br>' + t('geoPreviewCanadaInfo2', 'Both TCF string and GPP CA section generated.'), 1737 defaultCountry: 'ca', defaultState: '' 1738 }); 1739 } 1740 // 3b. Quebec (J129): the same Canadian regime by its own rows; previewed as a visitor in Quebec (gs=qc), who also 1741 // gets French first when the browser lists it. 1742 const QUEBEC_STATES = {'qc': t('geoPreviewCountryQc', 'Quebec')}; 1743 if (advQc) { 1744 groups.push({ 1745 id: 'qc_tcf', 1746 label: t('geoPreviewGroupQuebec', 'Quebec â TCF + GPP TCF-CA'), 1747 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1748 states: QUEBEC_STATES, 1749 info: '<b>' + t('geoPreviewCanadaHeader', 'TCF EU v2 + GPP TCF-CA (Section 5)') + '</b> â ' 1750 + t('geoPreviewCanadaInfo', 'Blocking modal.') 1751 + '<br>' + t('geoPreviewCanadaInfo2', 'Both TCF string and GPP CA section generated.'), 1752 defaultCountry: 'ca', defaultState: 'qc' 1753 }); 1754 } 1755 1756 // 4. Site-data-only regions â one dropdown entry per contributing 1757 // region, not a single group with every region concatenated into 1758 // the label. A <select> doesn't wrap option text, so a combined 1759 // label like "Site data consent only â EU/EEA, Brazil, South 1760 // Africa, Japan, Switzerland, Australia, South Korea, Thailand, 1761 // India, Turkey, Canada, Rest of World" used to overflow. Each 1762 // region gets its own short, readable option; clicking one loads 1763 // the preview for just that region's visitor. 1764 var siteDataRegions = []; 1765 var siteDataPrefix = t('geoPreviewSiteDataOnlyPrefix', 'Site data consent only â '); 1766 Object.keys(TCF_REGION_COUNTRIES).forEach(function(code) { 1767 if (!advChecked(code) && siteDataChecked(code)) { 1768 siteDataRegions.push({ 1769 id: 'sitedata_only_' + code, 1770 label: siteDataPrefix + t('geoPreviewRegion_' + code, TCF_REGION_LABELS[code]), 1771 countries: TCF_REGION_COUNTRIES[code], 1772 defaultCountry: Object.keys(TCF_REGION_COUNTRIES[code])[0] 1773 }); 1774 } 1775 }); 1776 if (!advCa && gcmCa) { 1777 siteDataRegions.push({ 1778 id: 'sitedata_only_ca', 1779 label: siteDataPrefix + t('geoPreviewCountryCa', 'Canada'), 1780 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1781 defaultCountry: 'ca' 1782 }); 1783 } 1784 if (!advQc && gcmQc) { 1785 siteDataRegions.push({ 1786 id: 'sitedata_only_qc', 1787 label: siteDataPrefix + t('geoPreviewCountryQc', 'Quebec'), 1788 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1789 states: QUEBEC_STATES, defaultState: 'qc', 1790 defaultCountry: 'ca' 1791 }); 1792 } 1793 if (!advOthers && gcmOthers) { 1794 siteDataRegions.push({ 1795 id: 'sitedata_only_others', 1796 label: siteDataPrefix + t('geoPreviewGroupRow', 'Rest of World'), 1797 countries: OTHERS_SAMPLE_COUNTRIES, 1798 defaultCountry: Object.keys(OTHERS_SAMPLE_COUNTRIES)[0] 1799 }); 1800 } 1801 siteDataRegions.forEach(function(r) { 1802 groups.push({ 1803 id: r.id, 1804 label: r.label, 1805 countries: r.countries, 1806 info: '<b>' + t('geoPreviewSiteDataHeader', 'Site data consent') + '</b> â ' 1807 + t('geoPreviewSiteDataInfo', 'Cookie categories (essential, analytics, marketing).') 1808 + '<br>' + t('geoPreviewSiteDataInfo2', 'No IAB TCF/GPP. Only site-specific cookie management.'), 1809 states: r.states, 1810 defaultCountry: r.defaultCountry, defaultState: r.defaultState || '' 1811 }); 1812 }); 1813 1814 // 5. No-consent catch-all â same per-region-per-entry treatment. 1815 // Lets the publisher click "No consent popup â Japan" and see 1816 // that specific visitor without scanning a long compound label. 1817 var noConsentRegions = []; 1818 var noConsentPrefix = t('geoPreviewNoConsentPrefix', 'No consent popup â ');
1819 Object.keys(TCF_REGION_COUNTRIES).forEach(function(code) { 1820 if (!advChecked(code) && !siteDataChecked(code)) { 1821 noConsentRegions.push({ 1822 id: 'no_consent_' + code, 1823 label: noConsentPrefix + t('geoPreviewRegion_' + code, TCF_REGION_LABELS[code]), 1824 countries: TCF_REGION_COUNTRIES[code], 1825 defaultCountry: Object.keys(TCF_REGION_COUNTRIES[code])[0] 1826 }); 1827 } 1828 }); 1829 if (!advCa && !gcmCa) { 1830 noConsentRegions.push({ 1831 id: 'no_consent_ca', 1832 label: noConsentPrefix + t('geoPreviewCountryCa', 'Canada'), 1833 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1834 defaultCountry: 'ca' 1835 }); 1836 } 1837 if (!advQc && !gcmQc) { 1838 noConsentRegions.push({ 1839 id: 'no_consent_qc', 1840 label: noConsentPrefix + t('geoPreviewCountryQc', 'Quebec'), 1841 countries: {'ca': t('geoPreviewCountryCa', 'Canada')}, 1842 states: QUEBEC_STATES, defaultState: 'qc', 1843 defaultCountry: 'ca' 1844 }); 1845 } 1846 if (!advOthers && !gcmOthers) { 1847 noConsentRegions.push({ 1848 id: 'no_consent_others', 1849 label: noConsentPrefix + t('geoPreviewGroupRow', 'Rest of World'), 1850 countries: OTHERS_SAMPLE_COUNTRIES, 1851 defaultCountry: Object.keys(OTHERS_SAMPLE_COUNTRIES)[0] 1852 }); 1853 } 1854 noConsentRegions.forEach(function(r) { 1855 groups.push({ 1856 id: r.id, 1857 label: r.label, 1858 countries: r.countries, 1859 info: '<b>' + t('geoPreviewNoConsentHeader', 'No consent required') + '</b> â ' 1860 + t('geoPreviewNoConsentInfo', 'No popup shown.') 1861 + '<br>' + t('geoPreviewNoConsentInfo2', 'Only the privacy button is visible (if enabled).'), 1862 states: r.states, 1863 defaultCountry: r.defaultCountry, defaultState: r.defaultState || '' 1864 }); 1865 }); 1866 1867 return groups; 1868 } 1869 1870 function populateGeoGroups() { 1871 const groups = buildGeoGroups(); 1872 geoGroupSelect.innerHTML = '<option value="" disabled selected>' + (translations['geoPreviewSelectRegion'] || 'Select a region...') + '</option>'; 1873 groups.forEach(function(g) { 1874 var opt = document.createElement('option'); 1875 opt.value = g.id; 1876 opt.textContent = g.label; 1877 geoGroupSelect.appendChild(opt); 1878 }); 1879 geoGroupSelect._groups = groups; 1880 } 1881 1882 function getSelectedGroup() { 1883 var groups = geoGroupSelect._groups || []; 1884 var val = geoGroupSelect.value; 1885 for (var i = 0; i < groups.length; i++) { 1886 if (groups[i].id === val) return groups[i]; 1887 } 1888 return null; 1889 } 1890 1891 geoGroupSelect.addEventListener('change', function() { 1892 var group = getSelectedGroup(); 1893 if (!group) return; 1894 1895 // Populate countries 1896 var countries = group.countries || {}; 1897 var keys = Object.keys(countries); 1898 if (keys.length > 1) { 1899 geoCountryContainer.classList.remove('d-none'); 1900 geoCountrySelect.innerHTML = ''; 1901 keys.sort(function(a,b){ return countries[a].localeCompare(countries[b]); }); 1902 keys.forEach(function(code) { 1903 var opt = document.createElement('option'); 1904 opt.value = code; 1905 opt.textContent = countries[code]; 1906 geoCountrySelect.appendChild(opt); 1907 }); 1908 // Set the selection explicitly on the mounted element â setting 1909 // opt.selected before append is unreliable in some browsers, which 1910 // left select.value empty and the server without a gc override. 1911 if (group.defaultCountry && Object.prototype.hasOwnProperty.call(countries, group.defaultCountry)) { 1912 geoCountrySelect.value = group.defaultCountry; 1913 } else { 1914 geoCountrySelect.value = keys[0]; 1915 } 1916 } else { 1917 geoCountryContainer.classList.add('d-none'); 1918 geoCountrySelect.innerHTML = '<option value="' + keys[0] + '">' + countries[keys[0]] + '</option>'; 1919 } 1920 1921 // Populate states. Respect group.stateOrder when supplied (US group 1922 // uses this to keep implemented states listed first in a specific 1923 // order instead of alphabetic); otherwise sort alphabetically by label. 1924 if (group.states && Object.keys(group.states).length > 0) { 1925 geoStateContainer.classList.remove('d-none'); 1926 geoStateSelect.innerHTML = ''; 1927 var stateKeys; 1928 if (group.stateOrder && group.stateOrder.length) { 1929 stateKeys = group.stateOrder.filter(function(k){ return Object.prototype.hasOwnProperty.call(group.states, k); }); 1930 } else { 1931 stateKeys = Object.keys(group.states); 1932 stateKeys.sort(function(a,b){ return group.states[a].localeCompare(group.states[b]); }); 1933 } 1934 stateKeys.forEach(function(code) { 1935 var opt = document.createElement('option'); 1936 opt.value = code; 1937 opt.textContent = group.states[code]; 1938 geoStateSelect.appendChild(opt); 1939 }); 1940 // Same reason as the country select above â explicit value 1941 // assignment after mount. 1942 if (Object.prototype.hasOwnProperty.call(group.states, group.defaultState || '')) { 1943 geoStateSelect.value = group.defaultState || ''; 1944 } else { 1945 geoStateSelect.value = stateKeys[0]; 1946 } 1947 } else { 1948 geoStateContainer.classList.add('d-none'); 1949 geoStateSelect.innerHTML = ''; 1950 } 1951 1952 // Show info card 1953 geoInfoCard.classList.remove('d-none'); 1954 geoInfoContent.innerHTML = group.info; 1955 1956 refreshGeoPreview(); 1957 }); 1958 1959 geoCountrySelect.addEventListener('change', refreshGeoPreview); 1960 geoStateSelect.addEventListener('change', refreshGeoPreview); 1961 geoLangSelect.addEventListener('change', refreshGeoPreview); 1962 1963 function refreshGeoPreview() { 1964 var country = geoCountrySelect.value || ''; 1965 var state = geoStateSelect.value || ''; 1966 var lang = geoLangSelect.value || ''; 1967 if (!country) return; 1968
1969 // clear=1 tells consent_preview.jsp to wipe CMP/IAB storage before the 1970 // CMP boots. Every geo-group switch should render as a fresh visitor 1971 // â otherwise stored consent from the previous region's selection 1972 // suppresses the auto-popup (and the IAB state cache can keep 1973 // displaying the prior region's purposes). 1974 var src = "../views/consent_preview.jsp?pid=" + projectId 1975 + "&key=" + projectAccessKey 1976 + "&gc=" + encodeURIComponent(country) 1977 + "&gs=" + encodeURIComponent(state) 1978 + (lang ? "&gl=" + encodeURIComponent(lang) : "") 1979 + "&clear=1" 1980 + "&rand=" + Math.random(); 1981 1982 if (geoPreviewLoaded) { 1983 geoPreviewFrame.src = src; 1984 } else { 1985 geoPreviewFrame.src = src; 1986 geoPreviewLoaded = true; 1987 } 1988 } 1989 1990 // Rebuild geo groups when advertising checkboxes or site-data toggles change 1991 CFG_TCF_REGIONS.concat(CFG_GCM_REGIONS).forEach(function(id) { 1992 var el = document.getElementById(id); 1993 if (el) el.addEventListener('change', populateGeoGroups); 1994 }); 1995 1996 // Lazy-load: populate groups when tab is first shown 1997 var geoTab = document.getElementById('geopreview-tab'); 1998 if (geoTab) { 1999 geoTab.addEventListener('shown.bs.tab', function() { 2000 if (!geoGroupSelect._groups) populateGeoGroups(); 2001 // Re-populate languages with server data if it arrived after initial populate 2002 if (serverAvailableLanguages) populateGeoLanguages(); 2003 }); 2004 populateGeoGroups(); 2005 } 2006 2007 // Mobile/Desktop toggle for geo preview 2008 var geoBrowserFrame = document.getElementById('geo_browser_frame'); 2009 var geoBtnMobile = document.getElementById('geo_btn_mobile_toggle'); 2010 var geoBtnDesktop = document.getElementById('geo_btn_desktop_ratio'); 2011 2012 if (geoBtnMobile) { 2013 geoBtnMobile.onclick = function(e) { 2014 e.preventDefault(); 2015 geoBtnMobile.classList.toggle('active'); 2016 if (geoBtnMobile.classList.contains('active')) { 2017 geoBrowserFrame.classList.add('mobile'); 2018 geoBtnDesktop.classList.remove('active'); 2019 geoPreviewFrame.style.zoom = 1; 2020 geoBtnDesktop.style.display = 'none'; 2021 } else { 2022 geoBrowserFrame.classList.remove('mobile'); 2023 geoBtnDesktop.style.display = 'inline-block'; 2024 } 2025 }; 2026 } 2027 2028 if (geoBtnDesktop) { 2029 geoBtnDesktop.onclick = function(e) { 2030 e.preventDefault(); 2031 geoBtnDesktop.classList.toggle('active'); 2032 geoPreviewFrame.style.zoom = geoBtnDesktop.classList.contains('active') 2033 ? geoPreviewFrame.offsetWidth / screen.width : 1; 2034 }; 2035 } 2036 2037 var geoBtnNewTab = document.getElementById('geo_btn_preview_new_tab'); 2038 if (geoBtnNewTab) { 2039 geoBtnNewTab.onclick = function(e) { 2040 e.preventDefault(); 2041 if (geoPreviewFrame.src) window.open(geoPreviewFrame.src, '_blank'); 2042 }; 2043 } 2044 } 2045 // ===================== End Geo Preview Tab ===================== 2046 2047 2048 // Same page as advertisingForm and registered first: a null here would take the whole 2049 // settings page's wiring down with it, not just its own tab. 2050 projectSettingsForm?.addEventListener('submit', function(event) { 2051 event.preventDefault(); 2052 2053 // GCM site-data toggles, GCM sub-settings and the Manage Privacy Settings trigger 2054 // now live on the Consent & Privacy tab and are saved by advertisingForm. 2055 // The server reads `projectName` (RestCommands, /project/config); this handler posted 2056 // `name` for years, so a rename from the General tab was acknowledged with a success 2057 // toast and silently dropped. tests/panel/verify-settings-save.js round-trips it. 2058 const data = { 2059 projectName: projectSettingsForm.elements['name'].value.trim(), 2060 cmpEnabled: projectSettingsForm.elements['consent_enabled'].checked, 2061 primaryLanguage: projectSettingsForm.elements['select_language'].value, 2062 useClientLanguage: projectSettingsForm.elements['use_client_info_for_language'].checked, 2063 cmpAutoBlockerEnabled: projectSettingsForm.elements['auto_blocker'].checked, 2064 cmpDisplayConsentButton: projectSettingsForm.elements['privacy_button'].checked, 2065 cmpShowRejectAllHome: projectSettingsForm.elements['cmp_show_reject_all_home'] 2066 ? projectSettingsForm.elements['cmp_show_reject_all_home'].checked 2067 : true, 2068 cmpHideEmptyCookieCategories: !!(projectSettingsForm.elements['cmp_hide_empty_cookie_categories'] 2069 && projectSettingsForm.elements['cmp_hide_empty_cookie_categories'].checked), 2070 cmpMonthlyScanReport: !!(projectSettingsForm.elements['cmp_monthly_scan_report'] 2071 && projectSettingsForm.elements['cmp_monthly_scan_report'].checked), 2072 cmpEmbedPlaceholder: projectSettingsForm.elements['cmp_embed_placeholder'] 2073 ? projectSettingsForm.elements['cmp_embed_placeholder'].checked : true,
2074 cmpRegateOnWithdrawal: projectSettingsForm.elements['cmp_regate_on_withdrawal'] 2075 ? projectSettingsForm.elements['cmp_regate_on_withdrawal'].checked : false, 2076 cmpAccessibilityWidget: projectSettingsForm.elements['cmp_accessibility_widget'] 2077 ? projectSettingsForm.elements['cmp_accessibility_widget'].checked : false, 2078 cmpExperiment: fcCollectExperiment(), 2079 // §12. This is the payload the Save button actually sends. A merchant reported 2080 // on 2026-09-10 that the banner fields never reached the server: §12 had been 2081 // wired into saveTemplateSettings() instead, which reads identically and is 2082 // never called. The round-trip verifier's mustCarry list now names this field. 2083 cmpBannerText: fcCollectBannerText() 2084 }; 2085 updateTemplateSettings(data, projectId); 2086 }); 2087 2088 // "Apply US National settings to all states" â copies the personal_data, targeted_ads, 2089 // and sensitive_data values from US National into CA/VA/CO/UT/CT, so the publisher 2090 // doesn't have to configure each state block individually. California has no 2091 // targeted_ads field (CCPA/uscav1 folds it into the sale/share control), so that 2092 // field is skipped for CA. 2093 const applyUsNatToAllBtn = document.getElementById('us_nat_apply_to_all_states'); 2094 if (applyUsNatToAllBtn) applyUsNatToAllBtn.addEventListener('click', function() { 2095 const personal = document.getElementById('us_nat_pref_personal_data').value; 2096 const targeted = document.getElementById('us_nat_pref_targeted_ads').value; 2097 const sensitive = document.getElementById('us_nat_pref_sensitive_data').value; 2098 US_SECTION_PREFS.forEach(function(sec) { 2099 if (sec.key === 'usNat') return; 2100 const p = document.getElementById(sec.prefix + '_personal_data'); 2101 const s = document.getElementById(sec.prefix + '_sensitive_data'); 2102 if (p) p.value = personal; 2103 if (s) s.value = sensitive; 2104 if (sec.withTargetedAds) { 2105 const t = document.getElementById(sec.prefix + '_targeted_ads'); 2106 if (t) t.value = targeted; 2107 } 2108 }); 2109 }); 2110 2111 // Guarded like layoutForm below, and for a reason that is not hypothetical: this form is 2112 // called `advertising_settings_form` and holds 19 controls named `advertising_*`. Those are 2113 // exactly the names ad blockers strip with cosmetic filters. If the form is removed from the 2114 // DOM, an unguarded addEventListener here throws DURING SCRIPT LOAD â so every handler 2115 // registered AFTER this line never attaches, while the General tab above keeps working 2116 // because its handler was registered first. The visible symptom is a Save button that sends 2117 // no request at all plus a console error, on one tab only, for one user. 2118 // 2119 // The same reasoning applies to the four regions below: nine of the thirteen were already 2120 // written defensively, and eu/us/ca/others were not â a missing checkbox there throws inside 2121 // the handler and loses the save just as completely. 2122 advertisingForm?.addEventListener('submit', function (event) { 2123 event.preventDefault(); 2124 2125 const checked = (name) => { 2126 const el = advertisingForm.elements[name]; 2127 return el && el.checked ? 1 : 0; 2128 }; 2129 const advertisingSettings = { 2130 eu: checked('advertising_eu'), 2131 us: checked('advertising_us'), 2132 ca: checked('advertising_ca'), 2133 tr: checked('advertising_tr'), 2134 br: checked('advertising_br'), 2135 za: checked('advertising_za'), 2136 jp: checked('advertising_jp'), 2137 ch: checked('advertising_ch'), 2138 au: checked('advertising_au'), 2139 kr: checked('advertising_kr'), 2140 th: checked('advertising_th'), 2141 in: checked('advertising_in'), 2142 others: checked('advertising_others'), 2143 regulations: { 2144 us: document.getElementById('regulation_enable_us') && document.getElementById('regulation_enable_us').checked ? 1 : 0, 2145 ca: 1, 2146 tr: 1 2147 }, 2148 siteDataConsent: collectSiteDataConsent() 2149 }; 2150 addQuebecAdvertising(advertisingSettings); 2151 const sectionPreferences = collectUsSectionPreferences(); 2152 const data = { 2153 cmpAdvertisingSettings: advertisingSettings, 2154 cmpSectionPreferences: sectionPreferences, 2155 cmpConsentModeV2: { 2156 enabled: isAnySiteDataEnabled(), 2157 urlPassthrough: document.getElementById('gcm_url_passthrough').checked, 2158 adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked, 2159 googleSignals: document.getElementById('gcm_google_signals').checked, 2160 adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked, 2161 restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked, 2162 sendPageView: document.getElementById('gcm_send_page_view').value !== 'false', 2163 waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000 2164 } 2165 }; 2166 updateTemplateSettings(data, projectId); 2167 }); 2168
2169 setupConsentConfigureModal(); 2170 2171 layoutForm?.addEventListener('submit', function (event) { 2172 event.preventDefault(); 2173 2174 const data = { 2175 customCss: { 2176 enabled: layoutForm.elements['layout_properties_enabled'].checked, 2177 fontFamily: layoutForm.elements['font_family'].value, 2178 fontSize: layoutForm.elements['font_size'].value, 2179 css: layoutForm.elements['custom_css'].value, 2180 customLogo: layoutForm.elements['custom_logo'].value, 2181 customIcon: layoutForm.elements['custom_icon'].value, 2182 homeModalTheme: layoutForm.elements['home_modal_theme'].value, 2183 homeModalLocation: layoutForm.elements['home_modal_location'].value, 2184 allowPageInteraction: layoutForm.elements['page_interaction'].checked 2185 } 2186 }; 2187 updateTemplateSettings(data, projectId); 2188 }); 2189 2190 2191 const tabsUl = document.getElementById('settings_tabs'); 2192 const tabContents = document.getElementById('tab_contents'); 2193 const nextButtons = document.getElementsByClassName('btn-next'); 2194 Array.prototype.forEach.call(nextButtons, function(button) { 2195 button.addEventListener('click', function() { 2196 const currentForm = this.closest('form'); 2197 const currentIndex = creatingForms.indexOf(currentForm); 2198 if (currentIndex >= 0 && currentIndex < creatingForms.length - 1) { 2199 for (let form of creatingForms) { 2200 const elements = form.getElementsByTagName("*"); 2201 for (let child of elements) { 2202 if (child.tagName === 'INPUT' && child.hasAttribute('required') && child.offsetParent !== null) { 2203 if (child.value.trim() === "") { 2204 showAlert((translations['fieldRequired'] || "This field is required") + ": " + (child.getAttribute('data-field-name') || child.id), "warning"); 2205 child.focus(); 2206 return; 2207 } 2208 } 2209 } 2210 } 2211 2212 // Deactivate current tab 2213 tabsUl.children[currentIndex].querySelector('button').classList.remove('active'); 2214 tabContents.children[currentIndex].classList.remove('show', 'active'); 2215 // Activate next tab 2216 tabsUl.children[currentIndex + 1].querySelector('button').classList.add('active'); 2217 tabContents.children[currentIndex + 1].classList.add('show', 'active'); 2218 window.scrollTo(0, 0); 2219 } 2220 }); 2221 }); 2222} 2223 2224// saveTemplateSettings() lived here and nothing ever called it: the settings page saves 2225// through the projectSettingsForm submit handler above. It was a complete, plausible second 2226// copy of the same payload â its own fetch, its own field list â and on 2026-09-10 a merchant 2227// reported that the §12 banner fields never reached the server because §12 had been wired into 2228// this copy instead of the live one. Deleted rather than left as a trap for whoever wires the 2229// next setting. The create path is createTemplateSettings() below, which IS called. 2230 2231async function createTemplateSettings() { 2232 const name = document.getElementById('name').value.trim(); 2233 const cmpEnabled = document.getElementById('consent_enabled').checked; 2234 const primaryLanguage = document.getElementById('select_language').value; 2235 const useClientLanguage = document.getElementById('use_client_info_for_language').checked; 2236 const googleConsentModeV2 = { 2237 enabled: isAnySiteDataEnabled(), 2238 urlPassthrough: document.getElementById('gcm_url_passthrough').checked, 2239 adsDataRedaction: document.getElementById('gcm_ads_data_redaction').checked, 2240 googleSignals: document.getElementById('gcm_google_signals').checked, 2241 adPersonalizationSignals: document.getElementById('gcm_ad_personalization_signals').checked, 2242 restrictedDataProcessing: document.getElementById('gcm_restricted_data_processing').checked, 2243 sendPageView: document.getElementById('gcm_send_page_view').value !== 'false', 2244 waitForUpdate: parseInt(document.getElementById('gcm_wait_for_update').value, 10) || 1000 2245 }; 2246 const autoBlockerEnabled = document.getElementById('auto_blocker').checked; 2247 const cmpDisplayPrivacyButton = document.getElementById('privacy_button').checked; 2248 2249 const advertisingSettings = { 2250 eu: document.getElementById('advertising_eu').checked ? 1 : 0, 2251 us: document.getElementById('advertising_us').checked ? 1 : 0, 2252 ca: document.getElementById('advertising_ca').checked ? 1 : 0, 2253 tr: document.getElementById('advertising_tr') && document.getElementById('advertising_tr').checked ? 1 : 0, 2254 br: document.getElementById('advertising_br') && document.getElementById('advertising_br').checked ? 1 : 0, 2255 za: document.getElementById('advertising_za') && document.getElementById('advertising_za').checked ? 1 : 0, 2256 jp: document.getElementById('advertising_jp') && document.getElementById('advertising_jp').checked ? 1 : 0, 2257 ch: document.getElementById('advertising_ch') && document.getElementById('advertising_ch').checked ? 1 : 0, 2258 au: document.getElementById('advertising_au') && document.getElementById('advertising_au').checked ? 1 : 0, 2259 kr: document.getElementById('advertising_kr') && document.getElementById('advertising_kr').checked ? 1 : 0, 2260 th: document.getElementById('advertising_th') && document.getElementById('advertising_th').checked ? 1 : 0, 2261 in: document.getElementById('advertising_in') && document.getElementById('advertising_in').checked ? 1 : 0, 2262 others: document.getElementById('advertising_others').checked ? 1 : 0, 2263 regulations: { 2264 us: document.getElementById('regulation_enable_us') && document.getElementById('regulation_enable_us').checked ? 1 : 0, 2265 ca: 1, 2266 tr: 1 2267 }, 2268 siteDataConsent: collectSiteDataConsent() 2269 } 2270 addQuebecAdvertising(advertisingSettings); 2271 const sectionPreferences = collectUsSectionPreferences(); 2272 2273 const data = { 2274 projectTypeId: 2, 2275 sid: getSid(), 2276 2277 projectName: name, 2278 cmpEnabled: cmpEnabled, 2279 useClientLanguage: useClientLanguage, 2280 primaryLanguage: primaryLanguage, 2281 cmpConsentModeV2: googleConsentModeV2, 2282 cmpAutoBlockerEnabled: autoBlockerEnabled, 2283 cmpDisplayConsentButton: cmpDisplayPrivacyButton, 2284 cmpShowRejectAllHome: document.getElementById('cmp_show_reject_all_home') 2285 ? document.getElementById('cmp_show_reject_all_home').checked : true, 2286 cmpHideEmptyCookieCategories: !!(document.getElementById('cmp_hide_empty_cookie_categories') 2287 && document.getElementById('cmp_hide_empty_cookie_categories').checked), 2288 cmpMonthlyScanReport: !!(document.getElementById('cmp_monthly_scan_report') 2289 && document.getElementById('cmp_monthly_scan_report').checked), 2290 cmpEmbedPlaceholder: document.getElementById('cmp_embed_placeholder') 2291 ? document.getElementById('cmp_embed_placeholder').checked : true,
2292 cmpRegateOnWithdrawal: document.getElementById('cmp_regate_on_withdrawal') 2293 ? document.getElementById('cmp_regate_on_withdrawal').checked : false, 2294 cmpAccessibilityWidget: document.getElementById('cmp_accessibility_widget') 2295 ? document.getElementById('cmp_accessibility_widget').checked : false, 2296 cmpBannerText: fcCollectBannerText(), 2297 cmpExperiment: fcCollectExperiment(), 2298 2299 cmpAdvertisingSettings: advertisingSettings, 2300 cmpSectionPreferences: sectionPreferences 2301 }; 2302 2303 const websiteId = new URLSearchParams(window.location.search).get("wid"); 2304 if (websiteId) { 2305 data.projectDomainId = websiteId; 2306 } 2307 2308 const response = await postRequest(endpoint + '/project/create', data); 2309 if (response.ok) { 2310 const responseBody = await response.json(); 2311 if (responseBody.statusCode === 0) { 2312 displayAlert('templateSaveSuccess', 'success'); 2313 if (responseBody.pageCode) { 2314 console.dir('show code'); 2315 document.getElementById('settings_tabs').style.display = 'none'; 2316 document.getElementById('tab_contents').style.display = 'none'; 2317 var scriptc = document.getElementById('script_card'); 2318 scriptc.classList.remove('d-none'); 2319 scriptc.style.display = 'block'; 2320 scriptc = document.getElementById('page_script'); 2321 scriptc.textContent = responseBody.pageCode; 2322 createCopyToClipboard('copy_btn', 'page_script', 'copy-info'); 2323 } 2324 } else { 2325 console.log('Saving Error Code: ' + responseBody.statusCode); 2326 displayAlert('Cannot create project: ' + responseBody.statusMessage); 2327 } 2328 } else { 2329 console.error('Saving failed: ' + JSON.stringify(response)); 2330 displayAlert('Cannot create project. Please try later'); 2331 } 2332} 2333 2334async function updateTemplateSettings(data, projectId) { 2335 data['projectTypeId'] = 2; 2336 data['sid'] = getSid(); 2337 data['project'] = projectId; 2338 const response = await postRequest(endpoint + '/project/config', data); 2339 if (response.ok) { 2340 const responseBody = await response.json(); 2341 if (responseBody.statusCode === 0) { 2342 displayAlert('templateSaveSuccess', 'success'); 2343 } else { 2344 console.log('Saving Error Code: ' + responseBody.statusCode); 2345 displayAlert('Cant save project: ' + responseBody.statusMessage); 2346 } 2347 } else { 2348 console.error('Saving failed: ' + JSON.stringify(response)); 2349 displayAlert('Cant save project: Please try later'); 2350 } 2351} 2352 2353 2354/* WEBSITES */ 2355 2356async function loadWebsites(includeRejected = false) { 2357 if (checkIfLoginRequired()) 2358 return; 2359 2360 /*const scriptModal = document.getElementById('scriptModal'); 2361 const scriptModalTitle = scriptModal.querySelector('h1'); 2362 setTextContentTranslationByElement(scriptModalTitle, 'pageCode'); 2363 setTextContentTranslationById('copy-info', 'copyScriptLabel'); 2364 const btnCopy = document.getElementById('copy_btn'); 2365 setTextContentTranslationByElement(btnCopy, 'copyCodeToClipboard'); 2366 setTextContentTranslationByElement(btnCopy.nextElementSibling, 'close');*/ 2367 2368 const websitesTable = document.getElementById('websites_table'); 2369 // Reset rows so the deleted-domains toggle can reload cleanly. 2370 const existingBody = websitesTable.querySelector('tbody'); 2371 if (existingBody) existingBody.innerHTML = ''; 2372 2373 const data = { sid: getSid(), projectTypeId: 2, includeRejected: includeRejected }; 2374 const response = await postRequest(endpoint + '/project-domain/list', data); 2375 if (!response.ok) { 2376 console.error('Websites could not be loaded: ' + JSON.stringify(response)); 2377 return; 2378 } 2379 2380 const responseBody = await response.json(); 2381 if (responseBody.statusCode === 0) { 2382 // Toggle ON = deleted-only view; toggle OFF = active-only (backend already 2383 // excludes rejected when includeRejected is false, filter is a belt-and-braces). 2384 const websites = (responseBody.domains || []).filter(w => includeRejected ? w.deleted : !w.deleted); 2385 if (!websites.length) { 2386 websitesTable.classList.add('d-none'); 2387 if (includeRejected) 2388 displayAlert(translations['noDeletedDomains'] || 'You have no deleted domains.', 'info'); 2389 else 2390 showWebsiteCreateLink('websites_table_container'); 2391 return; 2392 } 2393 2394 const tableBody = websitesTable.querySelector('tbody'); 2395 websites.sort((a, b) => a.domainName.localeCompare(b.domainName)); 2396 // websites.sort((a, b) => a.id.localeCompare(b.id));
2397 websites.forEach(website => addWebsiteRow(tableBody, website)); 2398 websitesTable.classList.remove('d-none'); 2399 } else if (responseBody.statusCode === 3) { 2400 deleteCookie('sid'); 2401 deleteCookie('name'); 2402 deleteCookie('email'); 2403 clearLocalStorage(); 2404 alog("Jump to login 14"); 2405 window.location.href = '../views/login.jsp'; 2406 } 2407 2408 //createCopyToClipboard('copy_btn', 'script_content_area'); 2409 2410 function addWebsiteRow(tableBody, website) { 2411 const row = document.createElement('tr'); 2412 2413 // Previously deleted (rejected) domain: render a muted row whose only 2414 // action is to restore it. It does not count toward the plan until restored. 2415 if (website.deleted) { 2416 row.classList.add('table-secondary'); 2417 2418 const delDomainCell = document.createElement('td'); 2419 delDomainCell.textContent = website.domainName; 2420 const badge = document.createElement('span'); 2421 badge.className = 'badge bg-secondary ms-2'; 2422 badge.textContent = translations['deletedLabel'] || 'Deleted'; 2423 delDomainCell.appendChild(badge); 2424 row.appendChild(delDomainCell); 2425 2426 const delTemplateCell = document.createElement('td'); 2427 delTemplateCell.textContent = website.project.name; 2428 delTemplateCell.classList.add('align-middle', 'text-muted'); 2429 row.appendChild(delTemplateCell); 2430 2431 if (!isReseller) 2432 row.appendChild(document.createElement('td')); // empty plan cell 2433 2434 const delActionCell = document.createElement('td'); 2435 delActionCell.classList.add('text-center'); 2436 const reacceptBtn = document.createElement('button'); 2437 reacceptBtn.textContent = translations['reAcceptDomain'] || 'Re-accept'; 2438 reacceptBtn.classList.add('btn', 'btn-outline-success', 'btn-sm'); 2439 reacceptBtn.addEventListener('click', async () => { 2440 if (!await showConfirm(translations['reAcceptDomainConfirm'] || 'Re-accept this domain? It will count toward your plan again.', translations['reAcceptDomain'] || 'Re-accept')) 2441 return; 2442 const rdata = { sid: getSid(), projectTypeId: 2, domainId: website.id }; 2443 const rresp = await postRequest(endpoint + '/project-domain/reaccept', rdata); 2444 if (rresp.ok) { 2445 const rbody = await rresp.json(); 2446 if (rbody.statusCode === 0) { 2447 displayAlert(translations['reAcceptDomainSuccessMsg'] || 'The domain was restored.', 'success'); 2448 setTimeout(() => window.location.reload(), 1500); 2449 } else { 2450 displayAlert('Error: ' + rbody.statusMessage); 2451 } 2452 } else { 2453 displayAlert('Domain could not be re-accepted: Please try later'); 2454 } 2455 }); 2456 delActionCell.appendChild(reacceptBtn); 2457 row.appendChild(delActionCell); 2458 tableBody.appendChild(row); 2459 return; 2460 } 2461 2462 const domainCell = document.createElement('td'); 2463 domainCell.textContent = website.domainName; 2464 row.appendChild(domainCell); 2465 2466 const templateCell = document.createElement('td'); 2467 const templateLink = document.createElement('a'); 2468 templateLink.textContent = website.project.name; 2469 templateLink.href = "template_settings.jsp?id=" + website.project.id + "&pp=w"; 2470 templateLink.classList.add("name-link", "align-middle"); 2471 templateCell.appendChild(templateLink); 2472 row.appendChild(templateCell); 2473 2474 if (!isReseller) { 2475 const subscriptionCell = document.createElement('td'); 2476 subscriptionCell.classList.add('text-center'); 2477 row.appendChild(subscriptionCell); 2478 if (website.st) { 2479 // const subscriptionLink = document.createElement('a'); 2480 // const st = responseBody.packages.find(p => p.id === website.st); 2481 // // if (st.name.includes('(Month)')) 2482 // // st.name = st.name.replace('(Month)', ''); 2483 // 2484 // if (st.durationMonth % 12 === 0) 2485 // subscriptionLink.textContent = st.name + ' (' + translations['annual'] + ')'; 2486 // else 2487 // subscriptionLink.textContent = st.name + ' (' + translations['monthly'] + ')'; 2488 // 2489 // subscriptionLink.href = "subscriptions.jsp?id=" + project.id; 2490 // subscriptionLink.classList.add("name-link", "align-middle"); 2491 // subscriptionCell.appendChild(subscriptionLink); 2492 2493 const st = responseBody.packages.find(p => p.id === website.st); 2494 if (st.durationMonth % 12 === 0) 2495 subscriptionCell.textContent = st.name + ' (' + translations['annual'] + ')'; 2496 else 2497 subscriptionCell.textContent = st.name + ' (' + translations['monthly'] + ')'; 2498 } 2499 } 2500 const actionCell = document.createElement('td'); 2501 actionCell.classList.add('text-center'); 2502 2503 const settingsButton = document.createElement('button'); 2504 settingsButton.textContent = translations['settings']; 2505 settingsButton.classList.add('btn', 'btn-outline-primary', 'btn-sm'); 2506 settingsButton.addEventListener('click', () => { 2507 alog("Jump to website settings " + website.id + " 11"); 2508 window.location.href = "website_settings.jsp?id=" + website.id; 2509 }); 2510 actionCell.appendChild(settingsButton); 2511
2512 // const trashIcon = "<i class='fa fa-trash text-danger'></i>";//<i class="fa-solid fa-trash-can"></i> 2513 2514 2515 const deleteBtn = document.createElement('button'); 2516 // deleteBtn.textContent = translations['delete']; 2517 // deleteBtn.innerHTML = "<i class='fa-solid fa-trash'></i>"; 2518 deleteBtn.innerHTML = "<i class='fa-regular fa-trash-can'></i>"; 2519 deleteBtn.classList.add('btn', 'btn-delete', /*'btn-outline-danger',*/ 'btn-sm', 'ms-2'); 2520 // deleteBtn.setAttribute('data-bs-toggle', 'tooltip'); 2521 // deleteBtn.setAttribute('data-bs-placement', 'top'); 2522 // deleteBtn.setAttribute('title', translations['delete'] || "Delete"); 2523 actionCell.appendChild(deleteBtn); 2524 deleteBtn.addEventListener('click', async () => { 2525 if (!await showConfirm(translations['deleteWebsiteConfirm'] || "Are you sure you want to delete this website?", translations['delete'] || "Delete")) 2526 return; 2527 2528 const data = {sid: getSid(), projectTypeId: 2, domainId: website.id}; 2529 const response = await postRequest(endpoint + '/project-domain/delete', data); 2530 if (response.ok) { 2531 const responseBody = await response.json(); 2532 if (responseBody.statusCode === 0) { 2533 displayAlert(translations['deleteWebsiteSuccessMsg'] ||"The website was deleted.", 'success'); 2534 tableBody.deleteRow(row.rowIndex - 1); 2535 // if (tableBody.rows.length === 0) { 2536 // window.location.reload(); 2537 // } 2538 2539 setTimeout(() => { 2540 window.location.reload(); 2541 }, 3000); 2542 } else { 2543 displayAlert("Error: " + responseBody.statusMessage); 2544 } 2545 } else { 2546 console.error('Website could not be deleted: ' + JSON.stringify(response)); 2547 displayAlert('Website could not be deleted: Please try later'); 2548 } 2549 }); 2550 2551 /*const scriptButton = document.createElement('button'); 2552 setTextContentTranslationByElement(scriptButton, 'pageCode'); 2553 scriptButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 2554 scriptButton.setAttribute('data-bs-toggle', 'modal'); 2555 scriptButton.setAttribute('data-bs-target', '#scriptModal'); 2556 actionCell.appendChild(scriptButton); 2557 scriptButton.addEventListener('click', async () => { 2558 const data = { sid: getSid(), domainId: domain.id }; 2559 const response = await postRequest(endpoint + '/scriptcode', data); 2560 if (response.ok) { 2561 const responseBody = await response.json(); 2562 if (responseBody.statusCode === 0) { 2563 const contentElement = document.getElementById('script_content_area'); 2564 contentElement.textContent = responseBody.content; 2565 } else { 2566 console.log('Script Error Code: ' + responseBody.statusCode); 2567 } 2568 } 2569 });*/ 2570 2571 if (responseBody.ul1) { 2572 const deleteButton = document.createElement('button'); 2573 deleteButton.textContent = translations['delete'] || 'Delete'; 2574 deleteButton.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 2575 actionCell.appendChild(deleteButton); 2576 deleteButton.addEventListener('click', async () => { 2577 const data2 = { sid: getSid(), projectTypeId: 2, projectId: website.project.id }; 2578 const response2 = await postRequest(endpoint + '/project/delete', data2); 2579 if (response2.ok) { 2580 const responseBody2 = await response2.json(); 2581 if (responseBody2.statusCode === 0) 2582 tableBody.deleteRow(row.rowIndex - 1); 2583 } 2584 }); 2585 } 2586 2587 row.appendChild(actionCell); 2588 tableBody.appendChild(row); 2589 } 2590} 2591 2592// Toggle button on the websites page: reloads the list with previously deleted 2593// (rejected) domains shown, so the user can restore them. 2594function setupDeletedDomainsToggle() { 2595 const btn = document.getElementById('btn_toggle_deleted'); 2596 if (!btn) 2597 return; 2598 let showing = false; 2599 btn.addEventListener('click', async () => { 2600 showing = !showing; 2601 await loadWebsites(showing); 2602 btn.textContent = showing 2603 ? (translations['hideDeletedDomains'] || 'Hide deleted domains') 2604 : (translations['showDeletedDomains'] || 'Show deleted domains'); 2605 }); 2606} 2607 2608// Website-settings page banner for a rejected (deleted) domain: persistent warning 2609// with an Accept button that restores the domain via /project-domain/reaccept. 2610function showRejectedDomainBanner(domainId) { 2611 const container = document.getElementById('alert_container'); 2612 if (!container) 2613 return; 2614 2615 const banner = document.createElement('div'); 2616 banner.classList.add('alert', 'alert-warning', 'd-flex', 'justify-content-between', 'align-items-center', 'mb-2'); 2617 banner.setAttribute('role', 'alert'); 2618 2619 const text = document.createElement('span'); 2620 text.textContent = translations['domainRejectedMsg'] 2621 || 'This domain is currently deleted (rejected). It is not served and does not count toward your plan.'; 2622 banner.appendChild(text); 2623 2624 const acceptBtn = document.createElement('button'); 2625 acceptBtn.type = 'button'; 2626 acceptBtn.classList.add('btn', 'btn-success', 'btn-sm', 'ms-3'); 2627 acceptBtn.textContent = translations['reAcceptDomain'] || 'Accept'; 2628 acceptBtn.addEventListener('click', async () => { 2629 if (!await showConfirm(translations['reAcceptDomainConfirm'] || 'Re-accept this domain? It will count toward your plan again.', translations['reAcceptDomain'] || 'Accept')) 2630 return; 2631 const rdata = { sid: getSid(), projectTypeId: 2, domainId: domainId };
2632 const rresp = await postRequest(endpoint + '/project-domain/reaccept', rdata); 2633 if (rresp.ok) { 2634 const rbody = await rresp.json(); 2635 if (rbody.statusCode === 0) { 2636 displayAlert(translations['reAcceptDomainSuccessMsg'] || 'The domain was restored.', 'success'); 2637 setTimeout(() => window.location.reload(), 1500); 2638 } else { 2639 displayAlert('Error: ' + rbody.statusMessage); 2640 } 2641 } else { 2642 displayAlert('Domain could not be re-accepted: Please try later'); 2643 } 2644 }); 2645 banner.appendChild(acceptBtn); 2646 2647 // Sibling of alert_container, not a child: displayAlert() clears the container 2648 // (innerHTML = '') and would otherwise wipe this banner. 2649 container.parentNode.insertBefore(banner, container); 2650} 2651 2652/* TEMPLATE SETTINGS: select-all masters for the region checkbox groups 2653 (IAB TCF advertising + browser storage consents). The master reflects the 2654 group (checked = all on, indeterminate = mixed) and toggling it dispatches 2655 a change event per checkbox so dependent UI (e.g. the US-preferences 2656 section tied to advertising_us) reacts exactly as with a manual click. */ 2657function setupSelectAllToggles() { 2658 wireSelectAll('advertising_select_all', 'advertising_section_body'); 2659 wireSelectAll('site_data_select_all', 'site_data_consent_section'); 2660 2661 function wireSelectAll(masterId, containerId) { 2662 const master = document.getElementById(masterId); 2663 const container = document.getElementById(containerId); 2664 if (!master || !container) 2665 return; 2666 const boxes = () => Array.from(container.querySelectorAll('input[type="checkbox"]')) 2667 .filter(cb => cb.id !== masterId); 2668 const refresh = () => { 2669 const bs = boxes(); 2670 const on = bs.filter(cb => cb.checked).length; 2671 master.checked = bs.length > 0 && on === bs.length; 2672 master.indeterminate = on > 0 && on < bs.length; 2673 }; 2674 master.addEventListener('change', () => { 2675 const target = master.checked; 2676 boxes().forEach(cb => { 2677 if (cb.checked !== target) { 2678 cb.checked = target; 2679 cb.dispatchEvent(new Event('change', { bubbles: true })); 2680 } 2681 }); 2682 refresh(); 2683 }); 2684 container.addEventListener('change', e => { 2685 if (e.target !== master) 2686 refresh(); 2687 }); 2688 refresh(); 2689 } 2690} 2691 2692/* ADMIN: USER SEARCH + IMPERSONATE (admin_users.jsp) */ 2693function loadAdminUsers() { 2694 if (checkIfLoginRequired()) 2695 return; 2696 const form = document.getElementById('admin_user_search_form'); 2697 if (!form) 2698 return; 2699 form.addEventListener('submit', async (e) => { 2700 e.preventDefault(); 2701 await searchAdminUsers(); 2702 }); 2703 // Live search: re-query as the admin types (debounced), no button click needed. 2704 let debounceTimer = null; 2705 form.addEventListener('input', () => { 2706 clearTimeout(debounceTimer); 2707 debounceTimer = setTimeout(() => searchAdminUsers(true), 400); 2708 }); 2709} 2710 2711async function searchAdminUsers(silent) { 2712 // ids go as strings â the backend partial-matches them (LIKE), so "12" finds 123. 2713 const data = { 2714 sid: getSid(), 2715 domain: (document.getElementById('search_domain').value || '').trim(), 2716 projectId: (document.getElementById('search_projectId').value || '').trim(), 2717 email: (document.getElementById('search_email').value || '').trim(), 2718 userId: (document.getElementById('search_userId').value || '').trim(), 2719 }; 2720 if (!data.domain && !data.projectId && !data.email && !data.userId) { 2721 if (!silent) 2722 displayAlert(translations['adminSearchNoCriteria'] || 'Enter at least one search criterion.', 'warning'); 2723 return; 2724 } 2725 // Sequence guard: with debounced live search, an older (slower) response 2726 // must not overwrite the results of a newer query. 2727 const seq = (searchAdminUsers._seq = (searchAdminUsers._seq || 0) + 1); 2728 const response = await postRequest(endpoint + '/user/search', data); 2729 if (seq !== searchAdminUsers._seq) 2730 return; 2731 if (!response.ok) { 2732 displayAlert('Search failed. Please try again.'); 2733 return; 2734 } 2735 const body = await response.json(); 2736 if (body.statusCode !== 0) { 2737 displayAlert('Error: ' + (body.statusMessage || 'not authorized')); 2738 return; 2739 } 2740 renderAdminUsers(body.users || []); 2741} 2742 2743function renderAdminUsers(users) { 2744 const table = document.getElementById('admin_users_table'); 2745 const empty = document.getElementById('admin_users_empty'); 2746 const tbody = table.querySelector('tbody'); 2747 tbody.innerHTML = ''; 2748 if (!users.length) { 2749 table.classList.add('d-none'); 2750 empty.classList.remove('d-none'); 2751 return; 2752 } 2753 empty.classList.add('d-none'); 2754 // Build cells with textContent (never innerHTML) â email/name are user-controlled.
2755 users.forEach(u => { 2756 const row = document.createElement('tr'); 2757 [u.id, u.email || '', u.fullName || ''].forEach(v => { 2758 const td = document.createElement('td'); 2759 td.textContent = v; 2760 row.appendChild(td); 2761 }); 2762 [u.numProjects || 0, u.numDomains || 0].forEach(v => { 2763 const td = document.createElement('td'); 2764 td.className = 'text-center'; 2765 td.textContent = v; 2766 row.appendChild(td); 2767 }); 2768 const actionCell = document.createElement('td'); 2769 actionCell.className = 'text-center'; 2770 const btn = document.createElement('button'); 2771 btn.className = 'btn btn-outline-primary btn-sm'; 2772 btn.textContent = translations['impersonate'] || 'Impersonate'; 2773 if (u.enabled === false) 2774 btn.disabled = true; 2775 btn.addEventListener('click', () => impersonateUser(u)); 2776 actionCell.appendChild(btn); 2777 row.appendChild(actionCell); 2778 tbody.appendChild(row); 2779 }); 2780 table.classList.remove('d-none'); 2781} 2782 2783async function impersonateUser(u) { 2784 const msg = (translations['impersonateConfirm'] || 'Sign in as [email]? You will be logged in as this user.').replace('[email]', u.email); 2785 if (!await showConfirm(msg, translations['impersonate'] || 'Impersonate')) 2786 return; 2787 const response = await postRequest(endpoint + '/user/impersonate', { sid: getSid(), uid: u.id }); 2788 if (!response.ok) { 2789 displayAlert('Impersonation failed. Please try again.'); 2790 return; 2791 } 2792 const body = await response.json(); 2793 if (body.statusCode !== 0 || !body.auth) { 2794 displayAlert('Error: ' + (body.statusMessage || 'could not impersonate')); 2795 return; 2796 } 2797 // Switch the session to the target user's new sid, then load their panel. 2798 createCookie('sid', body.auth); 2799 if (body.content) { 2800 createCookie('name', body.content.fullName || ''); 2801 createCookie('email', body.content.email || ''); 2802 } 2803 clearLocalStorage(); 2804 window.location.href = 'dashboard.jsp'; 2805} 2806 2807/* WEBSITE SETTINGS */ 2808 2809async function loadWebsiteSettings() { 2810 if (checkIfLoginRequired() ) 2811 return; 2812 2813 const websiteSettingsForm = document.getElementById('website_settings_form'); 2814 const subscriptionSection = document.getElementById('subscription_section'); 2815 2816 let packages = null, templates = null, isFirst = false; 2817 const params = new URLSearchParams(window.location.search); 2818 const domainId = params.get("id"); 2819 if (domainId == null) { //Create new website 2820 setTextContentTranslationById('subtitle', 'newWebsite'); 2821 2822 const scriptCard = document.getElementById('script_card'); 2823 scriptCard.classList.add('d-none'); 2824 //projectSettingsForm.addEventListener('submit', createProject); 2825 2826 const data = { sid: getSid(), projectTypeId: 2 }; 2827 const response = await postRequest(endpoint + '/project-domain/start', data); 2828 if (response.ok) { 2829 const responseBody = await response.json(); 2830 if (responseBody.statusCode === 0) { 2831 templates = responseBody.projects; 2832 if (templates.length === 0) { 2833 isFirst = true; 2834 document.getElementById('template_section').classList.add('d-none'); 2835 } else { 2836 fillTemplateSelector(null); 2837 } 2838 2839 //if (!isReseller) { 2840 // packages = responseBody.packages; 2841 //} 2842 2843 subscriptionSection.remove(); 2844 } 2845 } 2846 document.getElementById('save-button').removeAttribute('disabled'); 2847 } else { //Edit an existing project 2848 document.querySelector('label[for="domain"]').hidden = true; 2849 document.getElementById('domain').setAttribute('disabled', 'disabled'); 2850 setTextContentTranslationById('subtitle', 'websiteSettings'); 2851 2852 const data = { sid: getSid(), domainId: domainId, projectType: 2 }; 2853 const response = await postRequest(endpoint + '/project-domain/get', data); 2854 if (response.ok) { 2855 const responseBody = await response.json(); 2856 if (responseBody.statusCode === 0) { 2857 const website = responseBody.domain; 2858 if (website.deleted) 2859 showRejectedDomainBanner(website.id); 2860 websiteSettingsForm.elements['domain'].value = website.domainName; 2861 websiteSettingsForm.elements['cookie_policy'].value = website.settings.cookiePolicyUrl; 2862 websiteSettingsForm.elements['privacy_policy'].value = website.settings.privacyPolicyUrl; 2863 2864 templates = responseBody.projects; 2865 fillTemplateSelector(website.projectId); 2866 2867 //if (!isReseller) { 2868 packages = responseBody.packages; 2869 if (subscriptionSection) { 2870 loadSubscriptionPlan(website); 2871 } 2872 // } 2873 } else { 2874 console.log('Loading Error Code: ' + responseBody.statusCode); 2875 showAlert("The page could not be loaded!", "error"); 2876 } 2877 } 2878 document.getElementById('save-button').removeAttribute('disabled'); 2879 } 2880 2881 websiteSettingsForm.addEventListener('submit', function(event) { 2882 event.preventDefault(); 2883 saveWebsiteSettings(domainId, isFirst); 2884 }, false); 2885 2886 2887 function fillTemplateSelector(selectedValue) { 2888 if (!templates) 2889 return; 2890 2891 const templateSelector = document.getElementById('template');
2892 templates.forEach(template => { 2893 const option = document.createElement('option'); 2894 option.value = template.id; 2895 if (selectedValue && selectedValue === template.id) 2896 option.selected = true; 2897 option.textContent = template.name; 2898 templateSelector.appendChild(option); 2899 }); 2900 2901 // templateSelector.dispatchEvent(new Event('change')); 2902 } 2903 2904 function loadSubscriptionPlan(website) { 2905 const st = packages.find(p => p.id === website.st); 2906 const planContainer = document.getElementById('plan_container'); 2907 if (website.st > 0) { 2908 planContainer.innerHTML = ''; 2909 const planTitle = document.createElement('div'); 2910 planTitle.classList.add('mb-2'); 2911 planTitle.textContent = st.name; 2912 // planTitle.role = 'button'; 2913 planContainer.appendChild(planTitle); 2914 2915 // const chevron = document.createElement('i'); 2916 // chevron.classList.add('fa-solid', 'fa-chevron-right', 'ms-1'); 2917 // planTitle.appendChild(chevron); 2918 2919 // if (st.description) { 2920 // const planDescription = document.createElement('div'); 2921 // planDescription.classList.add('text-muted', 'small'); 2922 // planDescription.innerHTML = st.description; 2923 // planContainer.appendChild(planDescription); 2924 // } 2925 2926 const updateLink = subscriptionSection.querySelector('.btn-link'); 2927 updateLink.textContent = translations['update']; 2928 } else { 2929 planContainer.textContent = translations['notSelected'] || "Not selected"; 2930 } 2931 } 2932} 2933 2934async function saveWebsiteSettings(domainId, isFirst) { 2935 const domain = document.getElementById('domain').value.trim(); 2936 const cookiePolicyUrl = document.getElementById('cookie_policy').value.trim(); 2937 const privacyPolicyUrl = document.getElementById('privacy_policy').value.trim(); 2938 const templateId = document.getElementById('template').value || 0; 2939 2940 // let subscriptionTypeId = 0; 2941 // const planSelector = document.getElementById('subscription_plan'); 2942 // if (planSelector) 2943 // subscriptionTypeId = planSelector.value; 2944 2945 if (domain === '') { 2946 displayAlert('Please enter a domain!'); 2947 return; 2948 } 2949 2950 const data = { 2951 sid: getSid(), 2952 domainId: domainId, 2953 domainName: domain, 2954 cookiePolicyUrl: cookiePolicyUrl, 2955 privacyPolicyUrl: privacyPolicyUrl, 2956 projectId: templateId, 2957 // st: subscriptionTypeId 2958 }; 2959 2960 const url = endpoint + (domainId == null ? '/project-domain/create' : '/project-domain/update'); 2961 const response = await postRequest(url, data); 2962 if (response.ok) { 2963 const responseBody = await response.json(); 2964 if (responseBody.statusCode === 0) { 2965 displayAlert('Website settings saved successfully!', 'success'); 2966 2967 if (isFirst) { 2968 window.location.href = "template_settings.jsp?wid=" + responseBody.domainId; 2969 } else { 2970 window.location.href = "websites.jsp"; 2971 } 2972 } else if (responseBody.statusCode === 2) { 2973 displayAlert(translations['websiteAlreadyExistsError'] || 'Website already exists! Please enter a different domain name.'); 2974 } else { 2975 console.log('Saving Error Code: ' + responseBody.statusCode); 2976 displayAlert("Can't save website: " + responseBody.statusMessage); 2977 } 2978 } else { 2979 console.error('Saving failed: ' + JSON.stringify(response)); 2980 displayAlert("Can't save website: Please try later"); 2981 } 2982} 2983 2984function loadInitialWizard() { 2985 2986} 2987 2988/* DASHBOARD */ 2989 2990// The Shopify app's dashboard (FlexNewStatsPanel) with the template picker kept: a header card (range, template, 2991// export), the KPI tiles, consent over time, consent overview, visits by consent, pageviews, then the region and 2992// source cards. The page state lives in its URL (?id=&range=[&start=&end=]): picking a template reloads the page on 2993// that URL, a range change redraws in place and rewrites it, so a reload or the language menu keeps both. 2994const DASHBOARD_RANGES = [ 2995 ['today', 'Today', () => [moment(), moment()]], 2996 ['yesterday', 'Yesterday', () => [moment().subtract(1, 'days'), moment().subtract(1, 'days')]], 2997 // Monday, like the calendar (firstDay: 1) â moment's startOf('week') is Sunday in its default locale. 2998 ['thisWeek', 'This Week', () => [moment().startOf('isoWeek'), moment()]], 2999 ['last7Days', 'Last 7 Days', () => [moment().subtract(6, 'days'), moment()]], 3000 ['thisMonth', 'This Month', () => [moment().startOf('month'), moment()]], 3001 ['lastMonth', 'Last Month', () => [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]], 3002 ['last30Days', 'Last 30 Days', () => [moment().subtract(29, 'days'), moment()]], 3003 ['thisYear', 'This Year', () => [moment().startOf('year'), moment()]], 3004 ['lastYear', 'Last Year', () => [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')]], 3005 ['last365Days', 'Last 365 Days', () => [moment().subtract(364, 'days'), moment()]], 3006]; 3007const DASHBOARD_DEFAULT_RANGE = 'last30Days'; 3008// Longest custom range, in days (two years; Last 365 Days and This/Last Year fit). A link with a longer one gets the 3009// default: every day is a chart column and a table row. 3010const DASHBOARD_MAX_DAYS = 731; 3011const dashboardState = { projectId: '0', range: DASHBOARD_DEFAULT_RANGE, start: '', end: '' }; 3012 3013async function loadDashboard() { 3014 if (checkIfLoginRequired()) 3015 return; 3016 3017 const params = new URLSearchParams(window.location.search); 3018 const rangePreset = key => DASHBOARD_RANGES.find(r => r[0] === key); 3019 const urlDay = value => { 3020 const m = moment(value || '', 'YYYY-MM-DD', true); 3021 return m.isValid() ? m : null; 3022 }; 3023 3024 // The range from the URL: a preset, or "custom" with two valid days; anything else is the default. 3025 let range = params.get('range'); 3026 let start = urlDay(params.get('start')); 3027 let end = urlDay(params.get('end')); 3028 if (range === 'custom' && start && end && start.isAfter(end)) 3029 [start, end] = [end, start]; 3030 if (range !== 'custom' || !start || !end || end.diff(start, 'days') >= DASHBOARD_MAX_DAYS) { 3031 if (!rangePreset(range)) 3032 range = DASHBOARD_DEFAULT_RANGE; 3033 [start, end] = rangePreset(range)[2](); 3034 } 3035 dashboardState.range = range; 3036 dashboardState.start = start.format('YYYY-MM-DD'); 3037 dashboardState.end = end.format('YYYY-MM-DD'); 3038 // Until the template list confirms it, the URL's template stands: a rewrite of the URL before that (the range 3039 // line below) must not drop ?id=, or a refresh after a failed list load would open All. 3040 dashboardState.projectId = params.get('id') || '0'; 3041 3042 const rangeSelect = document.getElementById('date_range_select'); 3043 for (const [key, label] of DASHBOARD_RANGES) 3044 rangeSelect.appendChild(new Option(translations[key] || label, key)); 3045 rangeSelect.appendChild(new Option(translations['customRange'] || 'Custom Range', 'custom')); 3046 rangeSelect.value = range; 3047 rangeSelect.setAttribute('aria-label', translations['dateRange'] || 'Date range'); 3048 3049 // The calendar opens from the range line, which turns into a button for a custom range (as in the Shopify app). 3050 const picker = $('#dashboard_range_button'); 3051 picker.daterangepicker({ 3052 opens: 'right', 3053 startDate: start, 3054 endDate: end, 3055 maxSpan: { days: DASHBOARD_MAX_DAYS - 1 }, 3056 locale: { 3057 format: 'YYYY-MM-DD', 3058 applyLabel: translations['apply'], 3059 cancelLabel: translations['cancel'], 3060 daysOfWeek: [translations['sundayShort'], translations['mondayShort'], translations['tuesdayS
3060hort'], translations['wednesdayShort'], translations['thursdayShort'], translations['fridayShort'], translations['saturdayShort']], 3061 monthNames: [translations['january'], translations['february'], translations['march'], translations['april'], translations['may'], translations['june'], translations['july'], translations['august'], translations['september'], translations['october'], translations['november'], translations['december']], 3062 firstDay: 1, 3063 } 3064 }, function(s, e) { 3065 dashboardState.start = s.format('YYYY-MM-DD'); 3066 dashboardState.end = e.format('YYYY-MM-DD'); 3067 refreshDashboard(); 3068 }); 3069 const pickerData = picker.data('daterangepicker'); 3070 3071 rangeSelect.addEventListener('change', function() { 3072 if (rangeSelect.value === 'custom') { 3073 // The days on screen stay until the calendar's Apply picks new ones. 3074 dashboardState.range = 'custom'; 3075 showDashboardRange(); 3076 pickerData.show(); 3077 return; 3078 } 3079 // A preset picked while the calendar is still open (keyboard: Custom Range, then another option) closes it 3080 // without applying its days; hide() would apply half-picked ones. 3081 if (pickerData.isShowing) 3082 pickerData.clickCancel(); 3083 const [s, e] = rangePreset(rangeSelect.value)[2](); 3084 dashboardState.range = rangeSelect.value; 3085 dashboardState.start = s.format('YYYY-MM-DD'); 3086 dashboardState.end = e.format('YYYY-MM-DD'); 3087 pickerData.setStartDate(s); 3088 pickerData.setEndDate(e); 3089 refreshDashboard(); 3090 }); 3091 showDashboardRange(); 3092 3093 let projects = null; 3094 try { 3095 projects = await getProjects(); 3096 } catch (e) { 3097 console.error('project/list', e); 3098 } 3099 if (!projects) { 3100 rangeSelect.disabled = true; 3101 document.getElementById('dashboard_kpis').replaceChildren(dashboardBanner(translations['statsLoadFailed'] 3102 || 'Unable to load statistics. Please try refreshing the page.', true)); 3103 return; 3104 } 3105 if (!projects.length) { 3106 document.getElementById('dashboard_controls').classList.add('d-none'); 3107 document.getElementById('date_range_text').classList.add('d-none'); 3108 document.getElementById('dashboard_range_button').classList.add('d-none'); 3109 const card = dashboardEl('section', 'fcd-card'); 3110 const link = dashboardEl('div'); 3111 link.id = 'dashboard_no_templates'; 3112 card.appendChild(link); 3113 document.getElementById('dashboard_kpis').replaceChildren(card); 3114 return showTemplateCreateLink('dashboard_no_templates', !0); 3115 } 3116 3117 const projectDropdown = document.getElementById('select_project'); 3118 projectDropdown.appendChild(new Option(translations['all'] || 'All', '0')); 3119 projects.forEach(project => projectDropdown.appendChild(new Option(project.projectName, String(project.id)))); 3120 // ?id= comes from the templates list's Stats button, or from this page's own reload; an id that is not in the 3121 // list (a stale link, a deleted template) shows All instead of a page that cannot load. 3122 const selectedProjectId = params.get('id'); 3123 projectDropdown.value = [...projectDropdown.options].some(o => o.value === selectedProjectId) ? selectedProjectId : '0'; 3124 dashboardState.projectId = projectDropdown.value; 3125 // A pointer pick reloads at once. Arrow keys (Windows, Linux) and type-ahead fire 'change' on every step of a 3126 // closed select, so after a key press the reload waits for Enter or for the focus to leave. 3127 let keyboardChange = false; 3128 const openTemplate = () => { 3129 if (projectDropdown.value !== dashboardState.projectId) 3130 window.location.assign(dashboardUrl(projectDropdown.value)); 3131 }; 3132 projectDropdown.addEventListener('pointerdown', () => { keyboardChange = false; }); 3133 projectDropdown.addEventListener('keydown', e => { 3134 if (e.key === 'Enter') 3135 openTemplate(); 3136 else if (e.key !== 'Tab') 3137 keyboardChange = true; 3138 }); 3139 projectDropdown.addEventListener('change', () => { if (!keyboardChange) openTemplate(); }); 3140 projectDropdown.addEventListener('blur', openTemplate); 3141 // Back after a template switch can restore this page from the back/forward cache with the picker still on the 3142 // template the user left for; put the controls back to what this page shows. 3143 window.addEventListener('pageshow', e => { 3144 if (e.persisted) { 3145 projectDropdown.value = dashboardState.projectId; 3146 rangeSelect.value = dashboardState.range; 3147 } 3148 }); 3149 3150 const exportDataBtn = document.getElementById('export_data'); 3151 exportDataBtn.addEventListener('click', exportDashboardData); 3152 exportDataBtn.disabled = false;
3153 3154 refreshDashboard(); 3155} 3156 3157// This page's URL for a template, carrying the range; other parameters (?lang=) are kept. 3158function dashboardUrl(projectId) { 3159 const params = new URLSearchParams(window.location.search); 3160 ['id', 'range', 'start', 'end'].forEach(k => params.delete(k)); 3161 if (projectId && projectId !== '0') 3162 params.set('id', projectId); 3163 params.set('range', dashboardState.range); 3164 if (dashboardState.range === 'custom') { 3165 params.set('start', dashboardState.start); 3166 params.set('end', dashboardState.end); 3167 } 3168 return window.location.pathname + '?' + params.toString(); 3169} 3170 3171// The range under the title: plain text for a preset, the calendar button for a custom range. 3172function showDashboardRange() { 3173 const custom = dashboardState.range === 'custom'; 3174 const start = dashboardDateStr(dashboardState.start); 3175 const end = dashboardDateStr(dashboardState.end); 3176 const text = document.getElementById('date_range_text'); 3177 const button = document.getElementById('dashboard_range_button'); 3178 text.querySelector('span').textContent = start + ' â ' + end; 3179 button.querySelector('span').textContent = start + ' - ' + end;
3180 text.classList.toggle('d-none', custom); 3181 button.classList.toggle('d-none', !custom); 3182 history.replaceState(null, '', dashboardUrl(dashboardState.projectId)); 3183} 3184 3185function refreshDashboard() { 3186 showDashboardRange(); 3187 const { projectId, start, end } = dashboardState; 3188 loadProjectStats(projectId, start, end); 3189 loadTrendStats(projectId, start, end); 3190 loadRegionStats(projectId, start, end); 3191 loadSourceStats(projectId, start, end); 3192} 3193 3194// The panel language as a BCP 47 tag (pt_br -> pt-BR) for Intl; <html> carries no lang attribute. 3195function dashboardLocale() { 3196 let tag = (typeof lang === 'string' && lang ? lang : 'en').replace('_', '-'); 3197 const parts = tag.split('-'); 3198 if (parts.length > 1) 3199 tag = parts[0] + '-' + parts[1].toUpperCase(); 3200 try { 3201 return Intl.getCanonicalLocales(tag)[0] || 'en'; 3202 } catch (e) { 3203 return 'en'; 3204 } 3205} 3206 3207function dashboardFormat(value, options) { 3208 try { 3209 return new Intl.NumberFormat(dashboardLocale(), options).format(value); 3210 } catch (e) { 3211 return String(value); 3212 } 3213} 3214 3215function dashboardNumber(value) { 3216 return dashboardFormat(value); 3217} 3218 3219// part / total as a percentage with `digits` decimals; "0%" when there is nothing to divide by. 3220function dashboardPercent(part, total, digits = 1) { 3221 const d = total > 0 ? digits : 0; 3222 return dashboardFormat(total > 0 ? part / total : 0, { style: 'percent', minimumFractionDigits: d, maximumFractionDigits: d }); 3223} 3224 3225// A YYYY-MM-DD day in the panel language. Parsed by moment in local time: new Date('2026-09-24') is UTC 3226// midnight, the previous day anywhere west of UTC. 3227function dashboardDateStr(day) { 3228 const m = moment(day, 'YYYY-MM-DD'); 3229 try { 3230 return m.toDate().toLocaleDateString(dashboardLocale(), { year: 'numeric', month: 'short', day: 'numeric' }); 3231 } catch (e) { 3232 return m.format('YYYY-MM-DD'); 3233 } 3234} 3235 3236// createElement with a class and a text; the text always goes through textContent (house XSS rule). 3237function dashboardEl(tag, className, text) { 3238 const el = document.createElement(tag); 3239 if (className) 3240 el.className = className; 3241 if (text !== undefined && text !== null) 3242 el.textContent = text; 3243 return el; 3244} 3245 3246function dashboardBanner(text, critical) { 3247 const banner = dashboardEl('div', 'fcd-banner' + (critical ? ' fcd-critical' : '')); 3248 banner.setAttribute('role', critical ? 'alert' : 'status'); 3249 const icon = dashboardEl('i', 'fcd-banner-icon fa-solid ' + (critical ? 'fa-circle-exclamation' : 'fa-circle-info')); 3250 icon.setAttribute('aria-hidden', 'true'); 3251 banner.append(icon, dashboardEl('span', null, text)); 3252 return banner; 3253} 3254 3255function dashboardLoadingCard() { 3256 const card = dashboardEl('section', 'fcd-card'); 3257 const inner = dashboardEl('div', 'fcd-loading'); 3258 inner.setAttribute('role', 'status'); 3259 const spinner = dashboardEl('div', 'spinner-border text-secondary'); 3260 spinner.setAttribute('aria-hidden', 'true'); 3261 inner.append(spinner, dashboardEl('div', null, translations['loading'] || 'Loading...')); 3262 card.appendChild(inner); 3263 return card; 3264} 3265 3266function dashboardCardHead(title, badge) { 3267 const head = dashboardEl('div', 'fcd-between'); 3268 head.appendChild(dashboardEl('h3', 'fcd-h-md', title)); 3269 if (badge) 3270 head.appendChild(dashboardEl('span', 'fcd-badge fcd-badge-info', badge)); 3271 return head; 3272} 3273 3274// rows: [label, cells[], indent] 3275function dashboardTable(heads, rows) { 3276 const table = dashboardEl('table', 'fcd-table'); 3277 const thead = document.createElement('thead'); 3278 const headRow = document.createElement('tr'); 3279 heads.forEach(h => headRow.appendChild(dashboardEl('th', null, h))); 3280 thead.appendChild(headRow); 3281 const tbody = document.createElement('tbody'); 3282 rows.forEach(([label, cells, indent]) => { 3283 const tr = document.createElement('tr'); 3284 tr.appendChild(dashboardEl('td', indent ? 'fcd-indent' : null, label)); 3285 cells.forEach(c => tr.appendChild(dashboardEl('td', null, c))); 3286 tbody.appendChild(tr); 3287 }); 3288 table.append(thead, tbody); 3289 const wrap = dashboardEl('div', 'fcd-table-wrap'); 3290 wrap.appendChild(table); 3291 return wrap; 3292} 3293 3294// The region and source tables' number columns: visitors or decisions, accepts, rejects, partials, accept rate. 3295// The rate is accepts over decisions â a partial accept is not an accept here (unlike the trend chart). 3296function dashboardDecisionCells(count, r) { 3297 const accepts = r.accepts || 0, rejects = r.rejects || 0, partials = r.partials || 0; 3298 const decisions = accepts + rejects + partials; 3299 return [ 3300 dashboardNumber(count || 0), 3301 dashboardNumber(accepts), 3302 dashboardNumber(rejects), 3303 dashboardNumber(partials), 3304 decisions > 0 ? dashboardPercent(accepts, decisions, 0) : 'â', 3305 ]; 3306} 3307 3308// Consent decisions over time (data-stats Days[]). The answer is keyed by template NAME with one entry per 3309// template ("All" = every template of the account), so the days are summed across entries. 3310let trendStatsSeq = 0; 3311// The chart is drawn at the card's width (see renderConsentTrend), so a new width â a rotated phone, the sidebar 3312// collapsed or opened â redraws it; the sidebar toggle fires no window resize, hence the observer. 3313let trendRedraw = null, trendResizeTimer = 0; 3314const trendResizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(() => { 3315 clearTimeout(trendResizeTimer); 3316 trendResizeTimer = setTimeout(() => { if (trendRedraw) trendRedraw(); }, 150); 3317}) : null; 3318async function loadTrendStats(projectId, startDate, endDate) { 3319 const container = document.getElementById('trend_stats'); 3320 if (!container) return; 3321 const seq = ++trendStatsSeq; 3322 trendRedraw = null; 3323 container.replaceChildren(); 3324 3325 let body = null; 3326 try { 3327 const response = await postRequest(endpoint + '/cmp/data-stats', 3328 { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }); 3329 if (response && response.ok) 3330 body = await response.json(); 3331 } catch (e) { 3332 console.error('data-stats', e); 3333 } 3334 if (seq !== trendStatsSeq) return; 3335 if (!body || !body.Projects) return; 3336 3337 const series = { days: [], accepts: [], partials: [], rejects: [] }; 3338 const add = (target, metric) =>
3338 ((metric && metric.Days) || []).forEach((v, i) => { 3339 target[i] = (target[i] || 0) + (Number(v) || 0); 3340 }); 3341 for (const name in body.Projects) { 3342 const p = body.Projects[name]; 3343 add(series.accepts, p.consentAccepts); 3344 add(series.partials, p.consentPartiallyAccepts); 3345 add(series.rejects, p.consentRejects); 3346 } 3347 const n = Math.max(series.accepts.length, series.partials.length, series.rejects.length); 3348 if (!n) return; 3349 const first = moment(body.startDate || startDate, 'YYYY-MM-DD'); 3350 for (let i = 0; i < n; i++) 3351 series.days.push(first.clone().add(i, 'days').format('YYYY-MM-DD')); 3352 const dashboard = document.getElementById('dashboard'); 3353 let drawnWidth = dashboard.clientWidth; 3354 container.appendChild(renderConsentTrend(series, startDate, endDate, false)); 3355 trendRedraw = () => { 3356 if (Math.abs(dashboard.clientWidth - drawnWidth) < 8) return; 3357 drawnWidth = dashboard.clientWidth; 3358 const table = container.querySelector('.fcd-table-wrap'); 3359 container.replaceChildren(renderConsentTrend(series, startDate, endDate, !!table && !table.hidden)); 3360 }; 3361 if (trendResizeObserver) 3362 trendResizeObserver.observe(dashboard); 3363} 3364 3365// A port of the Shopify app's FlexConsentTrend: an accept RATE chart and a decision VOLUME chart â never one chart 3366// with a second y-axis â and a table view, so no reading depends on colour alone. 3367function renderConsentTrend(series, startDate, endDate, showTable) { 3368 const NS = 'http://www.w3.org/2000/svg'; 3369 // Drawn at the card's own width, so the 11px axis labels stay 11px instead of shrinking with the viewBox on a 3370 // phone or growing on a wide screen. 3371 const cardWidth = (document.getElementById('dashboard') || {}).clientWidth - 32; 3372 const W = Math.round(Math.max(320, cardWidth || 720)), H = W >= 600 ? 210 : 170, PAD = { l: 44, r: 14, t: 12, b: 26 }; 3373 const plotW = W - PAD.l - PAD.r, plotH = H - PAD.t - PAD.b; 3374 const svgEl = (tag, attrs, text) => { 3375 const el = document.createElementNS(NS, tag); 3376 for (const k in attrs) 3377 el.setAttribute(k, attrs[k]); 3378 if (text !== undefined) 3379 el.textContent = text; 3380 return el; 3381 }; 3382 3383 // A partial accept counts as an accept here: the visitor allowed something. A day with no decisions has no 3384 // rate (null), not 0% â a 0 would read as a collapse in consent rather than an absence of traffic. 3385 const rows = series.days.map((day, i) => { 3386 const a = series.accepts[i] || 0, p = series.partials[i] || 0, r = series.rejects[i] || 0; 3387 const total = a + p + r; 3388 return { day, a, p, r, total, rate: total > 0 ? (a + p) / total * 100 : null }; 3389 }); 3390 const peak = Math.max(1, ...rows.map(x => x.total)); 3391 const magnitude = Math.pow(10, Math.floor(Math.log10(peak))); 3392 let maxTotal = Math.ceil(peak / magnitude) * magnitude; 3393 // The middle gridline is maxTotal / 2, labelled as a whole number: keep maxTotal even. 3394 if (maxTotal % 2) 3395 maxTotal += 1; 3396 const n = rows.length; 3397 const bandW = Math.max(1, Math.min(28, plotW / n * 0.7)); 3398 // Day centres are inset by half a bar, so the first and last bars stay off the y-axis labels. 3399 const innerW = plotW - bandW; 3400 const xOf = i => PAD.l + bandW / 2 + (n === 1 ? innerW / 2 : i * innerW / (n - 1)); 3401 // One hit slot per day, ending halfway to the neighbouring days and kept inside the plot, so the pointer always 3402 // lands on the nearest day. 3403 const slotW = n > 1 ? innerW / (n - 1) : plotW; 3404 const hitL = i => Math.max(PAD.l, xOf(i) - slotW / 2), hitR = i => Math.min(W - PAD.r, xOf(i) + slotW / 2); 3405 // One label every ceil(n/7) days, so the ticks never collide whatever the range. 3406 const tickEvery = Math.max(1, Math.ceil(n / 7)); 3407 let tickFormat = null; 3408 try { 3409 tickFormat = new Intl.DateTimeFormat(dashboardLocale(), { day: 'numeric', month: 'numeric' }); 3410 } catch (e) { /* very old browser: DD/MM below */ } 3411 const tickLabel = day => { 3412 const [, mm, dd] = day.split('-'); 3413 return tickFormat ? tickFormat.format(moment(day, 'YYYY-MM-DD').toDate()) : dd + '/' + mm; 3414 }; 3415 const addTicks = svg => rows.forEach((r, i) => { 3416 if (i % tickEvery === 0) 3417 svg.appendChild(svgEl('text', { class: 'ax', x: xOf(i), y: H - 8, 'text-anchor': 'middle' }, tickLabel(r.day))); 3418 }); 3419 const legend = [ 3420 ['a', translations['accepts'] || 'Accepted', 'var(--fcd-s1)'], 3421 ['p', translations['partiallyAccepts'] || 'Partially accepted', 'var(--fcd-s2)'], 3422 ['r', translations['rejects'] || 'Rejected', 'var(--fcd-s3)'], 3423 ]; 3424 const swatch = color => { 3425 const sw = dashboardEl('span', 'fcd-swatch'); 3426 sw.style.background = color; 3427 return sw; 3428 }; 3429 3430 const card = dashboardEl('section', 'fcd-card fcd-stack fcd-trend'); 3431 const head = dashboardEl('div', 'fcd-between'); 3432 const toggle = dashboardEl('button', 'fcd-link', translations['trendShowTable'] || 'Show table'); 3433 toggle.type = 'button'; 3434 head.append(dashboardEl('h3', 'fcd-h-md', translations['trendTitle'] || 'Consent over time'), toggle); 3435 card.appendChild(head); 3436 3437 if (!rows.some(x => x.total > 0)) { 3438 toggle.hidden = true; 3439 card.appendChild(dashboardEl('p', 'fcd-muted',
3440 translations['trendNoDecisions'] || 'No consent decisions were recorded in this period.')); 3441 } else { 3442 const chart = dashboardEl('div', 'fcd-stack'); 3443 chart.style.gap = '20px'; 3444 3445 // Accept rate: one series, so the heading names it instead of a legend. 3446 const rateBox = dashboardEl('div'); 3447 rateBox.appendChild(dashboardEl('h4', 'fcd-h-sm fcd-muted', translations['acceptRate'] || 'Accept rate')); 3448 const rateSvg = svgEl('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 3449 'aria-label': translations['trendAcceptRateAria'] || 'Accept rate over time' }); 3450 [0, 25, 50, 75, 100].forEach(g => { 3451 const y = PAD.t + plotH - g / 100 * plotH; 3452 rateSvg.append(svgEl('line', { class: 'g', x1: PAD.l, x2: W - PAD.r, y1: y, y2: y }), 3453 svgEl('text', { class: 'ax', x: PAD.l - 8, y: y + 4, 'text-anchor': 'end' }, dashboardPercent(g, 100, 0))); 3454 }); 3455 // The line breaks across days with no decisions instead of drawing through them; a day on its own 3456 // between two gaps is a dot, since a one-point line draws nothing. 3457 const points = rows.map((r, i) => ({ r, x: xOf(i), y: r.rate === null ? null : PAD.t + plotH - r.rate / 100 * plotH })); 3458 let segment = []; 3459 const flush = () => { 3460 if (segment.length === 1) 3461 rateSvg.appendChild(svgEl('circle', { cx: segment[0].x, cy: segment[0].y, r: 2, fill: 'var(--fcd-s1)' })); 3462 else if (segment.length > 1) 3463 rateSvg.appendChild(svgEl('polyline', { fill: 'none', stroke: 'var(--fcd-s1)', 'stroke-width': 2, 3464 'stroke-linecap': 'round', 'stroke-linejoin': 'round', points: segment.map(p => p.x + ',' + p.y).join(' ') })); 3465 segment = []; 3466 }; 3467 points.forEach(p => p.y === null ? flush() : segment.push(p)); 3468 flush(); 3469 const last = points.slice().reverse().find(p => p.y !== null); 3470 if (last) { 3471 rateSvg.appendChild(svgEl('circle', { cx: last.x, cy: last.y, r: 4, fill: 'var(--fcd-s1)', 3472 stroke: 'var(--fcd-surface)', 'stroke-width': 2 })); 3473 const label = svgEl('text', { class: 'ax', x: Math.min(last.x + 8, W - PAD.r), y: last.y - 8, 'text-anchor': 'end' }, 3474 dashboardPercent(last.r.rate, 100, 0)); 3475 label.style.fontWeight = '600'; 3476 rateSvg.appendChild(label); 3477 } 3478 addTicks(rateSvg); 3479 rateBox.appendChild(rateSvg); 3480 3481 // Decision volume: three series, so a legend is always present. 3482 const volumeBox = dashboardEl('div'); 3483 const volumeHead = dashboardEl('div', 'fcd-between'); 3484 const legendEl = dashboardEl('div', 'fcd-legend'); 3485 legend.forEach(([, label, color]) => { 3486 const item = dashboardEl('span', 'fcd-inline fcd-sm fcd-muted'); 3487 item.style.gap = '6px'; 3488 item.append(swatch(color), document.createTextNode(label)); 3489 legendEl.appendChild(item); 3490 }); 3491 volumeHead.append(dashboardEl('h4', 'fcd-h-sm fcd-muted', translations['trendDecisions'] || 'Decisions per day'), legendEl); 3492 const plot = dashboardEl('div'); 3493 plot.style.position = 'relative'; 3494 const volumeSvg = svgEl('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 3495 'aria-label': translations['trendDecisionsAria'] || 'Consent decisions per day' }); 3496 [0, 0.5, 1].forEach(f => { 3497 const y = PAD.t + plotH - f * plotH; 3498 volumeSvg.append(svgEl('line', { class: 'g', x1: PAD.l, x2: W - PAD.r, y1: y, y2: y }), 3499 svgEl('text', { class: 'ax', x: PAD.l - 8, y: y + 4, 'text-anchor': 'end' }, dashboardNumber(Math.round(maxTotal * f)))); 3500 }); 3501 const tip = dashboardEl('div', 'fcd-tip'); 3502 tip.hidden = true; 3503 const showTip = i => { 3504 const r = rows[i], at = xOf(i) / W; 3505 const day = dashboardEl('div', 'fcd-strong', dashboardDateStr(r.day)); 3506 day.style.marginBottom = '4px'; 3507 tip.replaceChildren(day); 3508 legend.forEach(([k, label, color]) => { 3509 const line = dashboardEl('div', 'fcd-inline'); 3510 line.style.gap = '6px'; 3511 const text = dashboardEl('span', null, label + ': '); 3512 text.appendChild(dashboardEl('b', null, dashboardNumber(r[k]))); 3513 line.append(swatch(color), text); 3514 tip.appendChild(line); 3515 }); 3516 // Kept inside the card at both ends: a tip centred over the first or last day would hang off the page. 3517 tip.style.left = (at * 100) + '%'; 3518 tip.style.transform = at < .15 ? 'translate(0,-100%)' : at > .85 ? 'translate(-100%,-100%)' : 'translate(-50%,-100%)'; 3519 tip.hidden = false;
3520 }; 3521 rows.forEach((r, i) => { 3522 const x = xOf(i) - bandW / 2; 3523 let y = PAD.t + plotH; 3524 // Bottom to top in the legend's left-to-right order; a 2px gap between segments instead of a border. 3525 [[r.a, 'var(--fcd-s1)'], [r.p, 'var(--fcd-s2)'], [r.r, 'var(--fcd-s3)']].forEach(([v, color]) => { 3526 if (v <= 0) return; 3527 const h = Math.max(1, v / maxTotal * plotH - 2); 3528 y -= h; 3529 volumeSvg.appendChild(svgEl('rect', { x: x, y: y, width: bandW, height: h, fill: color, rx: 2 })); 3530 y -= 2; 3531 }); 3532 const hit = svgEl('rect', { class: 'hit', x: hitL(i), y: PAD.t, width: hitR(i) - hitL(i), height: plotH }); 3533 hit.addEventListener('mouseenter', () => showTip(i)); 3534 hit.addEventListener('click', () => showTip(i)); 3535 volumeSvg.appendChild(hit); 3536 }); 3537 addTicks(volumeSvg); 3538 volumeSvg.addEventListener('mouseleave', () => { tip.hidden = true; }); 3539 plot.append(volumeSvg, tip); 3540 volumeBox.append(volumeHead, plot); 3541 chart.append(rateBox, volumeBox); 3542 3543 const tableRows = rows.map(r => [dashboardDateStr(r.day), [dashboardNumber(r.a), dashboardNumber(r.p), 3544 dashboardNumber(r.r), r.rate === null ? 'â' : dashboardPercent(r.rate, 100, 0)]]); 3545 const table = dashboardTable([translations['date'] || 'Date', legend[0][1], legend[1][1], legend[2][1], 3546 translations['acceptRate'] || 'Accept rate'], tableRows); 3547 table.hidden = !showTable; 3548 chart.hidden = !!showTable; 3549 if (showTable) 3550 toggle.textContent = translations['trendShowChart'] || 'Show chart'; 3551 toggle.addEventListener('click', () => { 3552 const showTable = table.hidden; 3553 table.hidden = !showTable; 3554 chart.hidden = showTable; 3555 tip.hidden = true; 3556 toggle.textContent = showTable ? (translations['trendShowChart'] || 'Show chart') 3557 : (translations['trendShowTable'] || 'Show table'); 3558 }); 3559 card.append(chart, table); 3560 } 3561 3562 card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', dashboardDateStr(startDate) + ' â ' + dashboardDateStr(endDate))); 3563 return card; 3564} 3565 3566// Export: the server's per-visitor-day CSV for the template (or all templates) and the range on screen. 3567async function exportDashboardData() { 3568 const button = document.getElementById('export_data'); 3569 button.disabled = true; 3570 try { 3571 const data = { sid: getSid(), projectId: dashboardState.projectId, startDate: dashboardState.start, endDate: dashboardState.end }; 3572 const response = await postRequest(endpoint + '/cmp/export-data-stats', data); 3573 if (response === undefined) 3574 return; // 401: postRequest is already logging out 3575 const blob = response && response.ok ? await response.blob() : null; 3576 // text/csv;charset=UTF-8: doGetPost sets the charset before the command sets the type, so the blob type 3577 // carries it and an exact 'text/csv' comparison refused every file. 3578 if (!blob || !blob.type.startsWith('text/csv')) { 3579 showAlert(translations['unknownError'] || 'Unknown error, please try later', 'error'); 3580 return; 3581 } 3582 const disposition = response.headers.get('Content-Disposition'); 3583 const match = disposition && disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); 3584 const a = document.createElement('a'); 3585 a.href = URL.createObjectURL(blob); 3586 a.download = match && match[1] ? match[1].replace(/['"]/g, '') : 'ConsentStats.csv'; 3587 document.body.appendChild(a); 3588 a.click(); 3589 document.body.removeChild(a); 3590 setTimeout(() => URL.revokeObjectURL(a.href), 1000); 3591 } catch (e) { 3592 console.error('export-data-stats', e); 3593 showAlert(translations['unknownError'] || 'Unknown error, please try later', 'error'); 3594 } finally { 3595 button.disabled = false; 3596 } 3597} 3598 3599function showTemplateCreateLink(container, addLink) { 3600 var pc = document.getElementById(container); 3601 if (pc) { 3602 const noProjectsFound = translations['noTemplatesFound'] || 'No templates found.'; 3603 const createProject = translations['clickHereToCreateATemplate'] || 'Click here to create a template'; 3604 pc.innerHTML = noProjectsFound + (addLink ? ' <a href="template_settings.jsp?create=1">' + createProject + '</a>' : ''); 3605 // pc.innerHTML = 'No projects found.' + (addLink ? ' <a href="template_settings.jsp?create=1">Click here to create a project</a>' : ''); 3606 pc.className = 'no-projects-link'; 3607 } 3608} 3609 3610function showWebsiteCreateLink(containerId) { 3611 var tc = document.getElementById(containerId); 3612 if (tc) { 3613 const noWsTxt = translations['noWebsitesFound'] || 'No websites found.'; 3614 const createWsTxt = translations['clickHereToCreateAWebsite'] || 'Click here to create a website'; 3615 tc.innerHTML = noWsTxt + ' <a href="website_settings.jsp">' + createWsTxt + '</a>'; 3616 tc.className = 'no-projects-link'; 3617 } 3618} 3619 3620 3621// Consent-by-region table (data-stats-regions). Renders nothing for the "All" 3622// selector â the endpoint is per-project, matching its auth check. All cell 3623// text goes through textContent (house XSS rule); country names come from the 3624// browser's own Intl.DisplayNames in the panel language, so they need no 3625// entries in the translation sheet. 3626let regionStatsSeq = 0; 3627async function loadRegionStats(projectId, startDate, endDate) { 3628 const container = document.getElementById('region_stats'); 3629 if (!container) return; 3630 // Sequence guard: rapid project/date switches leave earlier requests in 3631 // flight, and a stale response must neither append a second table nor 3632 // fill the card after a switch to "All". Every call â including the "All"
3633 // early return â claims the sequence, so anything older is discarded when 3634 // it lands. The card hides while empty (.fcd > :empty). 3635 const seq = ++regionStatsSeq; 3636 container.replaceChildren(); 3637 if (!projectId || projectId === '0') return; 3638 3639 const response = await postRequest(endpoint + '/cmp/data-stats-regions', 3640 { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }); 3641 if (seq !== regionStatsSeq) return; 3642 // postRequest resolves null on non-2xx (and undefined on 401) â there is 3643 // no .ok to read on the failure it signals. 3644 if (!response || !response.ok) return; 3645 const body = await response.json(); 3646 if (seq !== regionStatsSeq) return; 3647 if (body.statusCode !== 0) return; 3648 3649 const regions = body.regions || []; 3650 const hasAny = regions.length > 0 || body.other || body.unknown; 3651 3652 const card = dashboardEl('section', 'fcd-card fcd-stack-8'); 3653 card.appendChild(dashboardEl('h3', 'fcd-h-sm', translations['regionStatsTitle'] || 'Consent by region')); 3654 3655 if (!hasAny) { 3656 card.appendChild(dashboardEl('p', 'fcd-muted', translations['regionNoData'] 3657 || 'No regional data for this range yet. Locations are recorded from new visits going forward.')); 3658 container.replaceChildren(card); 3659 return; 3660 } 3661 3662 let regionName = (code) => code.toUpperCase(); 3663 try { 3664 const dn = new Intl.DisplayNames([dashboardLocale()], { type: 'region' }); 3665 regionName = (code) => { try { return dn.of(code.toUpperCase()) || code.toUpperCase(); } catch (e) { return code.toUpperCase(); } }; 3666 } catch (e) { /* very old browser: fall back to the bare code */ } 3667 3668 const other = translations['otherRegions'] || 'Other'; 3669 const rows = []; 3670 for (const r of regions) { 3671 rows.push([regionName(r.country), dashboardDecisionCells(r.clients, r), false]); 3672 for (const st of (r.states || [])) 3673 rows.push([st.state === 'other' ? other + ' (' + dashboardNumber(st.statesFolded) + ')' : st.state.toUpperCase(), 3674 dashboardDecisionCells(st.clients, st), true]); 3675 } 3676 if (body.other) 3677 rows.push([other + ' (' + dashboardNumber(body.other.countries) + ')', dashboardDecisionCells(body.other.clients, body.other), false]); 3678 if (body.unknown) 3679 rows.push([translations['unknownRegion'] || 'Unknown', dashboardDecisionCells(body.unknown.clients, body.unknown), false]); 3680 3681 card.appendChild(dashboardTable([ 3682 translations['country'] || 'Country', 3683 translations['visitors'] || 'Visitors', 3684 translations['consentAccepts'] || 'Accepts', 3685 translations['consentRejects'] || 'Rejects', 3686 translations['consentPartiallyAccepts'] || 'Partial', 3687 translations['acceptRate'] || 'Accept rate', 3688 ], rows)); 3689 card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', translations['regionFoldNote'] 3690 || 'Countries and states with only a few visitors are grouped as "Other", so no single visitor can be identified from this table.')); 3691 container.replaceChildren(card); 3692} 3693 3694// --------------------------------------------------------------------------------------------------------- 3695// J90/J91: where each consent decision came from (the J86 `source`: banner / preferences / embed / page / 3696// automatic, "" = not recorded). The panel is the reference front end (J92): the Shopify app and the 3697// WordPress plugin use the same labels, columns and order. 3698function consentSourceLabel(source) { 3699 switch (source) { 3700 case 'banner': return translations['sourceBanner'] || 'Banner'; 3701 case 'preferences': return translations['sourcePreferences'] || 'Preferences screen'; 3702 case 'embed': return translations['sourceEmbed'] || 'Blocked video or embed'; 3703 case 'page': return translations['sourcePage'] || "Your site's own link or button"; 3704 case 'automatic': return translations['sourceAutomatic'] || 'Automatic (Global Privacy Control)'; 3705 default: return translations['sourceNotRecorded'] || 'Not recorded'; 3706 } 3707} 3708 3709// J109/J110: whether the US Global Privacy Control opt-out was in force for the decision. null = not recorded (every row 3710// before 2026-09-25, and saves from older bundles). 3711function consentGpcLabel(gpc) { 3712 if (gpc === true) return translations['gpcApplied'] || 'Applied';
3713 if (gpc === false) return translations['gpcNotApplied'] || 'Not applied'; 3714 return translations['sourceNotRecorded'] || 'Not recorded'; 3715} 3716 3717function consentStateLabel(state) { 3718 switch (state) { 3719 case 'ALL_ACCEPTED': return translations['consentStateAllAccepted'] || 'All Accepted'; 3720 case 'ALL_REJECTED': return translations['consentStateAllRejected'] || 'All Rejected'; 3721 case 'PARTIALLY_ACCEPTED': return translations['consentStatePartiallyAccepted'] || 'Partially Accepted'; 3722 case 'NOT_REQUIRED': return translations['consentStateNotRequired'] || 'Not Required'; 3723 case 'PENDING': return translations['consentStatePending'] || 'Pending'; 3724 default: return state || ''; 3725 } 3726} 3727 3728// Decisions-by-source card (data-stats-sources), beside the region card. Per project, like regions. 3729let sourceStatsSeq = 0; 3730async function loadSourceStats(projectId, startDate, endDate) { 3731 const container = document.getElementById('source_stats'); 3732 if (!container) return; 3733 const seq = ++sourceStatsSeq; 3734 container.replaceChildren(); 3735 if (!projectId || projectId === '0') return; 3736 3737 const response = await postRequest(endpoint + '/cmp/data-stats-sources', 3738 { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }); 3739 if (seq !== sourceStatsSeq) return; 3740 if (!response || !response.ok) return; 3741 const body = await response.json(); 3742 if (seq !== sourceStatsSeq) return; 3743 if (body.statusCode !== 0) return; 3744 3745 const sources = body.sources || []; 3746 const card = dashboardEl('section', 'fcd-card fcd-stack-8'); 3747 card.appendChild(dashboardEl('h3', 'fcd-h-sm', translations['sourceStatsTitle'] || 'Decisions by source')); 3748 3749 if (!sources.length) { 3750 card.appendChild(dashboardEl('p', 'fcd-muted', translations['sourceStatsNoData'] || 'No consent decisions in this date range.')); 3751 container.replaceChildren(card); 3752 return; 3753 } 3754 3755 card.appendChild(dashboardTable([ 3756 translations['source'] || 'Source', 3757 translations['decisions'] || 'Decisions', 3758 translations['consentAccepts'] || 'Accepts', 3759 translations['consentRejects'] || 'Rejects', 3760 translations['consentPartiallyAccepts'] || 'Partial', 3761 translations['acceptRate'] || 'Accept rate', 3762 ], sources.map(r => [consentSourceLabel(r.source), dashboardDecisionCells(r.decisions, r), false]))); 3763 card.appendChild(dashboardEl('p', 'fcd-sm fcd-muted', translations['sourceStatsNote'] 3764 || 'Each visitor counts once per day, under the source of their last decision that day. "Not recorded" means the decision was made before 24 September 2026 or through an older cached version of the banner.')); 3765 container.replaceChildren(card); 3766} 3767 3768// Consent log page (consent-logs): the newest decisions first, 40 at a time, one row per visitor-day with the 3769// last decision of that day. "Load more" pages back with idLimit; the CSV holds the rows loaded so far. 3770const CONSENT_LOG_PAGE = 40; 3771const consentLogView = { rows: [], lastId: 0, seq: 0 }; 3772 3773async function loadConsentLogs() { 3774 const select = document.getElementById('select_project'); 3775 const projects = await getProjects(); 3776 if (!projects || !projects.length) { 3777 showTemplateCreateLink('consent_logs', !0); 3778 return; 3779 } 3780 projects.forEach(project => { 3781 const option = document.createElement('option'); 3782 option.value = project.id; 3783 option.text = project.projectName; 3784 select.appendChild(option); 3785 }); 3786 const wanted = new URLSearchParams(window.location.search).get('id'); 3787 if (wanted && projects.some(p => String(p.id) === wanted)) select.value = wanted; 3788 select.classList.remove('d-none'); 3789 select.addEventListener('change', () => fetchConsentLogs(true)); 3790 document.getElementById('consent_logs_more').addEventListener('click', () => fetchConsentLogs(false)); 3791 document.getElementById('consent_logs_export').addEventListener('click', exportConsentLogsCsv); 3792 await fetchConsentLogs(true); 3793} 3794 3795// The API sends the report day as a bare "YYYY-MM-DD". new Date() reads that as UTC midnight, which is the 3796// PREVIOUS day anywhere west of UTC â so a calendar date is built from its parts, in local time. Formatted in 3797// the panel language, like the dashboard's range picker. 3798function consentLogDate(value) { 3799 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '')); 3800 const d = m ? new Date(+m[1], +m[2] - 1, +m[3]) : new Date(value); 3801 if (isNaN(d.getTime())) return String(value || ''); 3802 let locale; 3803 if (typeof lang === 'string' && lang) { 3804 const p = lang.replace('-', '_').split('_'); 3805 locale = p.length > 1 ? p[0] + '-' + p[1].toUpperCase() : p[0]; 3806 } 3807 try { 3808 return d.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: '2-digit' }); 3809 } catch (e) { 3810 return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: '2-digit' }); 3811 } 3812} 3813 3814async function fetchConsentLogs(reset) { 3815 const select = document.getElementById('select_project'); 3816 const tbody = document.querySelector('#consent_logs_table tbody'); 3817 const more = document.getElementById('consent_logs_more'); 3818 const exportBtn = document.getElementById('consent_logs_export'); 3819 const empty = document.getElementById('consent_logs_empty'); 3820 const seq = ++consentLogView.seq; 3821 if (reset) { 3822 // A new project starts from nothing: no rows, and no buttons that belong to the previous one. 3823 consentLogView.rows = []; 3824 consentLogView.lastId = 0; 3825 tbody.innerHTML = ''; 3826 more.classList.add('d-none'); 3827 exportBtn.classList.add('d-none'); 3828 empty.classList.add('d-none'); 3829 } 3830 more.disabled = true; 3831 const data = { sid: getSid(), projectId: select.value, limit: CONSENT_LOG_PAGE }; 3832 if (!reset && consentLogView.lastId) data.idLimit = consentLogView.lastId; 3833 const failed = (message) => { 3834 more.disabled = false;
3835 showAlert(message || translations['unknownError'] || 'Unknown error, please try later', 'error'); 3836 }; 3837 const response = await postRequest(endpoint + '/cmp/consent-logs', data); 3838 if (seq !== consentLogView.seq) return; 3839 if (!response) return failed(); 3840 let body; 3841 try { 3842 body = await response.json(); 3843 } catch (e) { 3844 body = null; 3845 } 3846 if (seq !== consentLogView.seq) return; 3847 if (!body || body.statusCode !== 0) return failed(body && body.statusMessage); 3848 more.disabled = false; 3849 const logs = body.logs || []; 3850 for (const log of logs) { 3851 consentLogView.rows.push(log); 3852 const tr = document.createElement('tr'); 3853 const cells = [consentLogDate(log.date), log.domain || '', log.clientId || '', consentStateLabel(log.consentState), 3854 consentSourceLabel(log.source), consentGpcLabel(log.gpcApplied), String(log.pageViews || 0)]; 3855 cells.forEach((text, i) => { 3856 const td = document.createElement('td'); 3857 td.textContent = text; 3858 if (i === 2) td.classList.add('text-break', 'small'); 3859 if (i === cells.length - 1) td.classList.add('text-end'); 3860 tr.appendChild(td); 3861 }); 3862 tbody.appendChild(tr); 3863 if (!consentLogView.lastId || log.id < consentLogView.lastId) consentLogView.lastId = log.id; 3864 } 3865 more.classList.toggle('d-none', logs.length < CONSENT_LOG_PAGE);
3866 exportBtn.classList.toggle('d-none', consentLogView.rows.length === 0); 3867 empty.classList.toggle('d-none', consentLogView.rows.length > 0); 3868} 3869 3870function exportConsentLogsCsv() { 3871 const rows = consentLogView.rows; 3872 if (!rows.length) return; 3873 const q = (v) => { 3874 const t = String(v == null ? '' : v); 3875 // Leading = + - @ would run as a formula in a spreadsheet; a quote in front keeps it text. 3876 const safe = /^[=+\-@]/.test(t) ? "'" + t : t; 3877 return '"' + safe.replace(/"/g, '""') + '"'; 3878 }; 3879 const head = [translations['date'] || 'Date', translations['domain'] || 'Domain', translations['clientId'] || 'Client ID', 3880 translations['consentState'] || 'Consent State', translations['source'] || 'Source', 3881 translations['gpcOptOutColumn'] || 'GPC opt-out', translations['pageViewsColumn'] || 'Page Views']; 3882 const lines = [head.map(q).join(',')]; 3883 // The raw ISO day in the file: unambiguous for any spreadsheet, whatever its locale. 3884 for (const log of rows) 3885 lines.push([String(log.date || ''), log.domain || '', log.clientId || '', consentStateLabel(log.consentState), 3886 consentSourceLabel(log.source), consentGpcLabel(log.gpcApplied), log.pageViews || 0].map(q).join(',')); 3887 const blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' }); 3888 const a = document.createElement('a'); 3889 a.href = URL.createObjectURL(blob); 3890 const project = document.getElementById('select_project'); 3891 const name = (project.options[project.selectedIndex] || {}).text || 'consent-log'; 3892 a.download = 'consent-log_' + name.replace(/[^A-Za-z0-9._-]+/g, '_') + '_' + new Date().toISOString().slice(0, 10) + '.csv'; 3893 document.body.appendChild(a); 3894 a.click(); 3895 document.body.removeChild(a); 3896 setTimeout(() => URL.revokeObjectURL(a.href), 1000); 3897} 3898 3899// KPI tiles, consent overview, visits by consent and pageviews (data-stats-simple), as in the Shopify app. 3900let projectStatsSeq = 0; 3901async function loadProjectStats(projectId, startDate, endDate) { 3902 const kpis = document.getElementById('dashboard_kpis'); 3903 const consent = document.getElementById('dashboard_consent'); 3904 const notice = document.getElementById('dashboard_notice'); 3905 // A slow answer for an earlier range must not overwrite the one on screen. 3906 const seq = ++projectStatsSeq; 3907 consent.replaceChildren(); 3908 notice.replaceChildren(); 3909 kpis.replaceChildren(dashboardLoadingCard()); 3910 3911 let body = null; 3912 try { 3913 const response = await postRequest(endpoint + '/cmp/data-stats-simple', 3914 { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }); 3915 if (response && response.ok) 3916 body = await response.json(); 3917 } catch (e) { 3918 // a failed command answers 200 with an empty body, which json() rejects 3919 console.error('data-stats-simple', e); 3920 } 3921 if (seq !== projectStatsSeq) return; 3922 if (!body || body.statusCode !== 0) { 3923 kpis.replaceChildren(dashboardBanner(translations['statsLoadFailed'] 3924 || 'Unable to load statistics. Please try refreshing the page.', true)); 3925 return; 3926 } 3927 3928 // An admin may pick a template of another account: data-stats-simple only covers the caller's own templates, so 3929 // the answer has no entry for it. Say so rather than show zeros and "No data yet". 3930 const stats = body.Projects && body.Projects['' + projectId]; 3931 if (!stats) { 3932 kpis.replaceChildren(dashboardBanner(translations['statsNotAvailable'] || 'Statistics are not available for this template.')); 3933 return; 3934 } 3935 const n = key => Number(stats[key]) || 0; 3936 const visits = n('visitsTotal'), pageViews = n('pageViewsTotal'), requests = n('consentRequests'); 3937 3938 if (visits === 0) 3939 notice.appendChild(dashboardBanner(translations['noDataYet'] || 'No data yet for the selected period.')); 3940 3941 const tiles = dashboardEl('div', 'fcd-grid fcd-grid-4'); 3942 const perVisit = visits > 0 ? pageViews / visits : 0; 3943 [ 3944 [translations['visitsTotal'] || 'Total visitors', visits, translations['visitsTotalSub'] || 'All visitors'], 3945 [translations['visitsNewClient'] || 'New visitors', n('visitsNewClient'), translations['visitsNewClientSub'] || 'First-time'], 3946 [translations['visitsExistingClient'] || 'Returning visitors', n('visitsExistingClient'), translations['visitsExistingClientSub'] || 'Repeat visitors'], 3947 [translations['pageViewsTotal'] || 'Total pageviews', pageViews, 3948 dashboardFormat(perVisit, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' ' + (translations['avgPerVisitShort'] || 'avg')], 3949 ].forEach(([title, value, sub]) => { 3950 const card = dashboardEl('section', 'fcd-card fcd-stack'); 3951 card.append(dashboardEl('div', 'fcd-muted', title), dashboardEl('div', 'fcd-2xl', dashboardNumber(value)), 3952 dashboardEl('div', 'fcd-sm fcd-muted', sub)); 3953 tiles.appendChild(card); 3954 }); 3955 kpis.replaceChildren(tiles); 3956 3957 // Shares of the decisions, not of consentRequests: that counts the visitor-days the banner was shown, and decisions 3958 // made without it that day (the preferences link, an embed) push those shares past 100%. The Shopify app does the 3959 // same (consentDecisionShares). 3960 const decided = n('consentAccepts') + n('consentRejects') + n('consentPartiallyAccepts'); 3961 const overview = dashboardEl('section', 'fcd-card fcd-stack'); 3962 overview.appendChild(dashboardCardHead(translations['consentOverview'] || 'Consent overview', 3963 dashboardNumber(requests) + ' ' + (translations['requestsBadge'] || 'requests'))); 3964 const decisions = dashboardEl('div', 'fcd-stack-12'); 3965 [ 3966 [translations['accepts'] || 'Accepted', 'success', n('consentAccepts')], 3967 [translations['rejects'] || 'Rejected', 'attention', n('consentRejects')], 3968 [translations['partiallyAccepts'] || 'Partially accepted', 'warning', n('consentPartiallyAccepts')], 3969 ].forEach(([label, tone, value]) => {
3970 const row = dashboardEl('div', 'fcd-stack-8'); 3971 const line = dashboardEl('div', 'fcd-between'); 3972 const left = dashboardEl('div', 'fcd-inline'); 3973 left.append(dashboardEl('span', 'fcd-badge fcd-badge-' + tone, label), dashboardEl('span', 'fcd-strong', dashboardNumber(value))); 3974 line.append(left, dashboardEl('span', 'fcd-muted', dashboardPercent(value, decided))); 3975 const share = decided > 0 ? value / decided * 100 : 0; 3976 const bar = dashboardEl('div', 'fcd-progress'); 3977 bar.setAttribute('role', 'progressbar'); 3978 bar.setAttribute('aria-label', label); 3979 bar.setAttribute('aria-valuemin', '0'); 3980 bar.setAttribute('aria-valuemax', '100'); 3981 bar.setAttribute('aria-valuenow', String(Math.round(share))); 3982 const fill = dashboardEl('div'); 3983 fill.style.width = share + '%'; 3984 bar.appendChild(fill); 3985 row.append(line, bar); 3986 decisions.appendChild(row); 3987 }); 3988 overview.appendChild(decisions); 3989 3990 const shareItem = (className, label, value, total, valueClass) => { 3991 const item = dashboardEl('div', className); 3992 item.append(dashboardEl('div', 'fcd-sm fcd-muted', label), dashboardEl('div', valueClass, dashboardNumber(value)), 3993 dashboardEl('div', 'fcd-sm fcd-muted', dashboardPercent(value, total))); 3994 return item; 3995 }; 3996 const withConsent = translations['withConsent'] || 'With consent'; 3997 const withoutConsent = translations['withoutConsent'] || 'Without consent'; 3998 const noPreference = translations['noPreference'] || 'No preference'; 3999 4000 const byConsent = dashboardEl('section', 'fcd-card fcd-stack'); 4001 byConsent.appendChild(dashboardCardHead(translations['visitsByConsent'] || 'Visits by consent')); 4002 // notRequired counts visitor-days (CmpCreateReports adds one per row), so it is a share of visits, as in the Shopify 4003 // app's Visits by Consent card. 4004 const visitsGrid = dashboardEl('div', 'fcd-grid fcd-grid-4'); 4005 visitsGrid.append( 4006 shareItem('fcd-box fcd-stack-8', withConsent, n('visitsWithConsent'), visits, 'fcd-xl'), 4007 shareItem('fcd-box fcd-stack-8', withoutConsent, n('visitsWithoutConsent'), visits, 'fcd-xl'), 4008 shareItem('fcd-box fcd-stack-8', noPreference, n('visitsNoPreference'), visits, 'fcd-xl'), 4009 shareItem('fcd-box fcd-stack-8', translations['notRequired'] || 'Consent not required', n('notRequired'), visits, 'fcd-xl')); 4010 byConsent.appendChild(visitsGrid); 4011 4012 const pageViewsCard = dashboardEl('section', 'fcd-card fcd-stack'); 4013 pageViewsCard.appendChild(dashboardCardHead(translations['pageviewsAnalysis'] || 'Pageviews', 4014 dashboardNumber(pageViews) + ' ' + (translations['totalBadge'] || 'total'))); 4015 const pageViewsGrid = dashboardEl('div', 'fcd-grid fcd-grid-3'); 4016 pageViewsGrid.append( 4017 shareItem('fcd-stack-8', withConsent, n('pageViewsWithConsent'), pageViews, 'fcd-lg'), 4018 shareItem('fcd-stack-8', withoutConsent, n('pageViewsWithoutConsent'), pageViews, 'fcd-lg'), 4019 shareItem('fcd-stack-8', noPreference, n('pageViewsWithNoPreference'), pageViews, 'fcd-lg')); 4020 pageViewsCard.appendChild(pageViewsGrid); 4021 4022 consent.replaceChildren(overview, byConsent, pageViewsCard); 4023} 4024 4025async function loadProjectStatsPrev(dashboardForm, projectId, startDate, endDate) { 4026 const data = { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }; 4027 const responseDataStats = await postRequest(endpoint + '/cmp/data-stats', data); 4028 // const responseDataStats = await fetch(endpoint + '/cmp/data-stats?sid=' + getSid()); 4029 4030 if (responseDataStats.ok) { 4031 const responseBodyDataStats = await responseDataStats.json(); 4032 if (responseBodyDataStats.statusCode === 0) { 4033 const firstKey = Object.keys(responseBodyDataStats.Projects)[0]; 4034 const projectStats = responseBodyDataStats.Projects[firstKey]; 4035 4036 const stats = { 4037 visitsTotal: { 4038 visitsTotal: projectStats['visitsTotal'].Total 4039 }, 4040 visitorVisits: { 4041 visitsNewClient: projectStats['visitsNewClient'].Total, 4042 visitsExistingClient: projectStats['visitsExistingClient'].Total, 4043 }, 4044 visitorConsentState: { 4045 visitsWithConsent: projectStats['visitsWithConsent'].Total, 4046 visitsWithoutConsent: projectStats['visitsWithoutConsent'].Total, 4047 visitsNoPreference: projectStats['visitsNoPreference'].Total, 4048 }, 4049 pageViews: { 4050 pageViewsWithNoPreference: projectStats['pageViewsWithNoPreference'].Total, 4051 pageViewsWithConsent: projectStats['pageViewsWithConsent'].Total, 4052 pageViewsWithoutConsent: projectStats['pageViewsWithoutConsent'].Total, 4053 pageViewsTotal: projectStats['pageViewsTotal'].Total, 4054 }, 4055 visitorPreferences: { 4056 notRequired: projectStats['notRequired'].Total, 4057 requests: projectStats['consentRequests'].Total, 4058 accepts: projectStats['consentAccepts'].Total, 4059 rejects: projectStats['consentRejects'].Total, 4060 partiallyAccepts: projectStats['consentPartiallyAccepts'].Total, 4061 cancels: projectStats['consentCancels'].Total, 4062 }, 4063 }; 4064 4065 dashboardForm.innerHTML = ''; 4066 for (const groupKey in stats) { 4067 const groupValue = stats[groupKey]; 4068 const div1 = document.createElement('div'); 4069 div1.classList.add('row'); 4070 dashboardForm.appendChild(div1); 4071 let count = Object.keys(groupValue).length; 4072 const width = 12 / count; 4073 const borderColor = 'border-color' + count; 4074 4075 for (const key in groupValue) { 4076 let value = groupValue[key]; 4077 4078 const div2 = document.createElement('div'); 4079 div2.classList.add('col-lg-' + width, 'px-3', 'py-2'); 4080 div1.appendChild(div2); 4081 4082 const card = document.createElement('div'); 4083 card.classList.add('card', 'card-std', borderColor); 4084 div2.appendChild(card); 4085 4086 const div3 = document.createElement('div'); 4087 div3.classList.add('stat-box', 'd-flex', 'flex-column', 'flex-wrap'); 4088 card.appendChild(div3); 4089 4090 const spanTitle = document.createElement('span'); 4091 spanTitle.classList.add('title'); 4092 // spanTitle.innerText = rfCmpData[key];
4093 spanTitle.innerText = keyToTitle(key); 4094 div3.appendChild(spanTitle); 4095 4096 const div4 = document.createElement('div'); 4097 div4.classList.add('d-flex', 'justify-content-center'); 4098 div3.appendChild(div4); 4099 4100 const [val, ending, color] = numberToText(value); 4101 4102 const spanValue = document.createElement('span'); 4103 spanValue.classList.add('value'); 4104 spanValue.innerText = val; 4105 div4.appendChild(spanValue); 4106 4107 const spanValue2 = document.createElement('span'); 4108 spanValue2.classList.add('value'); 4109 if (color.length > 0) 4110 spanValue2.classList.add(color); 4111 spanValue2.innerText = ending; 4112 4113 div4.appendChild(spanValue2); 4114 } 4115 } 4116 // console.log("Page loaded"); 4117 } else { 4118 // showAlert("The page could not be loaded!", "error"); 4119 } 4120 } else { 4121 showAlert("The page could not be loaded!", "error"); 4122 } 4123 4124 // var rfCmpData = {a: "Page Display", b: "Visitor Count", c: "Deneme", d: "Test"}; 4125 // 4126 // var stats = { 4127 // grp1: { 4128 // a: 12005436750, b: 240506 4129 // }, 4130 // grp2: { 4131 // c: 1500400, d: 5600, a: 10460 4132 // }, 4133 // grp3: { 4134 // c: 1500400, d: 600, a: 10460, b: 980778032 4135 // } 4136 // }; 4137} 4138 4139function getDateRange() { 4140 const dateRangeDropDown = document.getElementById('select_date_range'); 4141 const dateRangeValue = dateRangeDropDown.value; 4142 // let date = new Date(); 4143 const now = new Date(); 4144 const dayOfWeek = now.getDay(); 4145 const dayOfMonth = now.getDate(); 4146 // let startDate, endDate; 4147 let startDate = new Date(); 4148 let endDate = new Date() 4149 switch (dateRangeValue) { 4150 case 'today': 4151 startDate = now; 4152 endDate = now; 4153 break; 4154 4155 case 'yesterday': 4156 startDate.setDate(now.getDate() - 1); 4157 endDate.setDate(now.getDate() - 1); 4158 break; 4159 4160 case 'thisWeek': 4161 startDate.setDate(dayOfMonth - dayOfWeek); 4162 break; 4163 4164 case 'last7Days': 4165 startDate.setDate(now.getDate() - 6); 4166 break; 4167 4168 case 'thisMonth': 4169 startDate.setDate(1); 4170 break; 4171 4172 case 'last30Days': 4173 startDate.setDate(now.getDate() - 29); 4174 break; 4175 4176 case 'thisYear': 4177 startDate.setDate(1); 4178 startDate.setMonth(0); 4179 break; 4180 4181 case 'last365Days': 4182 startDate.setDate(now.getDate() - 364); 4183 break; 4184 4185 case 'allTime': 4186 startDate = null; 4187 endDate = null; 4188 break; 4189 4190 case 'custom': 4191 startDate = null; 4192 endDate = null; 4193 break; 4194 4195 default: 4196 startDate = null; 4197 endDate = null; 4198 break; 4199 } 4200 4201 console.log('Date Range: ' + dateRangeValue); 4202 console.log('Start: ' + startDate); 4203 console.log('End: ' + endDate); 4204 console.log(''); 4205 return [startDate, endDate]; 4206} 4207 4208 4209/* USER */ 4210 4211async function loadUserSettings(userSettingsForm) { 4212 if (checkIfLoginRequired() ) 4213 return; 4214 4215 // document.querySelectorAll('.btn-success').forEach(btn => { 4216 // // btn.textContent = translations['save']; 4217 // setTextContentTranslationByElement(btn, 'save'); 4218 // }); 4219 // 4220 // document.querySelectorAll('.btn-cancel').forEach(btn => { 4221 // // btn.textContent = translations['cancel']; 4222 // setTextContentTranslationByElement(btn, 'cancel'); 4223 // }); 4224 4225 4226 const languagePicker = document.getElementById('languages');
4227 languages.forEach(function (language) { 4228 languagePicker.add(new Option(language.name, language.id)); 4229 }); 4230 4231 document.querySelectorAll('input[type="password"]').forEach(input => { 4232 const btnEye = input.nextElementSibling; 4233 btnEye.addEventListener('click', (event) => { 4234 event.preventDefault(); 4235 const passwordIcon = btnEye.children[0]; 4236 if (input.type === "password") { 4237 input.type = "text"; 4238 passwordIcon.classList.remove("fa-eye"); 4239 passwordIcon.classList.add("fa-eye-slash"); 4240 } else { 4241 input.type = "password"; 4242 passwordIcon.classList.remove("fa-eye-slash"); 4243 passwordIcon.classList.add("fa-eye"); 4244 } 4245 }); 4246 }); 4247 4248 const data = { sid: getSid() }; 4249 const response = await postRequest(endpoint + '/user/get', data); 4250 if (response.ok) { 4251 const responseBody = await response.json(); 4252 if (responseBody.statusCode === 0) { 4253 document.getElementById('full_name').value = responseBody.fullName; 4254 document.getElementById('email').value = responseBody.email; 4255 document.getElementById('company').value = responseBody.companyName; 4256 // language 4257 4258 const invoiceInfo = responseBody.invoiceInfo; 4259 document.getElementById('nameOnInvoice').value = invoiceInfo.nameOnInvoice; 4260 document.getElementById('accountType').value = invoiceInfo.business ? 'business' : 'personal'; 4261 document.getElementById('accountType')[0].style.display = 'none'; 4262 document.getElementById('address1').value = invoiceInfo.addressLine1; 4263 document.getElementById('address2').value = invoiceInfo.addressLine2; 4264 document.getElementById('city').value = invoiceInfo.city; 4265 document.getElementById('postCode').value = invoiceInfo.postCode; 4266 4267 const countryPicker = document.getElementById('countries'); 4268 fetch(endpoint + '/countries') 4269 .then((response) => response.json()) 4270 .then((responseBody) => { 4271 if (responseBody.statusCode === 0) { 4272 responseBody.countries.forEach(async function(country) { 4273 countryPicker.add(new Option(country.name, country.id)); 4274 }); 4275 countryPicker.value = invoiceInfo.countryId; 4276 countryPicker[0].style.display = 'none'; 4277 } else 4278 console.log("Countries Error: " + responseBody.statusCode); 4279 }); 4280 document.getElementById('vatNumber').value = invoiceInfo.vatNumber; 4281 4282 4283 //// const languagesResponse = await fetch(endpoint + '/languages'); 4284 //// if(languagesResponse.ok){ } 4285 // const languageId = responseBody.languageId; 4286 // const languagePicker = document.getElementById('languages'); 4287 // fetch(endpoint + '/languages') 4288 // .then((response) => response.json()) 4289 // .then((responseBody) => { 4290 // if (responseBody.statusCode === 0) { 4291 // languages = responseBody.languages;
4292 // responseBody.languages.forEach(async function(language) { 4293 // languagePicker.add(new Option(language.name, language.id)); 4294 // }); 4295 // languagePicker.value = languageId; 4296 // // languagePicker[0].style.display = 'none'; 4297 // } else 4298 // console.log("Languages Error: " + responseBody.statusCode); 4299 // }); 4300 4301 languagePicker.value = responseBody.languageId; 4302 4303 userSettingsForm.addEventListener('submit', async (event) => { 4304 event.preventDefault(); 4305 saveUserSettings(responseBody.email); 4306 }); 4307 } else { 4308 console.log('User Get Error Code: ' + responseBody.errorCode + '\nMessage: ' + responseBody.errorMessage); 4309 } 4310 } else { 4311 console.error('User Get failed: ' + JSON.stringify(response)); 4312 } 4313} 4314 4315function saveUserSettings(currentEmail) { 4316 const name = document.getElementById('full_name').value.trim(); 4317 // const email = document.getElementById('email').value.trim(); 4318 const company = document.getElementById('company').value.trim(); 4319 const languageId = parseInt(document.getElementById('languages').value); 4320 const nameOnInvoice = document.getElementById('nameOnInvoice').value.trim(); 4321 const accountType = document.getElementById('accountType').value.trim(); 4322 const address1 = document.getElementById('address1').value.trim(); 4323 const address2 = document.getElementById('address2').value.trim(); 4324 const city = document.getElementById('city').value.trim(); 4325 const postCode = document.getElementById('postCode').value.trim(); 4326 const countryId = parseInt(document.getElementById('countries').value); 4327 const vatNumber = document.getElementById('vatNumber').value.trim(); 4328 4329 const data = { 4330 sid: getSid(), 4331 fullName: name, 4332 companyName: company, 4333 languageId: languageId, 4334 invoiceInfo: { 4335 nameOnInvoice: nameOnInvoice, 4336 business: accountType === 'business', 4337 addressLine1: address1, 4338 addressLine2: address2, 4339 city: city, 4340 postCode: postCode, 4341 countryId: countryId, 4342 vatNumber: vatNumber 4343 } 4344 }; 4345 4346 // if (email !== currentEmail) 4347 // data.updatedEmail = email; 4348 4349 postRequest(endpoint + '/user/update', data) 4350 .then((response) => { 4351 if (response.ok) { 4352 response.json().then((responseBody) => { 4353 if (responseBody.statusCode === 0) { 4354 createCookie("name", removeAfterLast(name, ' ')); 4355 const bundle = languages.find(l => l.id === languageId).bundle; 4356 createCookie("lang", bundle); 4357 let url = new URL(window.location.href); 4358 url.searchParams.delete('lang'); 4359 window.location.href = url.toString(); 4360 4361 // window.location.reload(); 4362 } else { 4363 showAlert("Error: " + responseBody.errorMessage, "error"); 4364 } 4365 }); 4366 } else { 4367 console.error('User Save failed: ' + JSON.stringify(response)); 4368 } 4369 }); 4370} 4371 4372async function updatePassword() { 4373 event.preventDefault(); 4374 4375 const currentPwd = document.getElementById('currentPassword').value.trim(); 4376 const newPwd = document.getElementById('newPassword').value.trim(); 4377 const confirmPwd = document.getElementById('confirmPassword').value.trim(); 4378 if (!currentPwd || !newPwd || !confirmPwd) { 4379 showAlert("Password cannot be empty!", "warning"); 4380 } else if (newPwd === currentPwd) { 4381 showAlert("Your new password cannot be the same as your current one!", "warning"); 4382 } else if (newPwd === confirmPwd) { 4383 // const data = { sid: getSid(), oldPassword: currentPwd, newPassword: newPwd }; 4384 // const response = await postRequest(endpoint + '/updatepassword', data); 4385 const response = await fetch(endpoint + '/updatepassword/' + getSid() + '/' + currentPwd + '/' + newPwd); 4386 if (response.ok) { 4387 const responseBody = await response.json(); 4388 if (responseBody.errorCode === 0) { 4389 logout(); 4390 } else if (responseBody.errorCode === 4) { 4391 showAlert("Current password is wrong!", "error"); 4392 } else if (responseBody.errorCode === 9) { 4393 showAlert("Your password must be at least 6 characters!", "warning"); 4394 } else if (responseBody.errorCode === 10) { 4395 // document.getElementById('password_req').style.border = "1px solid red"; 4396 showAlert("The new password does not comply with password requirements!", "warning"); 4397 } else { 4398 console.log('Uodate Password Error Code: ' + responseBody.errorCode); 4399 } 4400 } else {
4401 console.error('Uodate Password failed: ' + JSON.stringify(response)); 4402 } 4403 } else if (newPwd !== confirmPwd) { 4404 showAlert("Password confirmation doesn't match the new password!", "warning"); 4405 } 4406} 4407 4408 4409/* SUBSCRIPTIONS */ 4410 4411async function loadSubscriptions() { 4412 if (checkIfLoginRequired()) 4413 return; 4414 4415 const subscriptionsElement = document.getElementById('subscriptions'); 4416 4417 const modal = document.getElementById('modal_cancel_subscription'); 4418 const modalTitle = modal.getElementsByClassName('modal-title')[0]; 4419 setTextContentTranslationByElement(modalTitle, 'cancelSubscription'); 4420 // modal.addEventListener('show.bs.modal', function (event) { }); 4421 4422 setTextContentTranslationById('btn_cancel_subscription', 'cancelSubscription'); 4423 const btnClose = modal.getElementsByClassName('btn-secondary')[0]; 4424 setTextContentTranslationByElement(btnClose, 'close'); 4425 4426 const data = { sid: getSid(), projectTypeId: 2 }; 4427 const response = await postRequest(endpoint + '/package/list', data); 4428 if (response.ok) { 4429 const responseBody = await response.json(); 4430 if (responseBody.statusCode === 0) { 4431 /*if (isReseller) { 4432 window.location.href = "payments.jsp"; 4433 return; 4434 }*/ 4435 4436 const subscriptionsContainer = subscriptionsElement.parentElement; 4437 subscriptionsContainer.classList.remove('d-none'); 4438 4439 const packages = responseBody['packages']; 4440 const domains = responseBody['domains']; 4441 4442 // let freeSubscriptionExists = false, paidSubscriptionExists = false; 4443 const subscriptions = responseBody['subscriptions']; 4444 const trialSubscription = subscriptions.find(s => s.type === "Trial" && new Date(s.end) >= new Date()); 4445 if (trialSubscription) { 4446 subscriptions.splice(subscriptions.indexOf(trialSubscription), 1); 4447 subscriptions.unshift(trialSubscription); 4448 } 4449 4450 const freeSubscription = subscriptions.find(s => s.type === "Free"); 4451 if (freeSubscription) { 4452 subscriptions.splice(subscriptions.indexOf(freeSubscription), 1); 4453 subscriptions.unshift(freeSubscription); 4454 } 4455 subscriptions?.forEach(subscription => { 4456 if (subscription.state === "0") 4457 return; 4458 4459 const plan = packages.find(p => p.id === subscription.typeId); 4460 if (plan.price > 0 && new Date(subscription.end) < new Date()) 4461 return; 4462 if (subscription.type === "Trial" && new Date(subscription.end) < new Date()) 4463 return; 4464 4465 if (subscriptionsElement.children.length > 0) { 4466 const lineDivider = document.createElement('hr'); 4467 lineDivider.classList.add('my-3'); 4468 subscriptionsElement.appendChild(lineDivider); 4469 } 4470 4471 const tempDomains = domains.slice(); 4472 if (trialSubscription && subscription.type !== "Trial") { 4473 const domainTrial = domains.find(d => d.subscriptionId === trialSubscription.id); 4474 if (domainTrial) { 4475 const index = tempDomains.indexOf(domainTrial); 4476 tempDomains.splice(index, 1); 4477 } 4478 } 4479 addSubscriptionElement(subscriptionsElement, subscription, packages, subscriptions.length, tempDomains); 4480 }); 4481 4482 const btnSave = document.getElementById('btn_save');
4483 btnSave.addEventListener('click', async (event) => { 4484 event.preventDefault(); 4485 const selectedWebsites = {}; 4486 domains.forEach(domain => { 4487 selectedWebsites[domain.id] = 0; 4488 }); 4489 const websiteSelectors = subscriptionsElement.querySelectorAll('select[id^="websites_"]'); 4490 const websiteSelectorsKeys = Object.keys(websiteSelectors); 4491 for (let i = 0; i < websiteSelectorsKeys.length; i++) { 4492 const websiteSelector = websiteSelectors[websiteSelectorsKeys[i]]; 4493 const subscriptionId = parseInt(websiteSelector.id.split('_')[1]); 4494 // const subscriptionId = websiteSelector.subscriptionId; 4495 const selectedWebsiteId = parseInt(websiteSelector.value); 4496 if (selectedWebsiteId > 0) { 4497 if (selectedWebsites[selectedWebsiteId] > 0) { 4498 showAlert(translations['websiteAlreadySelected'] || 'You can only assign one website to one subscription!', "warning"); 4499 return; 4500 } else 4501 // selectedWebsites[selectedWebsiteId] = typeId; 4502 selectedWebsites[selectedWebsiteId] = subscriptionId; 4503 } 4504 } 4505 4506 // const selectedWebsiteIds = Object.values(selectedWebsites); 4507 // const uniqueWebsiteIds = new Set(selectedWebsiteIds); 4508 // if (selectedWebsiteIds.length !== uniqueWebsiteIds.size) { 4509 // alert(translations['websiteAlreadySelected'] || 'Website already selected'); 4510 // return; 4511 // } 4512 4513 const data = { sid: getSid(), domains: selectedWebsites, projectTypeId: 2 }; 4514 const response = await postRequest(endpoint + '/package/update', data); 4515 if (response.ok) { 4516 const responseBody = await response.json(); 4517 if (responseBody.statusCode === 0) { 4518 window.location.reload(); 4519 } else if (responseBody.statusCode === 27) { //ER_DOMAIN_TRIAL_EXPIRED 4520 showAlert(translations['domainTrialExpiredMsg'] || 'The trial period for this website has already expired. Please select another website.', "warning"); 4521 } else { 4522 showAlert("Error: " + responseBody.statusMessage, "error"); 4523 } 4524 } 4525 }); 4526 btnSave.disabled = false; 4527 4528 const packagesContainer = document.getElementById('plans_container'); 4529 const subscription = { id: 0, typeId: -1 }; 4530 showBuyPackages(subscription, packages, packagesContainer); 4531 4532 const buyNewPlanBtn = document.getElementById('btn_buy_new_plan'); 4533 buyNewPlanBtn.addEventListener('click', (event) => { 4534 event.preventDefault(); 4535 4536 if (packagesContainer.classList.contains('expanded')) { 4537 packagesContainer.classList.remove('expanded'); 4538 setTimeout(() => { 4539 setTextContentTranslationByElement(buyNewPlanBtn.firstElementChild, 'buyPlan'); 4540 buyNewPlanBtn.lastElementChild.innerHTML = '<i class="fa-solid fa-chevron-right"></i>'; 4541 buyNewPlanBtn.classList.replace('btn-outline-secondary', 'btn-success'); 4542 }, 500); 4543 } else { 4544 packagesContainer.classList.add('expanded'); 4545 setTextContentTranslationByElement(buyNewPlanBtn.firstElementChild, 'hidePlans'); 4546 buyNewPlanBtn.lastElementChild.innerHTML = '<i class="fa-solid fa-chevron-down"></i>'; 4547 buyNewPlanBtn.classList.replace('btn-success', 'btn-outline-secondary'); 4548 4549 setTimeout(() => { 4550 const pageBody = document.getElementsByTagName('body')[0]; 4551 const scrollOptions = { 4552 top: pageBody.scrollHeight, 4553 behavior: 'smooth' 4554 }; 4555 pageBody.scroll(scrollOptions); 4556 }, 500); 4557 } 4558 }); 4559 } 4560 } 4561} 4562 4563function addSubscriptionElement(subscriptionsElement, subscription, packages, numSubscriptions, domains) { 4564 const subscriptionElement = document.createElement('div'); 4565 subscriptionElement.setAttribute('id', 'subscription_' + subscription.id); 4566 subscriptionElement.classList.add('p-2'); 4567 subscriptionsElement.appendChild(subscriptionElement); 4568 4569 const plan = packages.find(p => p.id === subscription.typeId); 4570 const duration = "(" + (plan.durationMonth === 1 ? translations['monthly'] : translations['annual']) + ")"; 4571 4572 const titleRow = document.createElement('div'); 4573 titleRow.classList.add('d-flex', 'justify-content-between', 'align-items-center'); 4574 subscriptionElement.appendChild(titleRow); 4575 4576 const title = document.createElement('h6'); 4577 title.classList.add('fw-bold'); 4578 title.textContent = subscription.type + " " + duration; 4579 titleRow.appendChild(title); 4580 4581 const websiteContainer = document.createElement('div'); 4582 websiteContainer.classList.add('d-flex', 'align-items-center', 'mb-2'); 4583 subscriptionElement.appendChild(websiteContainer); 4584 4585 const websiteIcon = document.createElement('i'); 4586 websiteIcon.classList.add('fa-solid', 'fa-globe', 'me-2'); 4587 websiteContainer.appendChild(websiteIcon); 4588 4589 const websiteLabel = document.createElement('span'); 4590 websiteLabel.classList.add('card-text'); 4591 websiteLabel.textContent = translations['website'] || 'Website'; 4592 websiteContainer.appendChild(websiteLabel); 4593 4594 const websiteSelector = document.createElement('select'); 4595 websiteSelector.classList.add('form-select', 'form-select-sm', 'w-auto', 'ms-3'); 4596 websiteSelector.style.minWidth = '200px'; 4597 websiteSelector.setAttribute('aria-label', translations['websites'] || 'Websites'); 4598 // websiteSelector.setAttribute('id', 'websites_' + subscription.typeId); 4599 websiteSelector.setAttribute('id', 'websites_' + subscription.id); 4600 // websiteSelector.subscriptionId = subscription.id; 4601 websiteContainer.appendChild(websiteSelector); 4602 4603 const optionSelect = document.createElement('option'); 4604 optionSelect.value = "0"; 4605 optionSelect.textContent = translations['select'] || 'Select'; 4606 // websiteSelector.insertBefore(option, websiteSelector.firstChild); 4607 websiteSelector.appendChild(optionSelect); 4608 4609 const domain = domains.find(d => d.subscriptionId === subscription.id); 4610 if (domain) { 4611 for (let i = 0; i < domains.length; i++) { 4612 const option = document.createElement('option'); 4613 option.value = domains[i].id; 4614 option.textContent = domains[i].domainName; 4615 websiteSelector.appendChild(option); 4616 } 4617 websiteSelector.value = domain.id; 4618 4619 if (subscription.type === 'Trial') 4620 optionSelect.remove(); 4621 } else { 4622 let isSelected = false;
4623 for (let i = 0; i < domains.length; i++) { 4624 const option = document.createElement('option'); 4625 option.value = domains[i].id; 4626 option.textContent = domains[i].domainName; 4627 4628 if (!isSelected && !domains[i].isSelected && domains[i].st === subscription.typeId) { 4629 option.selected = true; 4630 isSelected = true; 4631 domains[i].isSelected = true; 4632 } 4633 websiteSelector.appendChild(option); 4634 } 4635 4636 if (isSelected && subscription.type === 'Trial') 4637 optionSelect.remove(); 4638 } 4639 4640 if (subscriptionsPageViewExceededList.includes(subscription.id)) { 4641 // if (translations['pageviewLimitMsgForDomain']) { 4642 let msg = translations['pageviewLimitMsgForDomain'] || "Your website [domain] has reached the monthly page view limit on the [planName] plan. To ensure uninterrupted service and accurate consent tracking, we recommend upgrading your plan."; 4643 const domainLink = document.createElement('a'); 4644 // domainLink.href = "#" + websiteSelector.getAttribute('id'); 4645 domainLink.href = "#" + subscriptionElement.getAttribute('id'); 4646 domainLink.textContent = domain.domainName; 4647 domainLink.classList.add('alert-link'); 4648 // domainLink.onclick = function() { 4649 // const websiteSelector = document.getElementById('websites_' + subscription.id); 4650 // websiteSelector.scrollIntoView({ behavior: 'smooth', block: 'center' }); 4651 // websiteSelector.focus(); 4652 // } 4653 4654 const planLink = document.createElement('a'); 4655 // planLink.href = "#" + websiteSelector.getAttribute('id'); 4656 planLink.href = "#" + subscriptionElement.getAttribute('id'); 4657 // planLink.textContent = plan.name; 4658 planLink.textContent = subscription.type; 4659 planLink.classList.add('alert-link'); 4660 // planLink.onclick = function() { 4661 // subscriptionElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); 4662 // subscriptionElement.focus(); 4663 // } 4664 4665 msg = msg.replace('[domain]', domainLink.outerHTML).replace('[planName]', planLink.outerHTML); 4666 displayAlert(msg); 4667 // } else { 4668 // displayAlert(translations['pageviewLimitMsg'] + "<br>Plan: " + subscription.type + "<br>Website: " + domain.domainName); 4669 // } 4670 } 4671 4672 const periodContainer = document.createElement('div'); 4673 periodContainer.classList.add('d-flex', 'align-items-center', 'mb-2'); 4674 subscriptionElement.appendChild(periodContainer); 4675 4676 const periodIcon = document.createElement('i'); 4677 periodIcon.classList.add('fa-regular', 'fa-calendar', 'me-2'); 4678 periodContainer.appendChild(periodIcon); 4679 4680 const periodLabel = document.createElement('span'); 4681 periodLabel.classList.add('card-text'); 4682 periodLabel.textContent = (translations['subscriptionPeriod'] || 'Subscription period') + ":"; 4683 periodContainer.appendChild(periodLabel); 4684 4685 const periodText = document.createElement('span'); 4686 periodText.classList.add('card-text', 'ms-2'); 4687 periodText.textContent = subscription.start + " - " + subscription.end; 4688 periodContainer.appendChild(periodText); 4689 4690 4691 const remainingContainer = document.createElement('div'); 4692 remainingContainer.classList.add('d-flex', 'align-items-center', 'mb-2'); 4693 subscriptionElement.appendChild(remainingContainer); 4694 4695 const remainingIcon = document.createElement('i'); 4696 remainingIcon.classList.add('fa-regular', 'fa-clock', 'me-2'); 4697 remainingContainer.appendChild(remainingIcon); 4698 4699 let remainTime = new Date(subscription.end) - new Date(), remainStr; 4700 if (remainTime < 0) 4701 remainTime = 0; 4702 const remainDays = Math.floor(remainTime / (1000 * 60 * 60 * 24)); 4703 if (remainDays > 30) { 4704 const remainMonths = Math.floor(remainDays / 30); 4705 remainStr = translations[remainMonths > 1 ? 'remainMonthsCount' : 'remainMonthCount']; 4706 remainStr = remainStr.replace('[monthCount]', remainMonths.toString()); 4707 } else { 4708 remainStr = translations[remainDays > 1 ? 'remainDaysCount' : 'remainDayCount']; 4709 remainStr = remainStr.replace('[dayCount]', remainDays.toString()); 4710 } 4711 4712 const text2 = document.createElement('span'); 4713 text2.classList.add('card-text'); 4714 if (subscription.nextTypeId === 0) { 4715 setTextContentTranslationByElement(text2, 'subscriptionCancelMessage'); 4716 text2.classList.add('text-danger'); 4717 } else if (subscription.nextTypeId === subscription.typeId) { 4718 setTextContentTranslationByElement(text2, 'subscriptionRenewMessage'); 4719 text2.classList.add('text-muted'); 4720 } 4721 text2.textContent += " " + remainStr; 4722 remainingContainer.appendChild(text2); 4723 4724 4725 const descriptionContainer = document.createElement('div'); 4726 subscriptionElement.appendChild(descriptionContainer); 4727 4728 const descriptionTitle = document.createElement('div'); 4729 descriptionTitle.textContent = translations['planFeatures'] || 'Plan Features'; 4730 descriptionTitle.role = 'button';
4731 descriptionTitle.classList.add('mt-2'); 4732 descriptionContainer.appendChild(descriptionTitle); 4733 4734 const chevronParent = document.createElement('span'); 4735 chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>'; 4736 chevronParent.classList.add('ms-1'); 4737 descriptionTitle.appendChild(chevronParent); 4738 4739 const descriptionTextContainer = document.createElement('div'); 4740 descriptionTextContainer.classList.add('expandable'); 4741 descriptionContainer.appendChild(descriptionTextContainer); 4742 4743 const descriptionText = document.createElement('p'); 4744 descriptionText.classList.add('card-text', 'bg-light', 'p-2', 'rounded-3', 'text-muted'); 4745 // const planDescription = plan.description.replace(/<br\s*\/?>/gi, '<br>'); 4746 // const planDescription = translations[plan.name + 'PlanDescription'] || plan.description; 4747 let planName = (plan.name.split(' ')[0]).toLowerCase(); 4748 planName = planName.replace('trial', 'plus'); 4749 descriptionText.innerHTML = translations[planName + 'PlanDescription'] || plan.description; 4750 descriptionTextContainer.appendChild(descriptionText); 4751 4752 descriptionTitle.onclick = function() { 4753 descriptionTextContainer.classList.toggle('expanded'); 4754 4755 if (descriptionTextContainer.classList.contains('expanded')) 4756 chevronParent.innerHTML = '<i class="fa-solid fa-chevron-down"></i>'; 4757 else 4758 chevronParent.innerHTML = '<i class="fa-solid fa-chevron-right"></i>'; 4759 } 4760 4761 const isFreePlan = Math.floor(plan.price) === 0; 4762 if (isFreePlan) 4763 return; 4764 4765 if (subscription.id > 0 && subscription.nextTypeId > 0) { 4766 // const btnCancelContainer = document.createElement('div'); 4767 // btnCancelContainer.classList.add('d-flex', 'justify-content-start', 'mt-1'); 4768 // subscriptionElement.appendChild(btnCancelContainer); 4769 4770 const btnCancel = document.createElement('a'); 4771 btnCancel.classList.add('danger-cancel-link'); 4772 // btnCancel.classList.add('link-secondary', 'link-offset-3-hover', 'link-underline', 'link-underline-opacity-0', 'link-underline-opacity-75-hover');
4772 //link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover 4773 setTextContentTranslationByElement(btnCancel, 'cancelSubscription'); 4774 btnCancel.setAttribute('data-bs-toggle', 'modal'); 4775 btnCancel.setAttribute('data-bs-target', '#modal_cancel_subscription'); 4776 // btnCancelContainer.appendChild(btnCancel); 4777 titleRow.appendChild(btnCancel); 4778 btnCancel.addEventListener('click', event => { 4779 event.preventDefault(); 4780 4781 const subscriptionInfo = document.getElementById('modal_subscription_info'); 4782 subscriptionInfo.textContent = translations['subscription'] + ": " + subscription.type + ' (' + subscription.start + ' - ' + subscription.end + ')'; 4783 4784 const modalBodyText1 = subscriptionInfo.nextElementSibling; 4785 setTextContentTranslationByElement(modalBodyText1, 'cancelSubscriptionText1'); 4786 4787 const modalBodyText2 = modalBodyText1.nextElementSibling; 4788 setTextContentTranslationByElement(modalBodyText2, 'cancelSubscriptionText2'); 4789 4790 const btnConfirm = document.getElementById('btn_cancel_subscription'); 4791 btnConfirm.textContent = "Cancel Subscription"; 4792 setTextContentTranslationByElement(btnConfirm, 'cancelSubscription'); 4793 btnConfirm.onclick = async function () { 4794 event.preventDefault(); 4795 4796 btnConfirm.textContent = 'Cancelling...'; 4797 setTextContentTranslationByElement(btnConfirm, 'cancelling'); 4798 const spinner = generateSpinner(); 4799 btnConfirm.appendChild(spinner); 4800 4801 const data = {sid: getSid(), subscriptionId: subscription.id}; 4802 const response = await postRequest(endpoint + '/payment/unsubscribe', data); 4803 if (response.ok) { 4804 const responseBody = await response.json(); 4805 if (responseBody.statusCode === 0) { 4806 // window.location.reload(); 4807 // if (!packagescontainer.classList.contains('d-none')) { 4808 // btnChangePlan.click(); 4809 // } 4810 4811 // const packageCards = packagescontainer.querySelectorAll('.card-body'); 4812 // packageCards.forEach(card => { 4813 // card.classList.remove('d-none'); 4814 // }); 4815 4816 setTextContentTranslationByElement(text2, 'subscriptionCancelMessage'); 4817 // subscriptionCard.classList.add('border-danger'); 4818 text2.classList.remove('text-muted'); 4819 text2.classList.add('text-danger'); 4820 4821 const modal = document.getElementById('modal_cancel_subscription'); 4822 const modal2 = bootstrap.Modal.getOrCreateInstance(modal); 4823 modal2.hide(); 4824 } else { 4825 showAlert("Error: " + responseBody.statusMessage, "error"); 4826 } 4827 } else { 4828 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 4829 } 4830 4831 removeSpinner(spinner); 4832 4833 // packagescontainer.classList.add('d-none'); 4834 // setTextContentTranslationByElement(btnChangePlan, 'changePlan'); 4835 // btnChangePlan.classList.add('d-none'); 4836 }; 4837 4838 4839 }); 4840 } 4841} 4842 4843function showBuyPackages(subscription, packages, packagesContainer) { 4844 const tabHeader = document.createElement('ul'); 4845 tabHeader.classList.add('nav', 'nav-tabs', 'nav-fill', 'px-3'); 4846 packagesContainer.appendChild(tabHeader); 4847 4848 const annualTabHeader = document.createElement('li'); 4849 annualTabHeader.classList.add('nav-item'); 4850 tabHeader.appendChild(annualTabHeader); 4851 4852 const annualTabLink = document.createElement('a'); 4853 annualTabLink.classList.add('nav-link', 'text-black', 'active', 'pe-auto'); 4854 setTextContentTranslationByElement(annualTabLink, 'annual'); 4855 4856 annualTabHeader.appendChild(annualTabLink); 4857 4858 4859 const monthlyTabHeader = document.createElement('li'); 4860 monthlyTabHeader.classList.add('nav-item'); 4861 tabHeader.appendChild(monthlyTabHeader); 4862 4863 const monthlyTabLink = document.createElement('a'); 4864 monthlyTabLink.classList.add('nav-link', 'text-black'); 4865 setTextContentTranslationByElement(monthlyTabLink, 'monthly'); 4866 4867 monthlyTabHeader.appendChild(monthlyTabLink); 4868 4869 4870 const tabContentContainer = document.createElement('div'); 4871 tabContentContainer.classList.add('tab-content'); 4872 packagesContainer.appendChild(tabContentContainer); 4873 4874 const annualTabContent = document.createElement('div'); 4875 annualTabContent.classList.add('tab-pane', 'fade', 'show', 'active'); 4876 annualTabContent.id = 'annual'; 4877 tabContentContainer.appendChild(annualTabContent); 4878 4879 const subAnnualTabContent = document.createElement('div'); 4880 subAnnualTabContent.classList.add('d-flex', 'row', 'justify-content-center', 'py-3'); 4881 annualTabContent.appendChild(subAnnualTabContent); 4882 4883 const monthlyTabContent = document.createElement('div'); 4884 monthlyTabContent.classList.add('tab-pane', 'fade'); 4885 monthlyTabContent.id = 'monthly'; 4886 tabContentContainer.appendChild(monthlyTabContent); 4887 4888 const subMonthlyTabContent = document.createElement('div'); 4889 subMonthlyTabContent.classList.add('d-flex', 'row', 'justify-content-center', 'py-3'); 4890 monthlyTabContent.appendChild(subMonthlyTabContent); 4891 4892 annualTabLink.addEventListener('click', event => { 4893 event.preventDefault(); 4894 annualTabLink.classList.add('active'); 4895 monthlyTabLink.classList.remove('active'); 4896 annualTabContent.classList.add('show', 'active'); 4897 monthlyTabContent.classList.remove('show', 'active'); 4898 }); 4899 4900 monthlyTabLink.addEventListener('click', event => { 4901 event.preventDefault(); 4902 monthlyTabLink.classList.add('active'); 4903 annualTabLink.classList.remove('active'); 4904 monthlyTabContent.classList.add('show', 'active'); 4905 annualTabContent.classList.remove('show', 'active'); 4906 }); 4907 4908 const groups = {}; 4909 // // packages = packages.sort((a, b) => a.duration - b.duration);
4910 packages.forEach(plan => { 4911 if (!plan.enabled) 4912 return; 4913 4914 if (parseFloat(plan.price) === 0) 4915 return; 4916 4917 plan.name = plan.name.split(' (')[0]; 4918 plan.externalInfoLink = plan.externalInfoLink.trim(); 4919 4920 const tabPanelContent = plan.duration === 12 ? subAnnualTabContent : subMonthlyTabContent; 4921 4922 const cardCol = document.createElement('div'); 4923 cardCol.classList.add('col', 'card-col'); 4924 tabPanelContent.appendChild(cardCol); 4925 4926 const planCard = document.createElement('div'); 4927 planCard.classList.add('plan-card'); 4928 cardCol.appendChild(planCard); 4929 4930 const planCardBody = document.createElement('div'); 4931 planCardBody.classList.add('plan-card-body'); 4932 planCard.appendChild(planCardBody); 4933 4934 const headerText = document.createElement('span'); 4935 headerText.classList.add('header-text'); 4936 headerText.textContent = plan.name; 4937 planCardBody.appendChild(headerText); 4938 4939 const priceContainer = document.createElement('div'); 4940 priceContainer.classList.add('price-container'); 4941 4942 const priceElement = document.createElement('div'); 4943 priceElement.classList.add('paid'); 4944 4945 let oldPriceContainer = null; 4946 if (plan.price > 0) { 4947 let price = parseFloat(plan.price); 4948 let oldPrice = 0; 4949 let discountPercentage = 0; 4950 4951 if (plan.discountRate) { 4952 price = Math.round(plan.price * (1 - plan.discountRate) * 100) / 100; 4953 oldPrice = parseFloat(plan.price); 4954 discountPercentage = Math.floor(plan.discountRate * 100); 4955 } 4956 4957 if (plan.durationMonth === 12 && plan.otherGroupMemberId) { 4958 const monthlyPlan = packages.find(p => p.id === plan.otherGroupMemberId); 4959 if (monthlyPlan) { 4960 if (oldPrice > 0) { 4961 const twelveMonthsPrice = Math.round(monthlyPlan.price * 12 * 100) / 100; 4962 oldPrice = Math.max(oldPrice, twelveMonthsPrice); 4963 } else { 4964 oldPrice = Math.round(monthlyPlan.price * 12 * 100) / 100; 4965 } 4966 discountPercentage = Math.floor((1 - price / oldPrice) * 100); 4967 } 4968 } 4969 plan.price = price; 4970 4971 const priceParts = price.toFixed(2).split('.'); 4972 const currency = currencyMap[plan.currency] || plan.currency; 4973 priceElement.innerHTML = `<span class="currency">${currency}</span>${priceParts[0]}<span class="cent">.${priceParts[1]}</span>`; 4974 4975 if (oldPrice > 0) { 4976 const oldPriceElement = document.createElement('span'); 4977 oldPriceElement.classList.add('old-price'); 4978 oldPriceElement.innerHTML = `${currency}${oldPrice.toFixed(2)}`; 4979 4980 const discountSvg = `<svg width="20" height="35" viewBox="0 0 21 39" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#FF8300" clip-rule="evenodd" fill-rule="evenodd" d="M18.0808 0C16.5161 0 15.0277 0.67567 13.9977 1.8535L1.34101 16.3268C-0.480869 18.4102 -0.442043 21.5311 1.4311 23.5685L14.0068 37.247C15.034 38.3642 16.4822 39 17.9998 39H20.2725V0L18.0808 0ZM11.361 23.4843C13.5614 23.2888 15.1868 21.3465 14.9912 19.146C14.7957 16.9455 12.8534 15.3202 10.6529 15.5157C8.45245 15.7113 6.82711 17.6536 7.02263 19.8541C7.21815 22.0545 9.16049 23.6799 11.361 23.4843Z"></path></svg>`; 4981 const discountElement = document.createElement('div'); 4982 let discountText = translations['discountText'] || "[Number]% off"; 4983 discountText = discountText.replace('[Number]', discountPercentage.toString()); 4984 discountElement.innerHTML = 4985 `<div class="discount-container"> 4986 <span class="d-flex">${discountSvg}</span> 4987 <span class="discount-text t-body-2">${discountText}</span> 4988 </div>`; 4989 4990 oldPriceContainer = document.createElement('div'); 4991 oldPriceContainer.classList.add('d-flex'); 4992 oldPriceContainer.appendChild(oldPriceElement); 4993 oldPriceContainer.appendChild(discountElement); 4994 priceContainer.appendChild(oldPriceContainer); 4995 } 4996 4997 } else if (plan.externalInfoLink) { 4998 priceElement.innerHTML = `<span class="custom-price">Custom Price</span>`; 4999 } else { 5000 priceElement.innerHTML = `<span class="free">Free</span>`; 5001 plan.price = 0; 5002 } 5003 5004 priceContainer.appendChild(priceElement); 5005 planCardBody.appendChild(priceContainer); 5006 5007 const cardText = document.createElement('div'); 5008 cardText.classList.add('card-text', 'mb-5', 'text-center', 'align-middle'); 5009 let planName = (plan.name.split(' ')[0]).toLowerCase(); 5010 plan.description = translations[planName + 'PlanDescription'] || plan.description; 5011 cardText.innerHTML = plan.description; 5012 planCardBody.appendChild(cardText); 5013 5014 const button = document.createElement('button'); 5015 button.id = `btn_${plan.id}`; 5016 button.classList.add('btn', 'btn-subscribe'); 5017 if (plan.id !== subscription.nextTypeId) { 5018 button.classList.add('btn-success'); 5019 setTextContentTranslationByElement(button, plan.externalInfoLink ? 'contactUs' : 'buy'); 5020 } else { 5021 button.classList.add('btn-secondary'); 5022 button.innerText = translations['subscribed']; 5023 button.disabled = true; 5024 } 5025 planCardBody.appendChild(button); 5026 button.addEventListener('click', event => { 5027 event.preventDefault(); 5028 localStorage.setItem('buying_package', JSON.stringify(plan)); 5029 localStorage.setItem('subscription', JSON.stringify(subscription)); 5030 window.location.href = 'payment.jsp?subject=subscription&sid=' + subscription.id + '&pid=' + plan.id; 5031 }); 5032 }); 5033 5034 if (subAnnualTabContent.children.length === 0) { 5035 monthlyTabLink.click(); 5036 annualTabHeader.style.display = 'none'; 5037 monthlyTabHeader.style.display = 'none'; 5038 } else if (subMonthlyTabContent.children.length === 0) { 5039 annualTabLink.click(); 5040 monthlyTabHeader.style.display = 'none'; 5041 annualTabHeader.style.display = 'none'; 5042 } 5043} 5044 5045 5046/* PAYMENT */ 5047 5048function loadPayment() { 5049 if (checkIfLoginRequired()) 5050 return; 5051 const params = new URLSearchParams(window.location.search); 5052 if (params.has('subject')) { 5053 const btnCreditCard = document.getElementById('credit_card'); 5054 setTextContentTranslationByElement(btnCreditCard.childNodes[btnCreditCard.childNodes.length - 1], 'debitOrCredit
5054Card'); 5055 setTextContentTranslationById('btn_free', 'cancelSubscription'); 5056 setTextContentTranslationById('go_back', 'goBackSubscriptions'); 5057 5058 const subject = params.get('subject'); 5059 if (subject === 'subscription') { 5060 loadSubscriptionPayment(); 5061 } else if (subject === 'checkout') { 5062 loadCheckoutPayment(); 5063 } 5064 } else { 5065 alog("Jump to subscriptions 18"); 5066 window.location.href = 'subscriptions.jsp'; 5067 } 5068} 5069 5070 function loadSubscriptionPayment() { 5071 const packageJson = localStorage.getItem('buying_package'); 5072 if (packageJson === undefined || packageJson === '') { 5073 alog("Jump to subscriptions 19"); 5074 window.location.href = 'subscriptions.jsp'; 5075 return; 5076 } 5077 5078 const plan = JSON.parse(packageJson); 5079 const subscriptionJson = localStorage.getItem('subscription'); 5080 if (subscriptionJson === undefined || subscriptionJson === '') { 5081 alog("Jump to subscriptions 20"); 5082 window.location.href = 'subscriptions.jsp'; 5083 return; 5084 } 5085 const subscription = JSON.parse(subscriptionJson); 5086 5087 const title = document.getElementById("title"); 5088 if (parseFloat(plan.price) === 0) { 5089 setTextContentTranslationByElement(title, 'cancelSubscriptionOfType'); 5090 title.textContent = title.textContent.replace("[Package]", subscription.type); 5091 } else { 5092 setTextContentTranslationByElement(title, 'choosePaymentMethod'); 5093 } 5094 5095 const paymentItemList = document.getElementById("paymentItemList"); 5096 addPaymentItem(paymentItemList, plan, true); 5097 5098 function subscribe(event) { 5099 if (document.getElementById('btn_spinner')) 5100 return; 5101 const spinner = generateSpinner(); 5102 event.currentTarget.appendChild(spinner); 5103 5104 const url = endpoint + "/payment/pay"; 5105 const data = { 5106 sid: getSid(), 5107 subscriptionId: subscription.id, 5108 subscriptionTypeId: plan.id, 5109 }; 5110 postRequest(url, data).then(async response => { 5111 if (response.ok) { 5112 const responseBody = await response.json(); 5113 if (responseBody.statusCode === 0) { 5114 alog("Jump to " + responseBody.content + " 21"); 5115 window.location.href = responseBody.content; 5116 removeSpinner(spinner); 5117 } else { 5118 removeSpinner(spinner); 5119 showAlert("Error: " + responseBody.statusMessage, "error"); 5120 } 5121 } else { 5122 removeSpinner(spinner); 5123 showAlert("Error: Response failed!", "error"); 5124 } 5125 }); 5126 } 5127 5128 if (plan.price > 0) { 5129 const btnPaypal = document.getElementById("paypal"); 5130 btnPaypal.classList.remove('d-none'); 5131 btnPaypal.addEventListener('click', subscribe); 5132 5133 const btnCreditCard = document.getElementById("credit_card"); 5134 btnCreditCard.classList.remove('d-none'); 5135 btnCreditCard.addEventListener('click', subscribe); 5136 } else { 5137 const btnFree = document.getElementById("btn_free");
5138 btnFree.classList.remove('d-none'); 5139 btnFree.addEventListener('click', subscribe); 5140 } 5141 5142 // document.getElementById("paypal").href = getReturnAddress(package.buyLink); 5143 // document.getElementById("credit_card").href = getReturnAddress(package.buyLink); 5144 5145 //const goBack = document.getElementById("go_back"); 5146 // goBack.addEventListener('click', function () { 5147 // window.location.href = 'subscriptions.jsp'; 5148 // }); 5149 //goBack.href = 'subscriptions.jsp'; 5150 } 5151 5152 function loadCheckoutPayment() { 5153 const paymentJson = localStorage.getItem('payment'); 5154 if (paymentJson === undefined || paymentJson === '') { 5155 alog("Jump to subscriptions 22"); 5156 window.location.href = 'subscriptions.jsp'; 5157 return; 5158 } 5159 5160 5161 const title = document.getElementById("title"); 5162 setTextContentTranslationByElement(title, 'choosePaymentMethod'); 5163 5164 const payment = JSON.parse(paymentJson); 5165 const paymentItemList = document.getElementById("paymentItemList"); 5166 // payment.items.forEach(item => { 5167 // addPaymentItem(paymentItemList, item); 5168 // }); 5169 5170 let totalPrice = 0; 5171 for (let i = 0; i < payment.items.length; i++) { 5172 // if (i>0) { 5173 // const line = document.createElement('hr'); 5174 // paymentItemList.appendChild(line); 5175 // } 5176 5177 addPaymentItem(paymentItemList, payment.items[i]); 5178 totalPrice += payment.items[i].price; 5179 5180 const line = document.createElement('hr'); 5181 paymentItemList.appendChild(line); 5182 5183 // if (i === payment.items.length - 1) { 5184 // line.classList.add('mb-5'); 5185 // } 5186 } 5187 5188 const totalPaymentItem = { 5189 name: translations['total'], 5190 //Total Payment Amount of [itemCount] item(s) 5191 description: translations['totalPaymentAmount'].replace('[itemCount]', payment.items.length), 5192 price: totalPrice, 5193 currency: payment.items[0].currency 5194 }; 5195 addPaymentItem(paymentItemList, totalPaymentItem, true); 5196 5197 const btnPaypal = document.getElementById("paypal"); 5198 btnPaypal.classList.remove('d-none'); 5199 btnPaypal.addEventListener('click', function() { 5200 if (document.getElementById('btn_spinner')) 5201 return; 5202 const spinner = generateSpinner(); 5203 btnPaypal.appendChild(spinner); 5204 5205 const url = endpoint + "/payment/checkout"; 5206 const data = { 5207 sid: getSid(), 5208 // projectTypeId: 2, 5209 paymentId: payment.id, 5210 source: "paypal" 5211 }; 5212 postRequest(url, data).then(async response => { 5213 if (response.ok) { 5214 const responseBody = await response.json(); 5215 if (responseBody.statusCode === 0) { 5216 alog("Jump to " + responseBody.content + " 23"); 5217 window.location.href = responseBody.content; 5218 removeSpinner(spinner); 5219 } else { 5220 removeSpinner(spinner); 5221 showAlert("Error: " + responseBody.statusMessage, "error"); 5222 } 5223 } 5224 }); 5225 }); 5226 5227 const btnCreditCard = document.getElementById("credit_card"); 5228 btnCreditCard.classList.remove('d-none'); 5229 btnCreditCard.addEventListener('click', function() { 5230 if (document.getElementById('btn_spinner')) 5231 return; 5232 const spinner = generateSpinner(); 5233 btnCreditCard.appendChild(spinner); 5234 5235 const url = endpoint + "/payment/checkout"; 5236 const data = { 5237 sid: getSid(), 5238 // projectTypeId: 2, 5239 paymentId: payment.id, 5240 source: 'card' 5241 }; 5242 postRequest(url, data).then(async response => { 5243 if (response.ok) { 5244 const responseBody = await response.json(); 5245 if (responseBody.statusCode === 0) { 5246 alog("Jump to " + responseBody.content + " 24"); 5247 window.location.href = responseBody.content; 5248 removeSpinner(spinner); 5249 } else { 5250 removeSpinner(spinner); 5251 showAlert("Error: " + responseBody.statusMessage, "error"); 5252 } 5253 } 5254 }); 5255 }); 5256 5257 const goBack = document.getElementById("go_back"); 5258 goBack.addEventListener('click', function() { 5259 window.location.href = 'subscriptions.jsp'; 5260 }); 5261 } 5262 5263function generateSpinner() { 5264 const spinner = document.createElement('span'); 5265 spinner.id = 'btn_spinner' 5266 spinner.attributes['role'] = 'status'; 5267 spinner.attributes['aria-hidden'] = 'true'; 5268 spinner.classList.add('spinner-border', 'spinner-border-sm', 'ms-2', 'text-light'); 5269 // spinner.style.width = '1.4rem'; 5270 // spinner.style.height = '1.4rem'; 5271 spinner.started = new Date(); 5272 return spinner; 5273} 5274 5275function removeSpinner(spinner, ff) { 5276 var endDate = new Date(); 5277 var msecs = endDate.getTime() - spinner.started.getTime(); 5278 console.debug("msecs=" + msecs); 5279 if (msecs >= 2000) { 5280 spinner.remove(); 5281 if (ff) 5282 ff(); 5283 } 5284 else 5285 setTimeout(function() { removeSpinner(spinner, ff); }, 2001 - msecs); 5286} 5287 5288 5289function addPaymentItem(container, item, isTotal) { 5290 const itemContainer = document.createElement('div'); 5291 itemContainer.classList.add('d-flex', 'align-items-stretch'); //border-top 5292 container.appendChild(itemContainer); 5293 5294 const textContainer = document.createElement('div'); 5295 textContainer.classList.add('d-flex', 'flex-column', 'flex-grow-1'); 5296 itemContainer.appendChild(textContainer); 5297 5298 const itemName = document.createElement('p'); 5299 itemName.classList.add('card-text', 'fs-5'); 5300 itemName.textContent = item.name; 5301 textContainer.appendChild(itemName); 5302 5303 const itemDescription = document.createElement('p'); 5304 itemDescription.classList.add('card-text', 'text-muted', 'fs-6'); 5305 itemDescription.innerHTML = item.description; 5306 textContainer.appendChild(itemDescription); 5307 5308 const itemPrice = document.createElement('p'); 5309 itemPrice.classList.add('card-text', 'ms-auto'); 5310 if (isTotal) { 5311 itemPrice.classList.add('fw-bold', 'fs-3'); 5312 } else { 5313 itemPrice.classList.add('fs-5'); 5314 } 5315 // itemPrice.textContent = item.price + ' ' + item.currency; 5316 itemPrice.textContent = item.price + ' ' + item.currency; 5317 itemContainer.appendChild(itemPrice); 5318} 5319 5320 5321/* PAYMENT RESULTS */ 5322 5323function loadPaymentResult() { 5324 if (checkIfLoginRequired() ) 5325 return; 5326 const params = new URLSearchParams(window.location.search); 5327 if (params.has('state')) { 5328 const state = params.get("state"); 5329 if (state === "1") { 5330 const successSection = document.getElementById("success_section"); 5331 setTextContentTranslationByElement(successSection.getElementsByTagName('h3')[0], 'paymentIsSuccessful'); 5332 setTextContentTranslationByElement(successSection.getElementsByTagName('p')[0], 'subscriptionPaymentSuccess'); 5333 setTextContentTranslationByElement(successSection.getElementsByTagName('a')[0], 'goSubscriptions'); 5334 successSection.classList.remove('d-none'); 5335 } else if (state === "0") { 5336 const failureSection = document.getElementById("failure_section"); 5337 setTextContentTranslationByElement(failureSection.getElementsByTagName('h3')[0], 'paymentFailed'); 5338 setTextContentTranslationByElement(failureSection.getElementsByTagName('p')[0], 'subscriptionPaymentFailed'); 5339 setTextContentTranslationByElement(failureSection.getElementsByTagName('a')[0], 'goSubscriptions'); 5340 failureSection.classList.remove('d-none'); 5341 } else if (state === "2") { 5342 const expiredSection = document.getElementById("expired_section"); 5343 setTextContentTranslationByElement(expiredSection.getElementsByTagName('h3')[0], 'paymentExpired'); 5344 setTextContentTranslationByElement(expiredSection.getElementsByTagName('p')[0], 'subscriptionPaymentExpired'); 5345 setTextContentTranslationByElement(expiredSection.getElementsByTagName('a')[0], 'goSubscriptions'); 5346 expiredSection.classList.remove('d-none'); 5347 } else 5348 showAlert("Invalid payment state!", "error"); 5349 } 5350} 5351 5352 5353/* PAYMENTS */ 5354 5355async function loadPayments() { 5356 if (checkIfLoginRequired()) 5357 return; 5358 5359 setTextContentTranslationById('title', 'paymentHistory'); 5360 const table = document.getElementById('payments_table'); 5361 const ths = table.getElementsByTagName('th'); 5362 let colIndex = 0; 5363 if (isReseller) 5364 ths[0].remove(); 5365 else 5366 setTextContentTranslationByElement(ths[colIndex++], 'plan'); 5367 setTextContentTranslationByElement(ths[colIndex++], 'date'); 5368 setTextContentTranslationByElement(ths[colIndex++], 'price'); 5369 setTextContentTranslationByElement(ths[colIndex++], 'invoice'); 5370 5371 const data = { 5372 sid: getSid(), 5373 };
5374 postRequest(endpoint + '/payment/list', data).then(async response => { 5375 if (response.ok) { 5376 const table = document.getElementById('payments_table'); 5377 5378 while (table.rows.length > 1) { 5379 table.deleteRow(1); 5380 } 5381 5382 const responseBody = await response.json(); 5383 if (responseBody.statusCode === 0) { 5384 const checkouts = []; 5385 responseBody.content.forEach(payment => { 5386 if (payment.state === 3) {//Completed 5387 const row = table.insertRow(-1); 5388 if (!isReseller) 5389 row.insertCell(-1).textContent = payment.subscriptionType; 5390 row.insertCell(-1).textContent = payment.createdDate; 5391 5392 const totalPrice = payment.items.reduce((total, item) => total + item.price, 0); 5393 row.insertCell(-1).textContent = totalPrice + ' ' + payment.items[0].currency; 5394 5395 const invoiceCell = document.createElement('td'); 5396 row.appendChild(invoiceCell); 5397 if (payment.invoiceCreated) { 5398 const invoiceOpenLink = document.createElement('a'); 5399 invoiceOpenLink.classList.add('btn', 'btn-outline-primary', 'btn-sm'); 5400 invoiceOpenLink.href = endpoint + '/payment/invoice/' + payment.id + '/open'; 5401 invoiceOpenLink.target = '_blank'; 5402 invoiceOpenLink.textContent = 'Open'; 5403 invoiceCell.appendChild(invoiceOpenLink); 5404 5405 const invoiceDownloadLink = document.createElement('a'); 5406 invoiceDownloadLink.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2'); 5407 invoiceDownloadLink.href = endpoint + '/payment/invoice/' + payment.id + '/download'; 5408 invoiceDownloadLink.textContent = 'Download'; 5409 invoiceCell.appendChild(invoiceDownloadLink); 5410 } 5411 } else if (payment.state === 0) {//Pending 5412 checkouts.push(payment); 5413 } 5414 }); 5415 5416 if (responseBody.content.length === 0) { 5417 table.innerHTML = translations['noPaymentsFound'] || 'No payments yet.'; 5418 table.classList.add('no-projects-link'); 5419 } 5420 5421 if (isReseller) { 5422 if (checkouts && checkouts.length > 0) 5423 displayCheckouts(checkouts); 5424 else 5425 displayUsedDomainCount(); 5426 } 5427 } else { 5428 console.log("Error: " + responseBody.statusMessage); 5429 } 5430 } 5431 }); 5432} 5433 5434async function getProjects() { 5435 const data = { sid: getSid(), projectTypeId: 2 }; 5436 const response = await postRequest(endpoint + '/project/list', data); 5437 //debugger; 5438 if (response.ok) { 5439 const responseBody = await response.json(); 5440 if (responseBody.statusCode === 0) 5441 return responseBody.contents; 5442 else if (responseBody.statusCode === 3) { 5443 deleteCookie('sid'); 5444 deleteCookie('name'); 5445 deleteCookie('email'); 5446 clearLocalStorage(); 5447 alog("Jump to login 23"); 5448 window.location.href = '../views/login.jsp'; 5449 } 5450 } else { 5451 console.log('Error: ' + response.statusText); 5452 } 5453 return null; 5454} 5455 5456async function loadProjectPayments(projectId) { 5457 const data = { 5458 sid: getSid(), 5459 projectId: projectId 5460 }; 5461 postRequest(endpoint + '/payment/list', data).then(async response => { 5462 if (response.ok) { 5463 const table = document.getElementById('payments_table'); 5464 5465 while (table.rows.length > 1) { 5466 table.deleteRow(1); 5467 } 5468 5469 const responseBody = await response.json(); 5470 if (responseBody.statusCode === 0) { 5471 // const tableBody = table.getElementsByTagName('tbody')[0]; 5472 responseBody.content.forEach(payment => { 5473 const row = table.insertRow(-1); 5474 row.insertCell(-1).textContent = payment.projectName; 5475 row.insertCell(-1).textContent = payment.subscriptionType; 5476 row.insertCell(-1).textContent = payment.amount + ' ' + payment.currency; 5477 row.insertCell(-1).textContent = payment.createdDate; 5478 5479 const invoiceCell = document.createElement('td'); 5480 row.appendChild(invoiceCell); 5481 5482 if (payment.invoiceCreated) { 5483 const invoiceOpenLink = document.createElement('a'); 5484 invoiceOpenLink.classList.add('btn', 'btn-outline-primary', 'btn-sm'); 5485 invoiceOpenLink.href = endpoint + '/payment/invoice/' + payment.id + '/open'; 5486 invoiceOpenLink.target = '_blank'; 5487 invoiceOpenLink.textContent = 'Open'; 5488 invoiceCell.appendChild(invoiceOpenLink); 5489 5490 const invoiceDownloadLink = document.createElement('a'); 5491 invoiceDownloadLink.classList.add('btn', 'btn-outline-primary', 'btn-sm', 'ms-2');
5492 invoiceDownloadLink.href = endpoint + '/payment/invoice/' + payment.id + '/download'; 5493 invoiceDownloadLink.textContent = 'Download'; 5494 invoiceCell.appendChild(invoiceDownloadLink); 5495 } 5496 }); 5497 } else { 5498 console.log("Error: " + responseBody.statusMessage); 5499 } 5500 } 5501 }); 5502} 5503 5504 5505/* CONTACT US */ 5506 5507function loadContactUs() { 5508 setTextContentTranslationById('contact_us_title', 'contactUs'); 5509 setTextContentTranslationByQuery('label[for="email"]', 'email'); 5510 setPlaceholderTranslationById('email', 'enterYourEmail'); 5511 setTextContentTranslationByQuery('label[for="message"]', 'message'); 5512 setPlaceholderTranslationById('message', 'enterYourMessage'); 5513 setTextContentTranslationByQuery('button[type="submit"]', 'send'); 5514 5515 const email = getCookie('email'); 5516 const emailElement = document.getElementById('email'); 5517 emailElement.value = email; 5518 5519 const contactForm = document.getElementById('contact_form'); 5520 contactForm.addEventListener('submit', event => { 5521 event.preventDefault(); 5522 onContact(contactForm); 5523 }); 5524} 5525 5526function onContact(contactForm) { 5527 const email = contactForm.email.value; 5528 const message = contactForm.message.value; 5529 const data = { sid: getSid(), email: email, message: message, projectTypeId: 2 }; 5530 postRequest(endpoint + '/contactus', data).then(async response => { 5531 if (response.ok) { 5532 const responseBody = await response.json(); 5533 if (responseBody.statusCode === 0) { 5534 displayAlert('Your message has been sent successfully!', 'success'); 5535 console.log('Your message has been sent successfully!'); 5536 contactForm.reset(); 5537 contactForm.email.value = email; 5538 } else { 5539 showAlert('Error: ' + responseBody.statusMessage, "error"); 5540 } 5541 } 5542 }); 5543} 5544 5545 5546async function postRequest(url, data) { 5547 return fetch(url, { 5548 //mode: 'cors', 5549 method: 'POST', 5550 headers: { 5551 'Content-Type': 'application/json; charset=UTF-8' 5552 }, 5553 body: JSON.stringify(data) 5554 }).then((response) => { 5555 if (response.ok) { 5556 return response; 5557 } else if (response.status === 401) { 5558 logout(); 5559 } else { 5560 console.error('Request failed: ' + response.statusText); 5561 return null; 5562 } 5563 }); 5564} 5565 5566function getRequest(url) { 5567 return fetch(url, { 5568 //mode: 'cors', 5569 method: 'GET', 5570 // headers: { 5571 // 'Content-Type': 'application/json' 5572 // } 5573 }); 5574} 5575 5576function createCookie(key, value) { 5577 const age = 60 * 60 * 24 * 7; // 7 days 5578 document.cookie = key + "=" + value + "; path=/; max-age=" + age; 5579} 5580 5581function getCookie(key) { 5582 const cookies = document.cookie.split("; "); 5583 for (let i = 0; i < cookies.length; i++) { 5584 const pair = cookies[i].split("="); 5585 if (pair[0] === key) 5586 return pair[1]; 5587 } 5588 return null; 5589} 5590 5591function getSid() { 5592 return getCookie('sid'); 5593} 5594 5595function deleteCookie(key) { 5596 document.cookie = key + "=; path=/; max-age=0"; 5597} 5598 5599function clearLocalStorage() { 5600 localStorage.removeItem('project'); 5601 localStorage.removeItem('buying_package'); 5602 localStorage.removeItem('payment'); 5603} 5604 5605 5606function displayCheckouts(checkouts) { 5607 // const userAlert = document.getElementById('user_alert'); 5608 // const messageElement = userAlert.getElementsByTagName('p')[0]; 5609 // let text = checkouts.length > 1 ? translations['pendingCheckoutMsgPlural'] : translations['pendingCheckoutMsg']; 5610 // messageElement.innerText = text.replace('[checkoutCount]', checkouts.length); 5611 // userAlert.classList.remove('d-none'); 5612 5613 displayAlert(translations['pendingCheckoutMsg'].replace('[checkoutCount]', checkouts.length)); 5614 5615 const checkoutsElement = document.getElementById('checkouts'); 5616 checkoutsElement.classList.remove('d-none'); 5617 5618 checkouts.forEach(checkout => { 5619 const items = checkout.items; 5620 5621 const card = document.createElement('div'); 5622 card.classList.add('card', 'mb-4', 'w-100', 'd-flex', 'align-items-stretch'); 5623 checkoutsElement.appendChild(card); 5624 5625 const row = document.createElement('div'); 5626 row.classList.add('row'); 5627 card.appendChild(row); 5628 5629 const col1 = document.createElement('div'); 5630 col1.classList.add('col-md-9', 'd-flex', 'align-items-stretch'); 5631 row.appendChild(col1); 5632 5633 const cardBody = document.createElement('div'); 5634 cardBody.classList.add('card-body'); 5635 col1.appendChild(cardBody); 5636 5637 addPaymentItem(cardBody, items[0]); 5638 let totalPrice = items[0].price; 5639 5640 if (items.length > 1) { 5641 const moreList = document.createElement('div'); 5642 moreList.classList.add('d-none'); 5643 cardBody.appendChild(moreList); 5644 5645 for (let i = 1; i < items.length; i++) { 5646 const line = document.createElement('hr'); 5647 moreList.appendChild(line); 5648 5649 const item = items[i]; 5650 addPaymentItem(moreList, item); 5651 totalPrice += item.price; 5652 } 5653 5654 const showMoreButton = document.createElement('button'); 5655 showMoreButton.classList.add('btn', 'btn-link', 'text-decoration-none', 'text-muted', 'fw-bold', 'mt-3', 'p-0'); 5656 // showMoreButton.textContent = 'Show all (' + (items.length - 1) + ')'; 5657 setTextContentTranslationByElement(showMoreButton, 'showAll'); 5658 const angleDown = document.createElement('i'); 5659 angleDown.classList.add('fas', 'fa-angle-down', 'ms-1'); 5660 showMoreButton.appendChild(angleDown); 5661 cardBody.appendChild(showMoreButton); 5662 showMoreButton.addEventListener('click', (event) => { 5663 event.preventDefault(); 5664 // const showMoreButton = event.target; 5665 5666 if (moreList.classList.contains('d-none')) { 5667 showMoreButton.textContent = translations['showLess']; 5668 const angleUp = document.createElement('i'); 5669 angleUp.classList.add('fas', 'fa-angle-up', 'ms-1'); 5670 showMoreButton.appendChild(angleUp); 5671 moreList.classList.remove('d-none'); 5672 } else { 5673 showMoreButton.textContent = translations['showAll']; 5674 showMoreButton.appendChild(angleDown); 5675 moreList.classList.add('d-none'); 5676 } 5677 // showMoreButton.remove(); 5678 }); 5679 } 5680 5681 const col2 = document.createElement('div'); 5682 col2.classList.add('col-md-3', 'd-block', 'py-3'); 5683 row.appendChild(col2); 5684 5685 const paymentPrice = document.createElement('span'); 5686 // paymentPrice.classList.add('d-flex', 'flex-column', 'align-items-center', 'justify-content-center', 'w-100', 'h-100'); 5687 paymentPrice.classList.add('d-block', 'text-center', 'mb-2', 'fw-bold', 'fs-4'); 5688 paymentPrice.textContent = totalPrice + ' ' + items[0].currency; 5689 col2.appendChild(paymentPrice); 5690 5691 const payButtonContainer = document.createElement('div'); 5692 payButtonContainer.classList.add('d-flex', 'align-items-center', 'justify-content-center', 'w-100'); 5693 col2.appendChild(payButtonContainer); 5694 5695 const payButton = document.createElement('button'); 5696 setTextContentTranslationByElement(payButton, 'pay'); 5697 payButton.classList.add('btn', 'btn-success', 'w-75'); 5698 payButtonContainer.appendChild(payButton); 5699 5700 payButton.addEventListener('click', async (event) => { 5701 event.preventDefault(); 5702 localStorage.setItem('payment', JSON.stringify(checkout)); 5703 window.location.href = 'payment.jsp?subject=checkout&cid=' + checkout.id; 5704 }); 5705 }); 5706} 5707 5708function displayUsedDomainCount() { 5709 if (usedDomainCount > 0) 5710 displayAlert(translations['usingDomainCountMsg'].replace('[domainCount]', usedDomainCount), 'info'); 5711 else 5712 displayAlert(translations['notUsingAnyDomainMsg'], 'info'); 5713} 5714 5715// function togglePasswordVisibility() { 5716 // const passwordInput = document.getElementById("password"); 5717 // const passwordIcon = document.getElementById("password_icon"); 5718 // 5719 // if (passwordInput.type === "password") { 5720 // passwordInput.type = "text"; 5721 // passwordIcon.classList.remove("fa-eye"); 5722 // passwordIcon.classList.add("fa-eye-slash"); 5723 // } else { 5724 // passwordInput.type = "password"; 5725 // passwordIcon.classList.remove("fa-eye-slash"); 5726 // passwordIcon.classList.add("fa-eye"); 5727 // } 5728// } 5729 5730function numberToText(num){ 5731 num = num.toString().replace(/[^0-9.]/g, ''); 5732 if (num < 1000) 5733 return [num, "", ""]; 5734 5735 let si = [ 5736 // { v: 1E3, s: "K", c: "text-success" }, 5737 // { v: 1E6, s: "M", c: "text-warning" }, 5738 // { v: 1E9, s: "B", c: "text-danger" }, 5739 // { v: 1E12, s: "T", c: "text-danger" }, 5740 // { v: 1E15, s: "P", c: "text-danger" }, 5741 // { v: 1E18, s: "E", c: "text-danger" } 5742 { v: 1E3, s: "k", c: "text-success" }, 5743 { v: 1E6, s: "m", c: "text-warning" }, 5744 { v: 1E9, s: "b", c: "text-danger" }, 5745 { v: 1E12, s: "T", c: "text-danger" }, 5746 { v: 1E15, s: "P", c: "text-danger" }, 5747 { v: 1E18, s: "E", c: "text-danger" } 5748 ]; 5749 let index; 5750 for (index = si.length - 1; index > 0; index--) { 5751 if (num >= si[index].v) { 5752 break; 5753 } 5754 } 5755 5756 // return (num / si[index].v).toFixed(2).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1") + si[index].s; 5757 5758 const value = (num / si[index].v).toFixed(2).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, "$1"); 5759 const ending = si[index].s; 5760 const color = si[index].c; 5761 return [value, ending, color]; 5762} 5763 5764// visitsTotal -> Visits Total
5765function keyToTitle(key) { 5766 let title = ""; 5767 for (let i = 0; i < key.length; i++) { 5768 if (i === 0) 5769 title = key[0].toUpperCase(); 5770 else if (key[i] === key[i].toUpperCase()) 5771 title += " " + key[i]; 5772 else 5773 title += key[i]; 5774 } 5775 return title; 5776} 5777 5778function displayAlert(message, type = 'danger', sticky = false) { 5779 if (type === 'error') 5780 type = 'danger'; 5781 5782 // Transient notices are toasts (top-right, auto-closing, not in the content 5783 // flow â an inline message above a long form was easy to miss). Messages here 5784 // may carry markup (<br>, links, translated strings), hence html: true. 5785 if (!sticky) { 5786 showToast(translations[message] || message, type, { html: true }); 5787 return; 5788 } 5789 5790 // sticky: a page-state notice with a link/action that must stay until the 5791 // user closes it (plan-limit banner) â rendered inline in #alert_container. 5792 const alertText = document.createElement('div'); 5793 alertText.classList.add('alert', 'alert-' + type, 'mb-2'); 5794 alertText.setAttribute('role', 'alert'); 5795 alertText.innerHTML = translations[message] || message; 5796 5797 const alertContainer = document.getElementById('alert_container'); 5798 if (!alertContainer) 5799 return; 5800 alertContainer.innerHTML = ''; 5801 alertContainer.classList.add('pb-1'); 5802 alertContainer.appendChild(alertText); 5803 5804 const closeBtn = document.createElement('button'); 5805 closeBtn.type = 'button'; 5806 closeBtn.classList.add('btn-close'); 5807 closeBtn.style.position = 'absolute'; 5808 closeBtn.style.top = '10px'; 5809 closeBtn.style.right = '5px'; 5810 closeBtn.style.width = '5px'; 5811 closeBtn.style.height = '5px'; 5812 closeBtn.setAttribute('aria-label', translations['close'] || 'Close'); 5813 closeBtn.addEventListener('click', function() { 5814 alertText.remove(); 5815 }); 5816 alertText.appendChild(closeBtn); 5817} 5818 5819function removeAfterLast(text, searchElement) { 5820 const lastSpaceIndex = text.lastIndexOf(searchElement); 5821 if (lastSpaceIndex > -1) 5822 return text.substring(0, lastSpaceIndex); 5823 return text; 5824} 5825 5826 5827function setTooltips() { 5828 const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); 5829 tooltipTriggerList.map(function(tooltipTriggerEl) { 5830 return new bootstrap.Tooltip(tooltipTriggerEl); 5831 }); 5832} 5833 5834/*function setCancelBtnClicking(page) { 5835 const btnCancel = document.getElementById('btn_cancel'); 5836 if (!btnCancel) 5837 return; 5838 5839 btnCancel.addEventListener('click', function() { 5840 const params = new URLSearchParams(window.location.search); 5841 const pp = params.get("pp"); 5842 if (pp) { 5843 switch (pp) { 5844 case 'w': 5845 window.location.href = "websites.jsp"; 5846 break; 5847 case 't': 5848 window.location.href = "templates.jsp"; 5849 break; 5850 default: 5851 window.location.href = "dashboard.jsp"; 5852 } 5853 } else { 5854 switch (page) { 5855 case 'template_settings.jsp': 5856 window.location.href = "templates.jsp"; 5857 break; 5858 case 'website_settings.jsp': 5859 window.location.href = "websites.jsp"; 5860 break; 5861 default: 5862 window.location.href = "dashboard.jsp"; 5863 } 5864 } 5865 }); 5866}*/ 5867 5868async function loadResellers() { 5869 const btnConfirm = document.getElementById('btn_resellersavesettings'); 5870 5871 if (checkIfLoginRequired() || !btnConfirm) 5872 return; 5873 5874 var setg = document.getElementById('setgoogleservers'); 5875 if (setg){ 5876 setg.onclick = function(){ 5877 event.preventDefault(); 5878 document.getElementById('smtphost').value = 'smtp.gmail.com'; 5879 document.getElementById('smtpport').value = '587'; 5880 document.getElementById('enableauth').checked='on'; 5881 document.getElementById('enabletls').checked='on'; 5882 }; 5883 } 5884 5885 btnConfirm.textContent = "Save"; 5886 setTextContentTranslationByElement(btnConfirm, 'save'); 5887 5888 const logo = document.getElementById('panellogo'); 5889 const fav = document.getElementById('panelfav'); 5890 const css = document.getElementById('panelcss'); 5891 const title = document.getElementById('paneltitle'); 5892 const url = document.getElementById('panelurl'); 5893 const consentCssUrl = document.getElementById('consentcss'); 5894 const consentLogoUrl = document.getElementById('consentlogo'); 5895 const dpaUrl = document.getElementById('dpaurl'); 5896 const termsUrl = document.getElementById('termsofuseurl'); 5897 const privUrl = document.getElementById('privacypolicyurl'); 5898 const supportEmail = document.getElementById('supportemail'); 5899 const domainName = document.getElementById('domainname'); 5900 const productName = document.getElementById('productname'); 5901 const vendorName = document.getElementById('vendorname'); 5902 5903 const emailsender = document.getElementById('emailsender'); 5904 const emailname = document.getElementById('emailname'); 5905 const emailpass = document.getElementById('emailpass'); 5906 const smtphost = document.getElementById('smtphost'); 5907 const smtpport = document.getElementById('smtpport'); 5908 const enableauth = document.getElementById('enableauth'); 5909 const enabletls = document.getElementById('enabletls'); 5910 5911 const usersTable = document.getElementById('reseller_users_table'); 5912 const tableCols = usersTable.querySelector('thead tr'); 5913 5914 setTextContentTranslationByElement(tableCols.children[0], 'id'); 5915 setTextContentTranslationByElement(tableCols.children[1], 'email'); 5916 setTextContentTranslationByElement(tableCols.children[2], 'enabled'); 5917 setTextContentTranslationByElement(tableCols.children[3], 'actions'); 5918 5919 btnConfirm.onclick = async function(event) { 5920 event.preventDefault(); 5921 5922 btnConfirm.textContent = 'updating...'; 5923 setTextContentTranslationByElement(btnConfirm, 'updating'); 5924 const spinner = generateSpinner();
5925 btnConfirm.appendChild(spinner); 5926 5927 const data = { 5928 sid: getSid(), panelLogoUrl: logo.value, panelCssUrl: css.value, panelTitle: title.value, 5929 panelHostName: url.value, panelFaviconUrl: fav.value, 5930 consentCssUrl: consentCssUrl.value, consentLogoUrl: consentLogoUrl.value, 5931 smtpEmail: emailname.value, smtpPass:emailpass.value, emailSender: emailsender.value, 5932 smtpHost: smtphost.value, smtpPort:smtpport.value, smtpAuth: enableauth.checked?'1':'0', 5933 smtpTls:enabletls.checked?'1':'0', 5934 dpaAgreementUrl: dpaUrl.value, termsOfUseUrl: termsUrl.value, privacyPolicyUrl:privUrl.value, 5935 productName:productName.value, domainName: domainName.value, supportEmail: supportEmail.value, 5936 vendorName:vendorName.value 5937 }; 5938 const response = await postRequest(endpoint + '/reseller/settings', data); 5939 5940 removeSpinner(spinner, () => { 5941 btnConfirm.textContent = "Save"; 5942 setTextContentTranslationByElement(btnConfirm, 'save'); 5943 }); 5944 5945 if (response.ok) { 5946 const responseBody = await response.json(); 5947 if (responseBody.statusCode === 0) { 5948 window.location.reload(); 5949 } else { 5950 showAlert("Error: " + responseBody.statusMessage, "error"); 5951 } 5952 } else { 5953 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 5954 } 5955 }; 5956 5957 //const btnConfirm = document.getElementById('btn_resellercreate_user'); 5958 const data = { sid: getSid() }; 5959 const response = await postRequest(endpoint + '/reseller/settings', data); 5960 if (response.ok) { 5961 const responseBody = await response.json(); 5962 if (responseBody.statusCode === 0) { 5963 //console.dir("============================="); 5964 //console.dir(responseBody); 5965 var set = responseBody.settings; 5966 logo.value = set.panelLogoUrl; 5967 css.value = set.panelCssUrl; 5968 title.value = set.panelTitle; 5969 url.value = set.panelHostName?set.panelHostName:set.domainName; 5970 fav.value = set.panelFaviconUrl; 5971 emailsender.value = set.emailSender; 5972 consentcss.value = set.consentCssUrl; 5973 consentlogo.value = set.consentLogoUrl; 5974 domainName.value = set.domainName; 5975 termsUrl.value= set.termsOfUseUrl; 5976 privUrl.value=set.privacyPolicyUrl; 5977 dpaUrl.value = set.dpaAgreementUrl; 5978 supportEmail.value = set.supportEmail; 5979 productName.value = set.productName; 5980 vendorName.value = set.vendorName || set.productName; 5981 emailname.value = set.smtpEmail; 5982 emailpass.value = set.smtpPass; 5983 smtphost.value = set.smtpHost; 5984 smtpport.value = set.smtpPort; 5985 enableauth.checked = set.smtpAuth === '1'; 5986 enabletls.checked = set.smtpTls === '1'; 5987 5988 const users = responseBody.settings.users; 5989 if (!users || !users.length) { 5990 document.getElementById('nousersfound').innerHTML = translations["noUsersFound"] ? translations["noUsersFound"] : 'No users Found'; 5991 return; 5992 } 5993 5994 usersTable.style.display = ''; 5995 const tableBody = usersTable.querySelector('tbody'); 5996 users.forEach(project => { 5997 const row = document.createElement('tr'); 5998 5999 var nameCell = document.createElement('td'); 6000 nameCell.textContent = project.id; 6001 row.appendChild(nameCell); 6002 6003 const websitesCell = document.createElement('td'); 6004 websitesCell.textContent = project.name + '(' + project.email +')'; 6005 row.appendChild(websitesCell); 6006 6007 const enabledCell = document.createElement('td'); 6008 enabledCell.classList.add('text-center'); 6009 const enabledIcon = document.createElement('i'); 6010 enabledIcon.classList.add('fa', project.enabled ? 'fa-check' : 'fa-times'); 6011 enabledIcon.classList.add(project.enabled ? 'text-success' : 'text-danger'); 6012 enabledCell.appendChild(enabledIcon); 6013 row.appendChild(enabledCell); 6014 6015 const actionCell = document.createElement('td'); 6016 actionCell.classList.add('text-center'); 6017 6018 // set password, set email 6019 addActionButtonToToolbar(actionCell, 'Settings', 'settings', () => { 6020 alog("Jump to user settings " + project.id); 6021 window.location.href = "reseller_user.jsp?id=" + project.id; 6022 }); 6023 6024 /*addActionButtonToToolbar(actionCell, project.enabled ? 'Disable' : 'Enable', project.enabled ? 'disable' : 'enable', function(btn) { 6025 }); 6026 addActionButtonToToolbar(actionCell, 'Settings', 'settings', () => { 6027 alog("Jump to user settings " + project.id); 6028 window.location.href = "reseller_user.jsp?id=" + project.id; 6029 });*/ 6030 6031 addActionButtonToToolbar(actionCell, 'Impersonate', 'impersonate', async function() { 6032 const data = { sid: getSid(), uid: project.id }; 6033 const response = await postRequest(endpoint + '/reseller/impersonate', data); 6034 if (response.ok) { 6035 const responseLogin = await response.json(); 6036 createSessionCookie(responseLogin.auth); 6037 console.dir(responseLogin); 6038 console.dir(responseLogin.content); 6039 createCookie("name", removeAfterLast(responseLogin.content.fullName, ' ')); 6040 // createCookie("name", responseLogin.content.fullName.split(' ')[0]); 6041 createCookie("email", responseLogin.content.email); 6042 // createCookie("lang", responseLogin.lang); 6043 // window.location.href = responseLogin.numProjects ? "templates.jsp" : "template_settings.jsp?create=1"; 6044 window.location.href = responseLogin.numProjects ? "dashboard.jsp" : "website_settings.jsp"; 6045 } else { 6046 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 6047 } 6048 }); 6049 6050 row.appendChild(actionCell); 6051 tableBody.appendChild(row); 6052 }); 6053 } else { 6054 showAlert("Error: " + responseBody.statusMessage, "error"); 6055 } 6056 } else { 6057 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 6058 } 6059} 6060 6061async function loadResellerUser() { 6062 if (checkIfLoginRequired() ) 6063 return; 6064 const params = new URLSearchParams(window.location.search); 6065 const userid = params.get("id") || 0; 6066 const isCreate = "1" === params.get("create"); 6067 6068 const btnConfirm = document.getElementById('btn_resellersaveuser'); 6069 //btnConfirm.textContent = isCreate ? (translations["create"] ? translations["create"] : 'Create') : 6070 // (translations["update"] ? translations["update"] : 'Update'); 6071 setTextContentTranslationByElement(btnConfirm, isCreate ? 'create' : 'update'); 6072 6073 const email = document.getElementById('email'); 6074 const name = document.getElementById('name'); 6075 const enabled = document.getElementById('enabled'); 6076 const pass1 = document.getElementById('pass1'); 6077 const pass2 = document.getElementById('pass2'); 6078 const domains = document.getElementById('domains'); 6079 const subsselect = document.getElementById('subsselect');
6080 const subdesc = document.getElementById('subdesc'); 6081 6082 if (!isCreate) { 6083 document.getElementById('pwdsection2').style.display = ''; 6084 document.getElementById('pwdsectiontitle').style.display = ''; 6085 } 6086 6087 const data = { sid: getSid(), uid: userid }; 6088 const response = await postRequest(endpoint + '/reseller/userget', data); 6089 if (response.ok) { 6090 const responseBody = await response.json(); 6091 if (responseBody.statusCode === 0) { 6092 name.value = responseBody.user.name; 6093 email.value = responseBody.user.email; 6094 enabled.checked = responseBody.user.enabled; 6095 domains.value = responseBody.user.domains; 6096 for (var i = 0; i < responseBody.subscriptionTypes.length; i++) { 6097 var opt = document.createElement('option'); 6098 var st = responseBody.subscriptionTypes[i]; 6099 opt.value = st.id; 6100 opt.innerHTML = st.name; 6101 if (st.id == responseBody.user.subscriptionType) { 6102 opt.selected = true; 6103 subdesc.innerHTML = st.description; 6104 } 6105 subsselect.appendChild(opt); 6106 } 6107 subsselect.onchange = () => { 6108 for (var i = 0; i < responseBody.subscriptionTypes.length; i++) { 6109 if ("" + responseBody.subscriptionTypes[i].id == "" + subsselect.value) 6110 subdesc.innerHTML = responseBody.subscriptionTypes[i].description; 6111 } 6112 }; 6113 } else { 6114 showAlert("Error: " + responseBody.statusMessage, "error"); 6115 } 6116 } else { 6117 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 6118 } 6119 6120 btnConfirm.onclick = async function() { 6121 event.preventDefault(); 6122 6123 btnConfirm.textContent = 'updating...'; 6124 setTextContentTranslationByElement(btnConfirm, 'updating'); 6125 const spinner = generateSpinner(); 6126 btnConfirm.appendChild(spinner); 6127 6128 const data = { 6129 sid: getSid(), email: email.value, name: name.value, enabled: enabled.checked, 6130 pass: isCreate ? pass1.value : '', uid: userid, domains: domains.value, subscriptionType: subsselect.value 6131 }; 6132 const response = await postRequest(endpoint + '/reseller/userupdate', data); 6133 6134 //console.dir("remove spinner"); 6135 6136 removeSpinner(spinner, () => { 6137 btnConfirm.textContent = isCreate ? 'Create' : 'Update'; 6138 setTextContentTranslationByElement(btnConfirm, isCreate ? 'create' : 'update'); 6139 }); 6140 6141 if (response.ok) { 6142 const responseBody = await response.json(); 6143 if (responseBody.statusCode === 0) { 6144 window.location.href = 'reseller.jsp'; 6145 } else { 6146 showAlert("Error: " + responseBody.statusMessage, "error"); 6147 } 6148 } else { 6149 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 6150 } 6151 }; 6152 6153 6154 const btnPwd = document.getElementById('btn_savepasswd'); 6155 setTextContentTranslationByElement(btnPwd, 'update'); 6156 6157 btnPwd.onclick = async function() { 6158 event.preventDefault(); 6159 6160 btnPwd.textContent = 'updating...'; 6161 setTextContentTranslationByElement(btnPwd, 'updating'); 6162 const spinner = generateSpinner(); 6163 btnPwd.appendChild(spinner); 6164 6165 const data = { sid: getSid(), pass: pass2.value, uid: params.get("id") }; 6166 const response = await postRequest(endpoint + '/reseller/userpassword', data); 6167 6168 removeSpinner(spinner, () => { 6169 btnPwd.textContent = 'Update'; 6170 setTextContentTranslationByElement(btnPwd, 'update'); 6171 }); 6172 6173 if (response.ok) { 6174 const responseBody = await response.json(); 6175 if (responseBody.statusCode === 0) { 6176 window.location.href = 'reseller.jsp'; 6177 } else { 6178 showAlert("Error: " + responseBody.statusMessage, "error"); 6179 } 6180 } else { 6181 showAlert("Error: " + (response.statusMessage || "Response failed"), "error"); 6182 } 6183 }; 6184} 6185 6186function addActionButtonToToolbar(parent, labelText, labelKey, ff) { 6187 const button = document.createElement('button'); 6188 button.textContent = translations[labelKey] ? translations[labelKey] : labelText; 6189 button.classList.add('btn', 'btn-outline-primary', 'btn-sm'); 6190 button.addEventListener('click', () => ff(button)); 6191 parent.appendChild(button); 6192 return button; 6193} 6194 6195function createSessionCookie(sessionId) { 6196 // var sessionCookie = "sid=" + sessionId + "; path=/; max-age=86400"; // 24 hours 6197 // document.cookie = sessionCookie; 6198 createCookie("sid", sessionId); 6199} 6200 6201 6202 6203/// Document Generator 6204 6205async function loadDocumentGenerator() { 6206 if (checkIfLoginRequired()) 6207 return; 6208 6209 // Check if required DOM elements exist 6210 const projectDropdown = document.getElementById('select_domain'); 6211 const languageDropdown = document.getElementById('select_language'); 6212 const container = document.getElementById('document_generator_container'); 6213 6214 if (!projectDropdown) { 6215 console.error('Project dropdown not found - make sure element with ID "select_domain" exists'); 6216 return; 6217 } 6218 6219 if (!languageDropdown) { 6220 console.error('Language dropdown not found - make sure element with ID "select_language" exists'); 6221 return; 6222 } 6223 6224 if (!container) { 6225 console.error('Document generator container not found - make sure element with ID "document_generator_container" exists'); 6226 return; 6227 } 6228 6229 try { 6230 // Load project domains 6231 let projects = null; 6232 const data = { sid: getSid(), projectTypeId: 2, method: 1 };
6233 const response = await postRequest(endpoint + '/project-domain/list', data); 6234 6235 if (response.ok) { 6236 const responseBody = await response.json(); 6237 if (responseBody.statusCode === 0) { 6238 projects = responseBody.domains; 6239 } else { 6240 console.error('Error loading projects: ' + responseBody.statusMessage); 6241 displayAlertDocument(translations['failedToLoadProjects'] || 'Failed to load projects: ' + responseBody.statusMessage, 'error'); 6242 return; 6243 } 6244 } else { 6245 console.error('Error: ' + response.statusText); 6246 displayAlertDocument(translations['failedToLoadProjects'] || 'Failed to load projects: ' + response.statusText, 'error'); 6247 return; 6248 } 6249 6250 // Clear and populate project dropdown 6251 projectDropdown.innerHTML = ''; 6252 6253 if (projects && projects.length > 0) { 6254 projects.forEach(project => { 6255 const option = document.createElement('option'); 6256 option.value = project.id; 6257 option.text = project.domainName; 6258 projectDropdown.appendChild(option); 6259 }); 6260 } else { 6261 console.error('No projects available'); 6262 displayAlertDocument(translations['noProjectsAvailable'] || 'No projects available', 'error'); 6263 return; 6264 } 6265 6266 // Add project change event listener 6267 projectDropdown.addEventListener('change', function() { 6268 const selectedProjectId = this.value; 6269 console.log('selectedProjectId='+selectedProjectId); 6270 if (selectedProjectId) { 6271 loadCompanyInformation(selectedProjectId).then(() => { 6272 populateCompanyData(window.companyData); 6273 }).catch(error => { 6274 console.error('Error loading company information:', error); 6275 }); 6276 } 6277 }); 6278 6279 // Load languages for the language dropdown 6280 try { 6281 await loadLanguageDropdown(); 6282 } catch (error) { 6283 console.error('Error loading languages:', error); 6284 // Continue even if languages fail to load 6285 } 6286 6287 // Trigger project selection to load company data 6288 if (projectDropdown.value) { 6289 projectDropdown.dispatchEvent(new Event('change')); 6290 } 6291 6292 // Clear any existing content in the container 6293 container.innerHTML = ''; 6294 6295 // Create form card 6296 const card = document.createElement('div'); 6297 card.classList.add('card', 'mb-4'); 6298 container.appendChild(card); 6299 6300 const cardBody = document.createElement('div'); 6301 cardBody.classList.add('card-body'); 6302 card.appendChild(cardBody); 6303 6304 // Document type selection section at TOP 6305 const docTypeSection = document.createElement('div'); 6306 docTypeSection.classList.add('mb-4'); 6307 cardBody.appendChild(docTypeSection); 6308 6309 const docTypeTitle = document.createElement('h5'); 6310 docTypeTitle.id = 'document_type_title'; 6311 docTypeTitle.classList.add('mb-3'); 6312 docTypeTitle.textContent = translations['selectDocumentType'] || 'Select Document Type'; 6313 docTypeSection.appendChild(docTypeTitle); 6314 6315 // Document types with descriptions and specific fields 6316 const documentTypes = [ 6317 { 6318 id: 'privacyPolicy', 6319 label: translations['privacyPolicy'] || 'Privacy Policy', 6320 descriptionId: 'privacyPolicyDescription', 6321 description: translations['privacyPolicyDescription'] || 'A document that explains how you collect, use, and manage users\' personal data.', 6322 fields: ['lastUpdated', 'effectiveDate', 'name', 'address', 'country', 'email', 'phone', 'websiteUrl'] 6323 }, 6324 { 6325 id: 'termsOfUse', 6326 label: translations['termsOfUse'] || 'Terms of Use', 6327 descriptionId: 'termsOfUseDescription', 6328 description: translations['termsOfUseDescription'] || 'A document that outlines the rules and guidelines for using your website or service.', 6329 fields: ['lastUpdated', 'effectiveDate', 'name', 'serviceName', 'address', 'country', 'email', 'phone', 'companyRegNo', 'websiteUrl', 'privacyUrl', 'pricingUrl'] 6330 }, 6331 { 6332 id: 'dpa', 6333 label: translations['dpa'] || 'Data Processing Agreement (DPA)', 6334 descriptionId: 'dpaDescription', 6335 description: translations['dpaDescription'] || 'A c
6335ontract that regulates the handling of personal data.', 6336 fields: ['lastUpdated', 'effectiveDate', 'name', 'legalEntity', 'address', 'country', 'termsUrl'] 6337 }, 6338 { 6339 id: 'cookiePolicy', 6340 label: translations['cookiePolicy'] || 'Cookie Policy', 6341 descriptionId: 'cookiePolicyDescription', 6342 description: translations['cookiePolicyDescription'] || 'A document that explains how cookies are used on your website.', 6343 fields: ['lastUpdated', 'effectiveDate', 'name', 'address', 'country', 'email', 'websiteUrl'] 6344 } 6345 ]; 6346 6347 // Create buttons container - vertical layout 6348 const buttonContainer = document.createElement('div'); 6349 buttonContainer.id = 'document_type_buttons'; 6350 buttonContainer.classList.add('d-flex', 'flex-column', 'gap-2'); 6351 docTypeSection.appendChild(buttonContainer); 6352 6353 // Create the Go Back button with red outline style 6354 const goBackBtn = document.createElement('button'); 6355 goBackBtn.type = 'button'; 6356 goBackBtn.id = 'cancel_selection'; 6357 goBackBtn.classList.add('btn', 'btn-outline-danger', 'me-2', 'd-none'); 6358 goBackBtn.textContent = translations['goBack'] || 'Go Back'; 6359 6360 // Add custom styling for better hover effect 6361 const style = document.createElement('style'); 6362 style.textContent = ` 6363 #cancel_selection { 6364 color: #dc3545 !important; /* Red text color */ 6365 background-color: transparent !important; /* No background */ 6366 border: 1px solid #dc3545 !important; /* Red border, always visible */ 6367 transition: all 0.15s ease-in-out !important; /* Smooth transition */ 6368 } 6369 6370 #cancel_selection:hover { 6371 color: #dc3545 !important; /* Keep red text on hover */ 6372 background-color: rgba(220, 53, 69, 0.1) !important; /* Light red background on hover */ 6373 border: 1px solid #dc3545 !important; /* Red border on hover */ 6374 transform: translateY(-1px) !important; /* Slight lift effect */ 6375 } 6376 6377 #cancel_selection:active { 6378 color: #fff !important; /* White text when clicked */ 6379 background-color: #dc3545 !important; /* Red background when clicked */ 6380 border: 1px solid #dc3545 !important; /* Red border when clicked */ 6381 transform: translateY(0) !important; /* Reset lift effect */ 6382 } 6383 6384 #cancel_selection:focus { 6385 box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important; /* Red focus outline */ 6386 } 6387 `; 6388 6389 // Add the custom styles to the document head 6390 document.head.appendChild(style); 6391 6392 // Go Back button click handler (same as before) 6393 goBackBtn.addEventListener('click', () => { 6394 // Reset title to "Select Document Type" 6395 const docTypeTitle = document.getElementById('document_type_title'); 6396 docTypeTitle.textContent = translations['selectDocumentType'] || 'Select Document Type'; 6397 6398 // Show all document type buttons 6399 document.querySelectorAll('.doc-type-btn').forEach(btn => { 6400 btn.classList.remove('d-none'); 6401 btn.classList.remove('btn-info', 'btn-primary', 'btn-secondary'); 6402 btn.classList.add('btn-outline-primary'); 6403 btn.setAttribute('aria-pressed', 'false'); 6404 6405 // Completely reset all inline styles 6406 btn.removeAttribute('style'); 6407 6408 // Reset all child elements 6409 const buttonContent = btn.querySelector('div'); 6410 if (buttonContent) { 6411 buttonContent.removeAttribute('style'); 6412 6413 // Reset label styling 6414 const label = buttonContent.querySelector('.fw-bold'); 6415 if (label) { 6416 label.removeAttribute('style'); 6417 label.classList.remove('text-white', 'text-dark', 'text-dark-75'); 6418 } 6419 6420 // Reset description styling 6421 const description = buttonContent.querySelector('small'); 6422 if (description) { 6423 description.removeAttribute('style'); 6424 description.classList.remove('text-white', 'text-white-75', 'text-dark', 'text-dark-75'); 6425 // Ensure it has the correct muted class 6426 if (!description.classList.contains('text-muted')) { 6427 description.classList.add('text-muted'); 6428 } 6429 } 6430 } 6431 6432 6433 }); 6434 6435 // Hide form fields, format options, and generate button container 6436 formFieldsSection.classList.add('d-none'); 6437 formatGroup.classList.add('d-none'); 6438 generateBtnContainer.classList.add('d-none'); 6439 6440 // Hide the document result section 6441 const resultSection = document.getElementById('document_result'); 6442 if (resultSection) { 6443 resultSection.classList.add('d-none'); 6444 resultSection.innerHTML = ''; 6445 } 6446 6447 // Clear any displayed alerts 6448 const existingAlerts = document.querySelectorAll('.alert');
6449 existingAlerts.forEach(alert => alert.remove()); 6450 }); 6451 6452 6453 // Create buttons for document types 6454 documentTypes.forEach(docType => { 6455 const button = document.createElement('button'); 6456 button.type = 'button'; 6457 button.id = `btn_${docType.id}`; 6458 button.classList.add('btn', 'btn-outline-info', 'text-start', 'py-2', 'doc-type-btn'); 6459 button.dataset.docType = docType.id; 6460 button.dataset.fields = JSON.stringify(docType.fields); 6461 buttonContainer.appendChild(button); 6462 6463 // Create content inside the button with flex layout 6464 const buttonContent = document.createElement('div'); 6465 buttonContent.classList.add('d-flex', 'flex-column', 'align-items-start'); 6466 button.appendChild(buttonContent); 6467 6468 const docTypeLabel = document.createElement('span'); 6469 docTypeLabel.classList.add('fw-bold', 'mb-1'); 6470 docTypeLabel.textContent = docType.label; 6471 buttonContent.appendChild(docTypeLabel); 6472 6473 const docTypeDesc = document.createElement('small'); 6474 docTypeDesc.classList.add('text-muted'); 6475 docTypeDesc.textContent = docType.description; 6476 buttonContent.appendChild(docTypeDesc); 6477 6478 // Add click handler for button 6479 button.addEventListener('click', () => { 6480 // Hide all unselected buttons 6481 document.querySelectorAll('.doc-type-btn').forEach(btn => { 6482 if (btn.id !== button.id) { 6483 btn.classList.add('d-none'); 6484 } 6485 }); 6486 6487 // Mark this button as selected with proper styling 6488 button.classList.remove('btn-outline-primary'); 6489 button.classList.add('btn-info'); 6490 button.setAttribute('aria-pressed', 'true'); 6491 6492 // Fix text color for selected button - make all text black 6493 button.style.color = 'black'; 6494 button.style.backgroundColor = '#d1ecf1'; // Light blue background 6495 button.style.borderColor = '#bee5eb'; // Light blue border 6496 const buttonContent = button.querySelector('div'); 6497 if (buttonContent) { 6498 buttonContent.style.color = 'black'; 6499 // Make sure all child elements are black 6500 const label = buttonContent.querySelector('.fw-bold'); 6501 const description = buttonContent.querySelector('.text-muted, small'); 6502 if (label) { 6503 label.style.color = 'black'; 6504 label.classList.remove('text-muted'); // Remove any muted classes 6505 } 6506 if (description) { 6507 description.style.color = '#495057'; // Dark gray for description 6508 description.classList.remove('text-muted'); // Remove muted class 6509 description.classList.add('text-dark-75'); // Add dark gray class 6510 } 6511 } 6512 6513 // Change title 6514 const docTypeTitle = document.getElementById('document_type_title'); 6515 const selectedLabel = docType.label; 6516 docTypeTitle.textContent = `${translations['documentInformationOf'] || 'Document Information of'} ${selectedLabel}`; 6517 6518 // Show cancel button 6519 goBackBtn.classList.remove('d-none'); 6520 6521 // Update form with fields for this document type 6522 updateFormFields(docType.fields, docType.id); 6523 6524 // Show the form fields section 6525 formFieldsSection.classList.remove('d-none'); 6526 6527 // Show format options and generate button 6528 formatGroup.classList.remove('d-none'); 6529 generateBtnContainer.classList.remove('d-none'); 6530 6531 // Load company information for the selected project 6532 const selectedProjectId = document.getElementById('select_domain').value; 6533 if (selectedProjectId) { 6534 loadCompanyInformation(selectedProjectId).then(() => { 6535 populateCompanyData(window.companyData); 6536 }).catch(error => { 6537 console.error('Error loading company information:', error); 6538 }); 6539 } 6540 }); 6541 }); 6542 6543 // Dynamic form fields section - initially hidden 6544 const formFieldsSection = document.createElement('div'); 6545 formFieldsSection.id = 'dynamic_form_fields'; 6546 formFieldsSection.classList.add('mb-4', 'd-none'); 6547 cardBody.appendChild(formFieldsSection); 6548 6549 // Format options - initially hidden 6550 const formatGroup = document.createElement('div'); 6551 formatGroup.classList.add('mb-3', 'mt-4', 'd-none'); 6552 cardBody.appendChild(formatGroup); 6553 6554 const formatLabel = document.createElement('label'); 6555 formatLabel.classList.add('form-label'); 6556 formatLabel.htmlFor = 'document_format'; 6557 formatLabel.textContent = translations['documentFormat'] || 'Document Format'; 6558 formatGroup.appendChild(formatLabel); 6559 6560 const formatSelect = document.createElement('select'); 6561 formatSelect.id = 'document_format'; 6562 formatSelect.classList.add('form-select'); 6563 formatGroup.appendChild(formatSelect); 6564 6565 const formatOptions = [ 6566 { value: 'html', label: translations['formatHtml'] || 'HTML' }, 6567 { value: 'pdf', label: translations['formatPdf'] || 'PDF' } 6568 ]; 6569 6570 formatOptions.forEach(option => { 6571 const optionEl = document.createElement('option'); 6572 optionEl.value = option.value; 6573 optionEl.textContent = option.label; 6574 formatSelect.appendChild(optionEl); 6575 }); 6576 6577 // Add generate button container with BOTH buttons - initially hidden 6578 const generateBtnContainer = document.createElement('div'); 6579 generateBtnContainer.classList.add('d-flex', 'justify-content-center', 'mt-4', 'd-none'); 6580 cardBody.appendChild(generateBtnContainer); 6581 6582 // Add the Go Back button FIRST (on the left) 6583 generateBtnContainer.appendChild(goBackBtn); 6584 6585 // Then add the Generate Document button (on the right) 6586 const generateBtn = document.createElement('button'); 6587 generateBtn.type = 'button'; 6588 generateBtn.id = 'generate_document_btn'; 6589 generateBtn.classList.add('btn', 'btn-primary', 'px-4', 'py-2'); 6590 generateBtn.textContent = translations['generateDocument'] || 'Generate Document'; 6591 generateBtnContainer.appendChild(generateBtn); 6592 6593 // Create result section 6594 const resultSection = document.createElement('div'); 6595 resultSection.id = 'document_result';
6596 resultSection.classList.add('mt-4', 'd-none'); 6597 container.appendChild(resultSection); 6598 6599 // Add event listener for generating documents 6600 generateBtn.addEventListener('click', handleGenerateDocument); 6601 6602 console.log('Document generator loaded successfully'); 6603 6604 } catch (error) { 6605 console.error('Error loading document generator:', error); 6606 displayAlertDocument(translations['failedToLoadDocumentGenerator'] || 'Failed to load document generator: ' + error.message, 'error'); 6607 } 6608} 6609 6610function isValidEmail(email) { 6611 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 6612 return emailRegex.test(email); 6613} 6614 6615function isValidUrl(url) { 6616 try { 6617 const urlObj = new URL(url); 6618 return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'; 6619 } catch (e) { 6620 return false; 6621 } 6622} 6623 6624function isValidPhoneNumber(phone) { 6625 const phoneRegex = /^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,3}[-\s.]?[0-9]{1,4}[-\s.]?[0-9]{1,4}$/; 6626 return phoneRegex.test(phone); 6627} 6628 6629function addInputValidation(field) { 6630 const input = document.getElementById(field); 6631 if (!input) return; 6632 6633 // Determine which validation to apply based on field ID or type 6634 if (field === 'email' || input.type === 'email') { 6635 // Email validation 6636 input.addEventListener('blur', function() { 6637 if (this.value && !isValidEmail(this.value)) { 6638 this.classList.add('is-invalid'); 6639 6640 // Add validation message if doesn't exist 6641 if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) { 6642 const feedback = document.createElement('div'); 6643 feedback.classList.add('invalid-feedback'); 6644 feedback.textContent = translations['invalidEmail'] || 'Please enter a valid email address'; 6645 this.parentNode.appendChild(feedback); 6646 } 6647 } else { 6648 this.classList.remove('is-invalid'); 6649 } 6650 }); 6651 } else if (field === 'websiteUrl' || field === 'privacyUrl' || field === 'termsUrl' || 6652 field === 'pricingUrl' || input.type === 'url') { 6653 // URL validation - must start with http:// or https:// 6654 input.addEventListener('blur', function() { 6655 if (this.value && !isValidUrl(this.value)) { 6656 this.classList.add('is-invalid'); 6657 6658 // Add validation message if doesn't exist 6659 if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) { 6660 const feedback = document.createElement('div'); 6661 feedback.classList.add('invalid-feedback'); 6662 feedback.textContent = translations['invalidUrl'] || 'Please enter a valid URL starting with http:// or https://'; 6663 this.parentNode.appendChild(feedback); 6664 } 6665 } else { 6666 this.classList.remove('is-invalid'); 6667 } 6668 }); 6669 } else if (field === 'phone' || input.type === 'tel') { 6670 // Phone validation 6671 input.addEventListener('blur', function() { 6672 if (this.value && !isValidPhoneNumber(this.value)) { 6673 this.classList.add('is-invalid'); 6674 6675 // Add validation message if doesn't exist 6676 if (!this.nextElementSibling || !this.nextElementSibling.classList.contains('invalid-feedback')) { 6677 const feedback = document.createElement('div'); 6678 feedback.classList.add('invalid-feedback'); 6679 feedback.textContent = translations['invalidPhone'] || 'Please enter a valid phone number'; 6680 this.parentNode.appendChild(feedback); 6681 } 6682 } else { 6683 this.classList.remove('is-invalid'); 6684 } 6685 }); 6686 } 6687} 6688 6689function updateFormFields(fields, docTypeId) { 6690 const formFieldsSection = document.getElementById('dynamic_form_fields'); 6691 formFieldsSection.innerHTML = ''; 6692 6693 // Create form fields row
6694 const row = document.createElement('div'); 6695 row.classList.add('row'); 6696 formFieldsSection.appendChild(row); 6697 6698 // Field definitions with their properties 6699 const fieldDefinitions = { 6700 lastUpdated: { 6701 id: 'lastUpdated', 6702 labelId: 'lastUpdated', 6703 label: 'Last Updated', 6704 type: 'date', 6705 placeholderId: 'lastUpdatedPlaceholder', 6706 placeholder: 'Select last updated date', 6707 required: true, 6708 colSize: 6, 6709 defaultValue: new Date().toISOString().split('T')[0] // Today's date 6710 }, 6711 effectiveDate: { 6712 id: 'effectiveDate', 6713 labelId: 'effectiveDate', 6714 label: 'Effective Date', 6715 type: 'date', 6716 placeholderId: 'effectiveDatePlaceholder', 6717 placeholder: 'Select effective date', 6718 required: true, 6719 colSize: 6, 6720 defaultValue: new Date().toISOString().split('T')[0] // Today's date 6721 }, 6722 name: { 6723 labelId: 'companyName', 6724 label: 'Company Name', 6725 type: 'text', 6726 placeholderId: 'companyNamePlaceholder', 6727 placeholder: 'Enter your company name', 6728 required: true, 6729 colSize: 12, 6730 companyField: 'companyName' 6731 }, 6732 serviceName: { 6733 labelId: 'serviceName', 6734 label: 'Service Name', 6735 type: 'text', 6736 placeholderId: 'serviceNamePlaceholder', 6737 placeholder: 'Enter your service name', 6738 required: true, 6739 colSize: 12, 6740 companyField: 'serviceName' 6741 }, 6742 address: { 6743 labelId: 'address', 6744 label: 'Address', 6745 type: 'text', 6746 placeholderId: 'companyAddressPlaceholder', 6747 placeholder: 'Enter your company address', 6748 required: true, 6749 colSize: 12, 6750 companyField: 'address' 6751 }, 6752 country: { 6753 labelId: 'country', 6754 label: 'Country', 6755 type: 'text', 6756 placeholderId: 'companyCountryPlaceholder', 6757 placeholder: 'Enter your country', 6758 required: true, 6759 colSize: 6, 6760 companyField: 'country' 6761 }, 6762 email: { 6763 labelId: 'email', 6764 label: 'Email', 6765 type: 'email', 6766 placeholderId: 'enterYourEmail', 6767 placeholder: 'Enter contact email', 6768 required: true, 6769 colSize: 6, 6770 companyField: 'email', 6771 validate: true 6772 }, 6773 phone: { 6774 labelId: 'phone', 6775 label: 'Phone', 6776 type: 'tel', 6777 placeholderId: 'companyPhonePlaceholder', 6778 placeholder: 'Enter contact phone number', 6779 required: true, 6780 colSize: 6, 6781 companyField: 'phone', 6782 validate: true 6783 }, 6784 websiteUrl: { 6785 labelId: 'websiteUrl', 6786 label: 'Website URL', 6787 type: 'url', 6788 placeholderId: 'companyWebsitePlaceholder', 6789 placeholder: 'https://example.com', 6790 required: true, 6791 colSize: 12, 6792 companyField: 'websiteUrl', 6793 validate: true 6794 }, 6795 privacyUrl: { 6796 labelId: 'privacyUrl', 6797 label: 'Privacy URL', 6798 type: 'url', 6799 placeholderId: 'companyPrivacyPlaceholder', 6800 placeholder: 'https://example.com/privacy', 6801 required: true, 6802 colSize: 12, 6803 companyField: 'privacyUrl', 6804 validate: true 6805 }, 6806 termsUrl: { 6807 labelId: 'termsUrl', 6808 label: 'Terms of Use URL', 6809 type: 'url', 6810 placeholderId: 'companyTermsPlaceholder', 6811 placeholder: 'https://example.com/terms', 6812 required: true, 6813 colSize: 12, 6814 companyField: 'termsUrl', 6815 validate: true 6816 }, 6817 pricingUrl: { 6818 labelId: 'pricingUrl', 6819 label: 'Pricing URL', 6820 type: 'url', 6821 placeholderId: 'companyPricingPlaceholder', 6822 placeholder: 'https://example.com/pricing', 6823 required: true, 6824 colSize: 12, 6825 companyField: 'pricingUrl', 6826 validate: true 6827 }, 6828 companyRegNo: { 6829 labelId: 'companyRegNo', 6830 label: 'Company Registration Number', 6831 type: 'text', 6832 placeholderId: 'companyRegNoPlaceholder', 6833 placeholder: 'Enter company registration number', 6834 required: true, 6835 colSize: 6, 6836 companyField: 'companyRegNo' 6837 }, 6838 legalEntity: { 6839 labelId: 'legalEntity', 6840 label: 'Company Legal Entity', 6841 type: 'text', 6842 placeholderId: 'companyLegalEntityPlaceholder', 6843 placeholder: 'Enter company legal entity', 6844 required: true, 6845 colSize: 12, 6846 companyField: 'legalEntity' 6847 } 6848 }; 6849 6850 // Access company data if available 6851 const companyData = window.companyData || {}; 6852 6853 // Create form fields based on the selected document type 6854 fields.forEach(fieldName => { 6855 const field = fieldDefinitions[fieldName]; 6856 if (!field) return; 6857 6858 const col = document.createElement('div'); 6859 col.classList.add('col-md-' + field.colSize, 'mb-3'); 6860 row.appendChild(col); 6861 6862 const label = document.createElement('label'); 6863 label.classList.add('form-label'); 6864 label.htmlFor = fieldName; 6865 label.textContent = translations[field.labelId] || field.label; 6866 if (field.required) { 6867 label.innerHTML += ' <span class="text-danger">*</span>'; 6868 } 6869 col.appendChild(label); 6870 6871 const input = document.createElement('input'); 6872 input.type = field.type; 6873 input.id = fieldName; 6874 input.name = fieldName; 6875 input.classList.add('form-control'); 6876 if (field.placeholder) input.placeholder = translations[field.placeholderId] || field.placeholder; 6877 if (field.required) input.required = true; 6878 6879 // Pre-fill with data from company if available 6880 if (field.companyField && companyData[field.companyField]) { 6881 input.value = companyData[field.companyField]; 6882 } else if (field.defaultValue) { 6883 input.value = field.defaultValue; 6884 } 6885 6886 col.appendChild(input); 6887 6888 // Add validation for specific field types 6889 if (field.validate || field.type === 'email' || field.type === 'url' || field.type === 'tel') { 6890 addInputValidation(fieldName); 6891 } 6892 }); 6893} 6894 6895async function handleGenerateDocument() { 6896 // Get the selected document type and format 6897 const activeBtn = document.querySelector('.doc-type-btn[aria-pressed="true"]'); 6898 if (!activeBtn) { 6899 showAlert(translations['selectDocTypeFirst'] || 'Please select a document type first.', "warning"); 6900 return; 6901 } 6902 6903 // Get form values and validate 6904 const formFields = document.getElementById('dynamic_form_fields').querySelectorAll('input'); 6905 const formData = {}; 6906 let isValid = true; 6907 let invalidFields = []; 6908 6909 formFields.forEach(field => { 6910 // Check required fields 6911 if (field.required && !field.value) { 6912 field.classList.add('is-invalid'); 6913 isValid = false;
6914 invalidFields.push(field.id); 6915 return; 6916 } 6917 6918 // Validate field types 6919 if (field.value) { 6920 if ((field.id === 'email' || field.type === 'email') && !isValidEmail(field.value)) { 6921 field.classList.add('is-invalid'); 6922 isValid = false; 6923 invalidFields.push(field.id); 6924 return; 6925 } 6926 6927 if ((field.id === 'websiteUrl' || field.id === 'privacyUrl' || 6928 field.id === 'termsUrl' || field.id === 'pricingUrl' || 6929 field.type === 'url') && !isValidUrl(field.value)) { 6930 field.classList.add('is-invalid'); 6931 isValid = false; 6932 invalidFields.push(field.id); 6933 return; 6934 } 6935 6936 if ((field.id === 'phone' || field.type === 'tel') && !isValidPhoneNumber(field.value)) { 6937 field.classList.add('is-invalid'); 6938 isValid = false; 6939 invalidFields.push(field.id); 6940 return; 6941 } 6942 } 6943 6944 field.classList.remove('is-invalid'); 6945 formData[field.id] = field.value; 6946 }); 6947 6948 if (!isValid) { 6949 // Create detailed validation message 6950 let errorMessage = translations['fillRequiredFields'] || 'Please fix the following issues:'; 6951 6952 if (invalidFields.includes('email')) { 6953 errorMessage += '\n- ' + (translations['invalidEmail'] || 'Please enter a valid email address'); 6954 } 6955 6956 if (invalidFields.some(field => field.includes('Url'))) { 6957 errorMessage += '\n- ' + (translations['invalidUrl'] || 'URLs must start with http:// or https://'); 6958 } 6959 6960 if (invalidFields.includes('phone')) { 6961 errorMessage += '\n- ' + (translations['invalidPhone'] || 'Please enter a valid phone number'); 6962 } 6963 6964 if (invalidFields.some(field => !field.includes('Url') && field !== 'email' && field !== 'phone')) { 6965 errorMessage += '\n- ' + (translations['missingRequiredFields'] || 'Fill in all required fields marked with *'); 6966 } 6967 6968 showAlert(errorMessage, "error"); 6969 return; 6970 } 6971 6972 const docType = activeBtn.dataset.docType; 6973 const format = document.getElementById('document_format').value; 6974 6975 // Get the selected project ID 6976 const selectedProjectId = document.getElementById('select_domain').value; 6977 6978 // Get the selected language ID from the dropdown 6979 const languageDropdown = document.getElementById('select_language'); 6980 const selectedLanguageId = languageDropdown ? parseInt(languageDropdown.value) : 2; 6981 6982 console.log('Generating document with language ID:', selectedLanguageId); 6983 6984 // Store the selected language ID globally for use in other functions 6985 window.currentLanguageId = selectedLanguageId; 6986 6987 // Store form data globally for use in save & download 6988 window.currentDocumentData = { 6989 docType: docType, 6990 format: format, 6991 projectId: selectedProjectId, 6992 languageId: selectedLanguageId, 6993 formData: formData 6994 }; 6995 6996 const generateBtn = document.getElementById('generate_document_btn'); 6997 const originalText = generateBtn.textContent; 6998 generateBtn.disabled = true; 6999 generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7000 (translations['generating'] || 'Generating...'); 7001 7002 try { 7003 // First save the company information (but not the document) 7004 const saveResult = await checkCompanyInformation(selectedProjectId); 7005 7006 if (!saveResult) { 7007 // If saving failed, stop the process 7008 generateBtn.disabled = false; 7009 generateBtn.textContent = originalText; 7010 return; 7011 } 7012 7013 // Fetch the document template from the server 7014 let documentContent; 7015 try { 7016 // Update button text to indicate template fetching 7017 generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7018 (translations['fetchingTemplate'] || 'Fetching template...'); 7019 7020 console.log('Fetching template for document type:', docType, 'language:', selectedLanguageId); 7021 7022 // Get the document template using the selected language 7023 const templateData = await getDocumentTemplate(docType, selectedLanguageId); 7024 7025 console.log('Template received for language', selectedLanguageId, ':', templateData); 7026 7027 if (!templateData || !templateData.templateText) { 7028 throw new Error('No template found for the selected document type and language'); 7029 } 7030 7031 generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7032 (translations['generating'] || 'Generating...'); 7033 7034 // Process the template by replacing placeholders with form data 7035 documentContent = processTemplate(templateData.templateText, formData); 7036 7037 // §10: a cookie policy has to say WHICH cookies. Fetch the composed section 7038 // from the domain's actual scan and fold it in. Failure is non-fatal â the 7039 // merchant still gets the templated document rather than nothing. 7040 if (docType === 'cookiePolicy') { 7041 try { 7042 const policy = await fcFetchCookiePolicy(selectedProjectId, fcSelectedLanguageCode()); 7043 documentContent = fcApplyCookiePolicySection(documentContent, policy); 7044 } catch (policyErr) { 7045 console.error('Could not add the tracker section:', policyErr); 7046 } 7047 } 7048 7049 console.log('Document content generated for language:', selectedLanguageId); 7050 7051 // Store the generated content globally for save & download 7052 window.currentDocumentData.generatedContent = documentContent; 7053 7054 } catch (error) { 7055 console.error('Error processing template:', error); 7056 displayAlertDocument('Failed to process document template: ' + error.message, 'error'); 7057 generateBtn.disabled = false;
7058 generateBtn.textContent = originalText; 7059 return; 7060 } 7061 7062 // Display the generated document (WITHOUT saving to database) 7063 // Pass null as documentId since we haven't saved it yet 7064 displayGeneratedDocument(documentContent, docType, format, null, selectedLanguageId); 7065 7066 // Reset button 7067 generateBtn.disabled = false; 7068 generateBtn.textContent = originalText; 7069 7070 // Show success message 7071 displayAlertDocument(translations['documentGeneratedSuccessfully'] || 'Document generated successfully. Click "Save & Download" to save and download it.', 'success'); 7072 7073 } catch (error) { 7074 // Handle unexpected errors 7075 displayAlertDocument('Error generating document: ' + error.message, 'error'); 7076 console.error("Error generating document:", error); 7077 7078 // Reset button 7079 generateBtn.disabled = false; 7080 generateBtn.textContent = originalText; 7081 } 7082} 7083 7084async function loadCompanyInformation(projectDomainId) { 7085 const data = { sid: getSid(), projectDomainId: projectDomainId }; 7086 const response = await postRequest(endpoint + '/document/getCompany', data); 7087 if (response.ok) { 7088 const responseData = await response.json(); 7089 if (responseData.statusCode === 0 && responseData.company) { 7090 window.companyData = responseData.company; 7091 } else { 7092 window.companyData = { 7093 name: '', 7094 address: '', 7095 country: '', 7096 email: '', 7097 phone: '', 7098 websiteUrl: '', 7099 serviceName: '', 7100 pricingUrl: '', 7101 privacyUrl: '', 7102 termsUrl: '', 7103 companyRegNo: '', 7104 legalEntity: '' 7105 } 7106 } 7107 } 7108 7109} 7110 7111function populateCompanyData(company) { 7112 $('#name').val(company.name || ''); 7113 $('#address').val(company.address || ''); 7114 $('#country').val(company.country || ''); 7115 $('#email').val(company.email || ''); 7116 $('#phone').val(company.phone || ''); 7117 $('#websiteUrl').val(company.websiteUrl || ''); 7118 $('#serviceName').val(company.serviceName || ''); 7119 $('#pricingUrl').val(company.pricingUrl || ''); 7120 $('#privacyUrl').val(company.privacyUrl || ''); 7121 $('#termsUrl').val(company.termsUrl || ''); 7122 $('#companyRegNo').val(company.companyRegNo || ''); 7123 $('#legalEntity').val(company.legalEntity || ''); 7124} 7125 7126async function saveCompanyInformation(projectDomainId, companyData) { 7127 // Prepare request data 7128 const data = { 7129 sid: getSid(), 7130 projectDomainId: projectDomainId, 7131 company: companyData 7132 }; 7133 7134 console.log("Saving company data:", data); // Debug log 7135 7136 // Show save indicator/spinner if needed 7137 const saveButton = $('#save_company_btn'); 7138 const originalText = saveButton.text(); 7139 if (saveButton.length) { 7140 saveButton.prop('disabled', true); 7141 saveButton.html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...'); 7142 } 7143 7144 try { 7145 // Send request to save company data - using correct endpoint path 7146 const response = await postRequest(endpoint + '/document/saveCompany', data); 7147 7148 console.log("Save company response status:", response.status); // Debug log 7149 7150 if (response.ok) { 7151 const responseData = await response.json(); 7152 console.log("Save company response data:", responseData); // Debug log 7153 7154 if (responseData.statusCode === 0) { 7155 // Show success message 7156 displayAlertDocument('Company information saved successfully', 'success'); 7157 7158 // Update form with returned data if needed 7159 if (responseData.company) { 7160 populateCompanyData(responseData.company); 7161 } 7162 7163 return true; 7164 } else { 7165 // Handle error 7166 displayAlertDocument('Error saving company information: ' + (responseData.statusMessage || 'Unknown error'), 'error'); 7167 console.error("Error saving company information:", responseData.statusMessage); 7168 return false;
7169 } 7170 } else { 7171 // Handle network error 7172 displayAlertDocument('Network error while saving company information: ' + response.status, 'error'); 7173 console.error("Network error saving company information:", response.status); 7174 return false; 7175 } 7176 } catch (error) { 7177 // Handle unexpected errors 7178 displayAlertDocument('Unexpected error while saving company information: ' + error.message, 'error'); 7179 console.error("Exception while saving company information:", error); 7180 return false; 7181 } finally { 7182 // Reset button state if button exists 7183 if (saveButton.length) { 7184 saveButton.prop('disabled', false); 7185 saveButton.text(originalText); 7186 } 7187 } 7188} 7189 7190async function checkCompanyInformation(projectDomainId) { 7191 initialCompanyData = window.companyData; 7192 7193// Collect all company data from form fields 7194 const companyData = { 7195 name: $('#name').val() || initialCompanyData.name, 7196 address: $('#address').val() || initialCompanyData.address, 7197 country: $('#country').val() || initialCompanyData.country, 7198 email: $('#email').val() || initialCompanyData.email, 7199 phone: $('#phone').val() || initialCompanyData.phone, 7200 websiteUrl: $('#websiteUrl').val() || initialCompanyData.websiteUrl, 7201 serviceName: $('#serviceName').val() || initialCompanyData.serviceName, 7202 pricingUrl: $('#pricingUrl').val() || initialCompanyData.pricingUrl, 7203 privacyUrl: $('#privacyUrl').val() || initialCompanyData.privacyUrl, 7204 termsUrl: $('#termsUrl').val() || initialCompanyData.termsUrl, 7205 companyRegNo: $('#companyRegNo').val() || initialCompanyData.companyRegNo, 7206 legalEntity: $('#legalEntity').val() || initialCompanyData.legalEntity 7207 }; 7208 7209 7210const saveResult = await saveCompanyInformation(projectDomainId, companyData); 7211 7212return saveResult; 7213} 7214 7215function processTemplate(template, formData) { 7216 // Replace all placeholders in the template with actual values 7217 let processedTemplate = template; 7218 7219 // Map form fields to template placeholders 7220 const placeholderMap = { 7221 'name': '{{COMPANY_NAME}}', 7222 'address': '{{COMPANY_ADDRESS}}', 7223 'country': '{{COUNTRY}}', 7224 'email': '{{EMAIL}}', 7225 'phone': '{{PHONE_NUMBER}}', 7226 'websiteUrl': '{{WEBSITE_URL}}', 7227 'lastUpdated': '{{LAST_UPDATED}}', 7228 'effectiveDate': '{{EFFECTIVE_DATE}}', 7229 'serviceName': '{{SERVICE_NAME}}', 7230 'companyRegNo': '{{COMPANY_REG_NO}}', 7231 'legalEntity': '{{LEGAL_ENTITY}}', 7232 'privacyUrl': '{{PRIVACY_URL}}', 7233 'termsUrl': '{{TERMS_URL}}', 7234 'pricingUrl': '{{PRICING_URL}}' 7235 }; 7236 7237 // Replace each placeholder with the actual value 7238 for (const [field, placeholder] of Object.entries(placeholderMap)) { 7239 let value = formData[field] || ''; 7240 7241 // Format dates nicely 7242 if (field === 'lastUpdated' || field === 'effectiveDate') { 7243 value = formatDateForDisplay(value); 7244 } 7245 7246 processedTemplate = processedTemplate.replace(new RegExp(placeholder, 'g'), value); 7247 } 7248 7249 // For HTML format, wrap with proper HTML structure 7250 // Replace newlines with <br> tags 7251 processedTemplate = processedTemplate.replace(/\n/g, '<br>'); 7252 7253 // Wrap the content in a document container 7254 return `<div class="document-container">${processedTemplate}</div>`; 7255} 7256 7257function downloadDocumentClientSide(format, documentId = null, languageId = null) { 7258 // Get the selected language ID 7259 const selectedLanguageId = languageId || 7260 window.currentLanguageId || 7261 (document.getElementById('select_language') ? document.getElementById('select_language').value : null) || 7262 2; // Default to 2 if not found 7263 7264 console.log('Download starting with language ID:', selectedLanguageId); 7265 7266 // For the Save & Download flow, we don't show additional loading indicators 7267 // since the Save & Download button already shows loading state 7268 7269 try { 7270 // Get the document content that's already displayed 7271 const contentContainer = document.querySelector('.document-preview'); 7272 if (!contentContainer) { 7273 throw new Error('Document content not found'); 7274 } 7275 7276 // Get the document title 7277 const title = document.querySelector('#document_result h4')?.textContent || 'document'; 7278 const sanitizedTitle = title.replace(/\s+/g, '_').toLowerCase(); 7279 7280 // Get the current date for the filename 7281 const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, ''); 7282 7283 if (format === 'html') { 7284 // For HTML, ensure we're returning the proper HTML document with color overrides 7285 let htmlContent = contentContainer.innerHTML; 7286 const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') || 7287 htmlContent.trim().toLowerCase().includes('<html'); 7288 7289 let content; 7290 if (isCompleteHtml) { 7291 // If it's already complete HTML, parse it to add our CSS overrides 7292 const parser = new DOMParser(); 7293 const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 7294 7295 // Add style to override red accent color 7296 const styleOverride = document.createElement('style'); 7297 styleOverride.textContent = ` 7298 :root { 7299 --accent-color: #555 !important; /* Override red with gray */ 7300 } 7301 .variable { 7302 color: #555 !important; /* Override any red variable text */ 7303 } 7304 .highlight { 7305 border-left-color: #3498db !important; /* Use secondary color instead of accent */ 7306 } 7307 .important-notice { 7308 border-left-color: #3498db !important; /* Use secondary color instead of accent */ 7309 } 7310 `; 7311 7312 // Add the style override to the head 7313 htmlDoc.head.appendChild(styleOverride); 7314 7315 // Add language metadata to the document 7316 const languageMeta = document.createElement('meta'); 7317 languageMeta.setAttribute('name', 'document-language-id'); 7318 languageMeta.setAttribute('content', selectedLanguageId.toString()); 7319 htmlDoc.head.appendChild(languageMeta); 7320 7321 // Add document ID metadata if available 7322 if (documentId) { 7323 const docIdMeta = document.createElement('meta'); 7324 docIdMeta.setAttribute('name', 'document-id'); 7325 docIdMeta.setAttribute('content', documentId.toString()); 7326 htmlDoc.head.appendChild(docIdMeta); 7327 } 7328 7329 content = '<!DOCTYPE html>\n' + htmlDoc.documentElement.outerHTML; 7330 } else { 7331 // If it's not complete HTML, wrap it properly and add our overrides 7332 content = `<!DOCTYPE html> 7333<html> 7334<head> 7335 <meta charset="UTF-8"> 7336 <meta name="document-language-id" content="${selectedLanguageId}"> 7337 ${documentId ? `<meta name="document-id" content="${documentId}">` : ''} 7338 <title>${title}</title> 7339 <style> 7340 body { font-family: Arial, sans-serif; margin: 2em; } 7341 .document-container { margin: 0 auto; line-height: 1.5; } 7342 .variable { color: #555 !important; } /* Remove red color for variables */ 7343 [style*="color: var(--accent-color)"] { color: #555 !important; } 7344 </style> 7345</head> 7346<body> 7347 <div class="document-container"> 7348 ${htmlContent} 7349 </div> 7350</body> 7351</html>`; 7352 } 7353 7354 // Create a Blob with the HTML content 7355 const blob = new Blob([content], { type: 'text/html' }); 7356 // Include language ID and document ID in filename if available 7357 const fileName = documentId ? 7358 `${sanitizedTitle}_id${documentId}_lang${selectedLanguageId}_${currentDate}.html` : 7359 `${sanitizedTitle}_lang${selectedLanguageId}_${currentDate}.html`; 7360 7361 // Create download link 7362 const url = window.URL.createObjectURL(blob); 7363 const a = document.createElement('a'); 7364 a.style.display = 'none'; 7365 a.href = url; 7366 a.download = fileName; 7367 document.body.appendChild(a); 7368 a.click(); 7369 7370 // Clean up 7371 window.URL.revokeObjectURL(url); 7372 document.body.removeChild(a); 7373 7374 console.log('HTML download completed'); 7375 7376 } else if (format === 'pdf') { 7377 // For PDF, use the modified function that removes red colors and includes language 7378 generatePdfFromIframe(contentContainer, sanitizedTitle, currentDate, null, null, null, selectedLanguageId, documentId); 7379 } else { 7380 throw new Error('Unsupported format: ' + format); 7381 } 7382 } catch (error) { 7383 console.error("Download error:", error); 7384 displayAlertDocument('Download failed: ' + error.message, 'error'); 7385 } 7386} 7387 7388function formatDateForDisplay(dateString) { 7389 if (!dateString) return ''; 7390 7391 const date = new Date(dateString); 7392 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 7393 return date.toLocaleDateString(undefined, options); 7394} 7395 7396function displayAlertDocument(message, type) { 7397 // Toast, not an inline alert: the old version also removed every .alert on the 7398 // page, taking the static hint boxes with it. 7399 showToast(message, type === 'error' ? 'error' : type === 'success' ? 'success' : 'info'); 7400} 7401 7402function downloadDocument(documentId, format) { 7403 // Show loading indicator 7404 const container = document.getElementById('document_result'); 7405 const loadingIndicator = document.createElement('div'); 7406 loadingIndicator.classList.add('text-center', 'my-3', 'download-loading'); 7407 loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7408 (translations['downloading'] || 'Downloading...'); 7409 container.appendChild(loadingIndicator); 7410 7411 // Get the current session ID 7412 const currentSid = getSid(); 7413 console.log("Using session ID:", currentSid); // Log for debugging 7414 7415 // Construct the URL for downloading 7416 const downloadUrl = `${endpoint}/document/download?sid=${currentSid}&documentId=${documentId}&format=${format}`; 7417 7418 // Use fetch API with credentials included 7419 fetch(downloadUrl, { 7420 method: 'GET', 7421 credentials: 'include', // Important: include cookies for session-based auth 7422 headers: { 7423 // Add any additional auth headers that your system might require 7424 'X-Auth-Token': currentSid 7425 } 7426 }) 7427 .then(response => { 7428 // Remove loading indicator 7429 const indicator = document.querySelector('.download-loading'); 7430 if (indicator) { 7431 container.removeChild(indicator); 7432 } 7433 7434 if (!response.ok) { 7435 return response.json().then(errorData => { 7436 throw new Error(`Download failed: ${errorData.statusMessage || response.statusText} (Code: ${errorData.statusCode || response.status})`); 7437 }); 7438 } 7439
7440 // Check if it's a JSON response (might be an error) 7441 const contentType = response.headers.get('content-type'); 7442 if (contentType && contentType.includes('application/json')) { 7443 return response.json().then(data => { 7444 if (data.statusCode !== 0) { 7445 throw new Error(data.statusMessage || 'Download failed'); 7446 } 7447 return data; // Might be JSON file data 7448 }); 7449 } 7450 7451 // Process as regular file download 7452 return response.blob().then(blob => { 7453 // If blob is empty or too small, it might be an error 7454 if (blob.size < 10) { 7455 throw new Error('Downloaded file appears to be empty or invalid'); 7456 } 7457 7458 // Create a proper filename 7459 const currentDate = new Date().toISOString().slice(0, 10); 7460 let docTypeName = document.querySelector('#document_result h4')?.textContent || 'document'; 7461 docTypeName = docTypeName.replace(/\s+/g, '_').toLowerCase(); 7462 const fileName = `${docTypeName}_${currentDate}.${format}`; 7463 7464 // Create and trigger download 7465 const url = window.URL.createObjectURL(blob); 7466 const a = document.createElement('a'); 7467 a.style.display = 'none'; 7468 a.href = url; 7469 a.download = fileName; 7470 document.body.appendChild(a); 7471 a.click(); 7472 7473 // Clean up 7474 window.URL.revokeObjectURL(url); 7475 document.body.removeChild(a); 7476 displayAlertDocument(translations['downloadCompleted'] || 'Document downloaded successfully.', 'success'); 7477 }); 7478 }) 7479 .catch(error => { 7480 // Remove loading indicator if still present 7481 const indicator = document.querySelector('.download-loading'); 7482 if (indicator) { 7483 container.removeChild(indicator); 7484 } 7485 console.error("Download error:", error); 7486 // Auth error specific handling 7487 if (error.message.includes('Auth') || error.message.includes('authentication') || error.message.includes('Code: 3')) { 7488 displayAlertDocument('Authentication failed. Please refresh the page and try again.', 'error'); 7489 refreshSession().then(() => { 7490 displayAlertDocument('Session refreshed. Please try downloading again.', 'info'); 7491 }).catch(refreshError => { 7492 console.error("Session refresh failed:", refreshError); 7493 }); 7494 } else { 7495 displayAlertDocument('Download failed: ' + error.message, 'error'); 7496 } 7497 }); 7498} 7499 7500async function refreshSession() { 7501 try { 7502 const response = await fetch(`${endpoint}/auth/refresh`, { 7503 method: 'POST', 7504 credentials: 'include', 7505 headers: { 7506 'Content-Type': 'application/json' 7507 }, 7508 body: JSON.stringify({ sid: getSid() }) 7509 }); 7510 if (!response.ok) { 7511 throw new Error('Failed to refresh session'); 7512 } 7513 const data = await response.json(); 7514 if (data.statusCode !== 0) { 7515 throw new Error(data.statusMessage || 'Session refresh failed'); 7516 } 7517 // If your system stores the session ID somewhere 7518 if (data.sid) { 7519 // Store the new session ID however your application does that 7520 localStorage.setItem('sid', data.sid); 7521 } 7522 return true; 7523 } catch (error) { 7524 console.error("Session refresh error:", error); 7525 throw error; 7526 } 7527} 7528 7529function displayGeneratedDocument(documentContent, docType, format, documentId, languageId) { 7530 // Get the result section 7531 const resultSection = document.getElementById('document_result'); 7532 7533 // Clear any existing content 7534 resultSection.innerHTML = ''; 7535 7536 // Make the result section visible 7537 resultSection.classList.remove('d-none'); 7538 7539 // Store language ID as a data attribute for use in download 7540 resultSection.dataset.languageId = languageId || window.currentLanguageId || 2; 7541 7542 // Create the result header 7543 const headerRow = document.createElement('div'); 7544 headerRow.classList.add('d-flex', 'justify-content-between', 'align-items-start', 'mb-3'); 7545 resultSection.appendChild(headerRow); 7546 7547 // Add title (left side) 7548 const titleContainer = document.createElement('div'); 7549 titleContainer.classList.add('flex-grow-1'); 7550 headerRow.appendChild(titleContainer); 7551 7552 const title = document.createElement('h4'); 7553 title.textContent = translations[docType] || getDocumentTypeName(docType); 7554 titleContainer.appendChild(title); 7555 7556 // Add button and disclaimer container (right side) 7557 const buttonContainer = document.createElement('div'); 7558 buttonContainer.classList.add('d-flex', 'flex-column', 'align-items-end'); 7559 headerRow.appendChild(buttonContainer); 7560 7561 // Add Save & Download button 7562 const saveDownloadBtn = document.createElement('button'); 7563 saveDownloadBtn.type = 'button'; 7564 saveDownloadBtn.classList.add('btn', 'btn-success', 'mb-2'); // Added mb-2 for spacing 7565 saveDownloadBtn.innerHTML = '<i class="fa fa-save me-2"></i>' + (translations['saveAndDownload'] || 'Save & Download'); 7566 7567 // Store document info as data attributes for save & download 7568 saveDownloadBtn.dataset.docType = docType; 7569 saveDownloadBtn.dataset.format = format; 7570 saveDownloadBtn.dataset.languageId = languageId || window.currentLanguageId || 2; 7571 7572 // Add click handler for Save & Download 7573 saveDownloadBtn.addEventListener('click', async () => { 7574 console.log('Save & Download initiated'); 7575 7576 // Show loading state 7577 const originalText = saveDownloadBtn.innerHTML; 7578 saveDownloadBtn.disabled = true; 7579 saveDownloadBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7580 (translations['savingAndDownloading'] || 'Saving & Downloading...'); 7581 7582 try { 7583 if (!window.currentDocumentData) { 7584 throw new Error('No document data available. Please regenerate the document.'); 7585 } 7586 7587 // First, save the document to the database 7588 const documentSaveData = { 7589 sid: getSid(), 7590 projectDomainId: window.currentDocumentData.projectId, 7591 documentTypeCode: window.currentDocumentData.docType, 7592 lastUpdated: window.currentDocumentData.formData.lastUpdated, 7593 effectiveDate: window.currentDocumentData.formData.effectiveDate, 7594 generatedText: window.currentDocumentData.generatedContent, 7595 languageId: window.currentDocumentData.languageId 7596 }; 7597 7598 console.log('Saving document to database with language ID:', window.currentDocumentData.languageId); 7599 7600 const response = await postRequest(endpoint + '/document/saveDocument', documentSaveData); 7601 7602 if (!response.ok) { 7603 throw new Error('Failed to save document: ' + response.status); 7604 } 7605 7606 const responseData = await response.json(); 7607 7608 if (responseData.statusCode !== 0) { 7609 throw new Error('Error saving document: ' + (responseData.statusMessage || 'Unknown error')); 7610 } 7611 7612 console.log('Document saved successfully with ID:', responseData.document.id); 7613 7614 // Update button text to indicate download is starting 7615 saveDownloadBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7616 (translations['downloading'] || 'Downloading...'); 7617 7618 // Now proceed with the download 7619 const selectedLanguageId = window.currentDocumentData.languageId; 7620 downloadDocumentClientSide(format, responseData.document.id, selectedLanguageId); 7621 7622 // Show success message 7623 displayAlertDocument(translations['documentSavedAndDownloaded'] || 'Document saved and download started successfully.', 'success'); 7624 7625 } catch (error) { 7626 console.error('Save & Download error:', error); 7627 displayAlertDocument('Save & Download failed: ' + error.message, 'error'); 7628 } finally { 7629 // Reset button state 7630 saveDownloadBtn.disabled = false;
7631 saveDownloadBtn.innerHTML = originalText; 7632 } 7633 }); 7634 7635 buttonContainer.appendChild(saveDownloadBtn); 7636 7637 // Add legal disclaimer text 7638 const disclaimerText = document.createElement('small'); 7639 disclaimerText.classList.add('text-muted', 'text-end', 'fst-italic'); 7640 disclaimerText.style.maxWidth = '450px'; // Increased width to accommodate 2 lines 7641 disclaimerText.style.lineHeight = '1.3'; 7642 disclaimerText.style.whiteSpace = 'nowrap'; // Prevent additional wrapping 7643 7644 // Get translation or use default text with icon 7645 const disclaimerTextContent = translations['legalDisclaimerText'] || 7646 'We strongly recommend having the final version reviewed<br>by a qualified legal professional to ensure compliance.'; 7647 7648 disclaimerText.innerHTML = '<span style="white-space: normal; display: inline-block;">' + 7649 '<i class="fa fa-exclamation-triangle me-1" style="color: #ffc107;"></i>' + 7650 disclaimerTextContent + 7651 '</span>'; 7652 buttonContainer.appendChild(disclaimerText); 7653 7654 // Create container for the document content 7655 const contentContainer = document.createElement('div'); 7656 contentContainer.classList.add('document-preview', 'p-3', 'border', 'rounded'); 7657 contentContainer.dataset.languageId = languageId || window.currentLanguageId || 2; 7658 resultSection.appendChild(contentContainer); 7659 7660 // Add the document content 7661 contentContainer.innerHTML = documentContent; 7662 7663 // Scroll to the result section 7664 resultSection.scrollIntoView({ behavior: 'smooth' }); 7665} 7666 7667function generatePdfFromIframe(contentElement, title, date, container, loadingIndicator, documentType, languageId, documentId) { 7668 // Get the language ID if not provided 7669 const selectedLanguageId = languageId || 7670 window.currentLanguageId || 7671 (document.getElementById('select_language') ? document.getElementById('select_language').value : null) || 7672 2; // Default fallback 7673 7674 console.log('Generating PDF with language ID:', selectedLanguageId); 7675 7676 // Create a loading indicator if none provided 7677 let createdLoadingIndicator = false; 7678 if (!loadingIndicator) { 7679 // Create a temporary loading indicator 7680 loadingIndicator = document.createElement('div'); 7681 loadingIndicator.classList.add('text-center', 'my-3', 'pdf-loading'); 7682 loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7683 (translations['preparingPdf'] || 'Preparing PDF...'); 7684 7685 // Find a good place to insert it 7686 const targetContainer = container || 7687 document.getElementById('document_result') || 7688 document.getElementById('document_generator_container') || 7689 document.body; 7690 7691 targetContainer.appendChild(loadingIndicator); 7692 createdLoadingIndicator = true; 7693 } 7694 7695 // Check if we need to load jsPDF library 7696 loadJsPdfLibraries(() => { 7697 try { 7698 // Update loading message safely 7699 if (loadingIndicator && loadingIndicator.innerHTML !== undefined) { 7700 loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7701 'Preparing document for PDF conversion...'; 7702 } 7703 7704 let htmlContent = contentElement.innerHTML; 7705 7706 // Check if it's a complete HTML document 7707 const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') || 7708 htmlContent.trim().toLowerCase().includes('<html'); 7709 7710 // Create an iframe to properly render the HTML with all CSS 7711 const iframe = document.createElement('iframe'); 7712 iframe.style.width = '1000px'; // Set to a suitable width 7713 iframe.style.height = '100px';
7713 // Will be adjusted based on content 7714 iframe.style.position = 'absolute'; 7715 iframe.style.left = '-9999px'; // Hide the iframe 7716 document.body.appendChild(iframe); 7717 7718 // Set iframe content 7719 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 7720 7721 if (isCompleteHtml) { 7722 // If it's a complete HTML document, parse it to add our CSS overrides 7723 const parser = new DOMParser(); 7724 const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 7725 7726 // Add style to override red accent color 7727 const styleOverride = document.createElement('style'); 7728 styleOverride.textContent = ` 7729 :root { 7730 --accent-color: #555 !important; /* Override red with gray */ 7731 } 7732 .variable { 7733 color: #555 !important; /* Override any red variable text */ 7734 } 7735 .highlight { 7736 border-left-color: #3498db !important; /* Use secondary color instead of accent */ 7737 } 7738 .important-notice { 7739 border-left-color: #3498db !important; /* Use secondary color instead of accent */ 7740 } 7741 `; 7742 7743 // Add the style override to the head 7744 htmlDoc.head.appendChild(styleOverride); 7745 7746 // Add language metadata 7747 const languageMeta = document.createElement('meta'); 7748 languageMeta.setAttribute('name', 'document-language-id'); 7749 languageMeta.setAttribute('content', selectedLanguageId.toString()); 7750 htmlDoc.head.appendChild(languageMeta); 7751 7752 // Add document ID metadata if provided 7753 if (documentId) { 7754 const docIdMeta = document.createElement('meta'); 7755 docIdMeta.setAttribute('name', 'document-id'); 7756 docIdMeta.setAttribute('content', documentId.toString()); 7757 htmlDoc.head.appendChild(docIdMeta); 7758 } 7759 7760 // Write the modified HTML to the iframe 7761 iframeDoc.open(); 7762 iframeDoc.write(htmlDoc.documentElement.outerHTML); 7763 iframeDoc.close(); 7764 } else { 7765 // If it's a fragment, wrap it in a basic HTML structure with our overrides 7766 iframeDoc.open(); 7767 iframeDoc.write(` 7768 <!DOCTYPE html> 7769 <html> 7770 <head> 7771 <meta charset="UTF-8"> 7772 <meta name="document-language-id" content="${selectedLanguageId}"> 7773 ${documentId ? `<meta name="document-id" content="${documentId}">` : ''} 7774 <title>${title}</title> 7775 <style> 7776 body { font-family: Arial, sans-serif; margin: 20px; } 7777 .document-container { width: 800px; margin: 0 auto; line-height: 1.5; } 7778 .variable { color: #555 !important; } /* Remove red color for variables */ 7779 [style*="color: var(--accent-color)"] { color: #555 !important; } 7780 </style> 7781 </head> 7782 <body> 7783 <div class="document-container">${htmlContent}</div> 7784 </body> 7785 </html> 7786 `); 7787 iframeDoc.close(); 7788 } 7789 7790 // Wait for iframe content to load completely 7791 setTimeout(() => { 7792 try { 7793 // Update loading message safely 7794 if (loadingIndicator && loadingIndicator.innerHTML !== undefined) { 7795 loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7796 'Converting to PDF format...'; 7797 } 7798 7799 // Apply additional color overrides directly to elements 7800 try { 7801 const iframeWindow = iframe.contentWindow || iframe; 7802 const redElements = iframeDoc.querySelectorAll('.variable, [style*="color: var(--accent-color)"], [style*="color:#e74
7802c3c"], [style*="color: #e74c3c"]'); 7803 redElements.forEach(el => { 7804 el.style.color = '#555'; 7805 }); 7806 7807 // Override border colors 7808 const borderElements = iframeDoc.querySelectorAll('.highlight, .important-notice'); 7809 borderElements.forEach(el => { 7810 el.style.borderLeftColor = '#3498db'; 7811 }); 7812 } catch (e) { 7813 console.warn('Could not apply all color overrides:', e); 7814 } 7815 7816 // Adjust iframe height to match content 7817 iframe.style.height = iframeDoc.body.scrollHeight + 50 + 'px'; 7818 7819 // Use html2canvas on the body of the iframe 7820 html2canvas(iframeDoc.body, { 7821 scale: 2, 7822 useCORS: true, 7823 allowTaint: true, 7824 scrollY: 0, 7825 windowWidth: 1000, 7826 windowHeight: iframeDoc.body.scrollHeight + 50 7827 }).then(canvas => { 7828 try { 7829 // Update loading message safely 7830 if (loadingIndicator && loadingIndicator.innerHTML !== undefined) { 7831 loadingIndicator.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> ' + 7832 'Generating PDF file...'; 7833 } 7834 7835 // Create PDF document with A4 dimensions 7836 const pdf = new jspdf.jsPDF({ 7837 orientation: 'portrait', 7838 unit: 'mm', 7839 format: 'a4' 7840 }); 7841 7842 // Calculate dimensions 7843 const imgWidth = 210; // A4 width in mm 7844 const pageHeight = 295; // A4 height in mm 7845 const imgHeight = (canvas.height * imgWidth) / canvas.width; 7846 7847 // Add image to PDF 7848 const imgData = canvas.toDataURL('image/jpeg', 1.0); 7849 7850 // Handle multi-page documents 7851 let heightLeft = imgHeight; 7852 let position = 0; 7853 let pageNumber = 1; 7854 7855 // Add first page 7856 pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight); 7857 heightLeft -= pageHeight; 7858 7859 // Add additional pages if content is taller than one page 7860 while (heightLeft > 0) { 7861 pageNumber++; 7862 position = -pageHeight * (pageNumber - 1); 7863 pdf.addPage(); 7864 pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight); 7865 heightLeft -= pageHeight; 7866 } 7867 7868 // Include language ID and document ID in filename for clarity 7869 let filename = `${title}_lang${selectedLanguageId}_${date}.pdf`; 7870 if (documentId) { 7871 filename = `${title}_id${documentId}_lang${selectedLanguageId}_${date}.pdf`; 7872 } 7873 7874 // Save the PDF 7875 pdf.save(filename); 7876 7877 console.log('PDF generated successfully for language:', selectedLanguageId); 7878 7879 // Show success message 7880 displayAlertDocument('PDF document downloaded successfully.', 'success'); 7881 } catch (error) { 7882 console.error('PDF generation error:', error); 7883 displayAlertDocument('Error creating PDF: ' + error.message, 'error'); 7884 } finally { 7885 // Clean up 7886 document.body.removeChild(iframe); 7887 7888 // Remove loading indicator 7889 if (loadingIndicator && loadingIndicator.parentNode) { 7890 if (createdLoadingIndicator) { 7891 // Remove the temporary loading indicator we created 7892 loadingIndicator.parentNode.removeChild(loadingIndicator); 7893 } else { 7894 // Just hide the existing one 7895 loadingIndicator.style.display = 'none'; 7896 } 7897 } 7898 } 7899 }).catch(error => { 7900 console.error('Canvas rendering error:', error); 7901 displayAlertDocument('Error creating PDF: ' + error.message, 'error'); 7902 7903 // Clean up 7904 document.body.removeChild(iframe); 7905 7906 // Remove loading indicator 7907 if (loadingIndicator && loadingIndicator.parentNode) { 7908 if (createdLoadingIndicator) { 7909 loadingIndicator.parentNode.removeChild(loadingIndicator); 7910 } else { 7911 loadingIndicator.style.display = 'none'; 7912 } 7913 } 7914 }); 7915 } catch (error) { 7916 console.error('PDF preparation error:', error); 7917 displayAlertDocument('Error preparing PDF: ' + error.message, 'error'); 7918 7919 // Clean up 7920 document.body.removeChild(iframe); 7921 7922 // Remove loading indicator 7923 if (loadingIndicator && loadingIndicator.parentNode) { 7924 if (createdLoadingIndicator) { 7925 loadingIndicator.parentNode.removeChild(loadingIndicator); 7926 } else { 7927 loadingIndicator.style.display = 'none'; 7928 } 7929 } 7930 } 7931 }, 1000); // Wait for 1 second to ensure iframe is fully rendered 7932 7933 } catch (error) { 7934 console.error('PDF setup error:', error); 7935 displayAlertDocument('Error setting up PDF generation: ' + error.message, 'error'); 7936 7937 // Remove loading indicator 7938 if (loadingIndicator && loadingIndicator.parentNode) { 7939 if (createdLoadingIndicator) { 7940 loadingIndicator.parentNode.removeChild(loadingIndicator); 7941 } else { 7942 loadingIndicator.style.display = 'none'; 7943 } 7944 } 7945 } 7946 }); 7947} 7948 7949function downloadDocumentClientSide(format, documentId = null, languageId = null) { 7950 // Get the selected language ID 7951 const selectedLanguageId = languageId || 7952 window.currentLanguageId || 7953 (document.getElementById('select_language') ? document.getElementById('select_language').value : null) || 7954 2; // Default to 2 if not found 7955 7956 console.log('Download starting with language ID:', selectedLanguageId); 7957 7958 try { 7959 // Get the document content that's already displayed 7960 const contentContainer = document.querySelector('.document-preview'); 7961 if (!contentContainer) { 7962 throw new Error('Document content not found'); 7963 } 7964 7965 // Get the document title 7966 const title = document.querySelector('#document_result h4')?.textContent || 'document'; 7967 const sanitizedTitle = title.replace(/\s+/g, '_').toLowerCase(); 7968 7969 // Get the current date for the filename 7970 const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, ''); 7971 7972 if (format === 'html') { 7973 // HTML download logic remains the same... 7974 let htmlContent = contentContainer.innerHTML; 7975 const isCompleteHtml = htmlContent.trim().toLowerCase().includes('<!doctype html>') || 7976 htmlContent.trim().toLowerCase().includes('<html'); 7977 7978 let content; 7979 if (isCompleteHtml) { 7980 const parser = new DOMParser(); 7981 const htmlDoc = parser.parseFromString(htmlContent, 'text/html'); 7982 7983 const styleOverride = document.createElement('style'); 7984 styleOverride.textContent = ` 7985 :root { 7986 --accent-color: #555 !important; 7987 } 7988 .variable { 7989 color: #555 !important; 7990 } 7991 .highlight { 7992 border-left-color: #3498db !important; 7993 } 7994 .important-notice { 7995 border-left-color: #3498db !important; 7996 } 7997 `; 7998 7999 htmlDoc.head.appendChild(styleOverride); 8000 8001 const languageMeta = document.createElement('meta'); 8002 languageMeta.setAttribute('name', 'document-language-id'); 8003 languageMeta.setAttribute('content', selectedLanguageId.toString()); 8004 htmlDoc.head.appendChild(languageMeta); 8005 8006 if (documentId) { 8007 const docIdMeta = document.createElement('meta'); 8008 docIdMeta.setAttribute('name', 'document-id'); 8009 docIdMeta.setAttribute('content', documentId.toString()); 8010 htmlDoc.head.appendChild(docIdMeta); 8011 } 8012 8013 content = '<!DOCTYPE html>\n' + htmlDoc.documentElement.outerHTML; 8014 } else { 8015 content = `<!DOCTYPE html> 8016<html> 8017<head> 8018 <meta charset="UTF-8"> 8019 <meta name="document-language-id" content="${selectedLanguageId}"> 8020 ${documentId ? `<meta name="document-id" content="${documentId}">` : ''} 8021 <title>${title}</title> 8022 <style> 8023 body { font-family: Arial, sans-serif; margin: 2em; } 8024 .document-container { margin: 0 auto; line-height: 1.5; } 8025 .variable { color: #555 !important; } 8026 [style*="color: var(--accent-color)"] { color: #555 !important; } 8027 </style> 8028</head> 8029<body> 8030 <div class="document-container"> 8031 ${htmlContent} 8032 </div> 8033</body> 8034</html>`; 8035 } 8036 8037 const blob = new Blob([content], { type: 'text/html' }); 8038 const fileName = documentId ? 8039 `${sanitizedTitle}_id${documentId}_lang${selectedLanguageId}_${currentDate}.html` : 8040 `${sanitizedTitle}_lang${selectedLanguageId}_${currentDate}.html`; 8041 8042 const url = window.URL.createObjectURL(blob); 8043 const a = document.createElement('a'); 8044 a.style.display = 'none'; 8045 a.href = url; 8046 a.download = fileName; 8047 document.body.appendChild(a); 8048 a.click(); 8049 8050 window.URL.revokeObjectURL(url); 8051 document.body.removeChild(a); 8052 8053 console.log('HTML download completed'); 8054 8055 } else if (format === 'pdf') { 8056 // For PDF, call the fixed function with all required parameters 8057 generatePdfFromIframe( 8058 contentContainer, 8059 sanitizedTitle, 8060 currentDate, 8061 null, // container - will be auto-detected 8062 null, // loadingIndicator - will be auto-created 8063 null, // documentType - not needed 8064 selectedLanguageId, 8065 documentId 8066 ); 8067 } else { 8068 throw new Error('Unsupported format: ' + format); 8069 } 8070 } catch (error) { 8071 console.error("Download error:", error); 8072 displayAlertDocument('Download failed: ' + error.message, 'error'); 8073 } 8074} 8075 8076function loadJsPdfLibraries(callback) { 8077 // Track how many libraries have loaded 8078 let librariesLoaded = 0; 8079 const requiredLibraries = 2; 8080 8081 // Function to check if all libraries are loaded 8082 const checkAllLoaded = () => { 8083 librariesLoaded++; 8084 if (librariesLoaded === requiredLibraries) { 8085 callback(); 8086 } 8087 }; 8088 8089 // Check if jsPDF is already loaded 8090 if (typeof jspdf !== 'undefined') { 8091 librariesLoaded++; 8092 } else { 8093 // Load jsPDF 8094 const jsPdfScript = document.createElement('script'); 8095 jsPdfScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; 8096 jsPdfScript.onload = checkAllLoaded; 8097 jsPdfScript.onerror = () => { 8098 console.error('Failed to load jsPDF library'); 8099 displayAlertDocument('Failed to load PDF generation library', 'error'); 8100 }; 8101 document.head.appendChild(jsPdfScript); 8102 } 8103 8104 // Check if html2canvas is already loaded 8105 if (typeof html2canvas !== 'undefined') { 8106 librariesLoaded++; 8107 } else { 8108 // Load html2canvas 8109 const html2canvasScript = document.createElement('script'); 8110 html2canvasScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; 8111 html2canvasScript.onload = checkAllLoaded; 8112 html2canvasScript.onerror = () => { 8113 console.error('Failed to load html2canvas library'); 8114 displayAlertDocument('Failed to load PDF generation library', 'error'); 8115 }; 8116 document.head.appendChild(html2canvasScript); 8117 } 8118 8119 // If both already loaded, call callback immediately 8120 if (librariesLoaded === requiredLibraries) { 8121 callback(); 8122 } 8123} 8124 8125function processTemplate(template, formData) { 8126 // Replace all placeholders in the template with actual values 8127 let processedTemplate = template; 8128 8129 // Map form fields to template placeholders 8130 const placeholderMap = { 8131 'name': '{{COMPANY_NAME}}', 8132 'address': '{{COMPANY_ADDRESS}}', 8133 'country': '{{COUNTRY}}', 8134 'email': '{{EMAIL}}', 8135 'phone': '{{PHONE_NUMBER}}', 8136 'websiteUrl': '{{WEBSITE_URL}}', 8137 'lastUpdated': '{{LAST_UPDATED}}', 8138 'effectiveDate': '{{EFFECTIVE_DATE}}', 8139 'serviceName': '{{SERVICE_NAME}}', 8140 'companyRegNo': '{{COMPANY_REG_NO}}', 8141 'legalEntity': '{{LEGAL_ENTITY}}', 8142 'privacyUrl': '{{PRIVACY_URL}}', 8143 'termsUrl': '{{TERMS_URL}}', 8144 'pricingUrl': '{{PRICING_URL}}' 8145 }; 8146 8147 // Replace each placeholder with the actual value 8148 for (const [field, placeholder] of Object.entries(placeholderMap)) { 8149 let value = formData[field] || ''; 8150 8151 // Format dates nicely 8152 if (field === 'lastUpdated' || field === 'effectiveDate') { 8153 value = formatDateForDisplay(value); 8154 } 8155 8156 // Use a global regex to replace all instances of the placeholder 8157 processedTemplate = processedTemplate.replace(new RegExp(placeholder, 'g'), value); 8158 } 8159 8160 // IMPORTANT: Do NOT convert newlines to <br> tags here 8161 // This was causing the issue with extra <br> tags in the HTML output 8162 // Remove this line: processedTemplate = processedTemplate.replace(/\n/g, '<br>'); 8163 8164 // Check if the template is already a complete HTML document 8165 const isCompleteHtml = processedTemplate.trim().toLowerCase().startsWith('<!doctype html>') || 8166 processedTemplate.trim().toLowerCase().startsWith('<html'); 8167 8168 // If it's already complete HTML, return it as-is 8169 if (isCompleteHtml) { 8170 return processedTemplate; 8171 } 8172 8173 // Otherwise, wrap the content in a minimal container 8174 return `<div class="document-container">${processedTemplate}</div>`; 8175} 8176 8177async function getDocumentTemplate(docTypeCode, languageId = null) { 8178 try { 8179 // If no language ID is provided, use the currently selected language 8180 if (languageId === null && window.currentLanguageId) { 8181 languageId = window.currentLanguageId; 8182 } else if (languageId === null) { 8183 // Default language ID (you may need to adjust this) 8184 languageId = 2; 8185 } 8186 8187
8187 const data = { 8188 sid: getSid(), 8189 documentTypeCode: docTypeCode, 8190 languageId: languageId 8191 }; 8192 8193 const response = await postRequest(endpoint + '/document/getDocumentTemplate', data); 8194 8195 if (!response.ok) { 8196 throw new Error('Failed to fetch document template: ' + response.status); 8197 } 8198 8199 const responseData = await response.json(); 8200 8201 if (responseData.statusCode !== 0) { 8202 throw new Error('Error fetching document template: ' + (responseData.statusMessage || 'Unknown error')); 8203 } 8204 8205 if (!responseData.template || !responseData.template.templateText) { 8206 throw new Error('No template found for document type: ' + docTypeCode); 8207 } 8208 8209 return responseData.template; 8210 } catch (error) { 8211 console.error('Error fetching document template:', error); 8212 throw error; 8213 } 8214} 8215 8216function getDocumentTypeName(docTypeCode) { 8217 const documentTypeMap = { 8218 'privacyPolicy': 'Privacy Policy', 8219 'termsOfUse': 'Terms of Use', 8220 'dpa': 'Data Processing Agreement', 8221 'cookiePolicy': 'Cookie Policy' 8222 }; 8223 8224 return documentTypeMap[docTypeCode] || 'Document'; 8225} 8226 8227function getDGCookie(name) { 8228 const value = `; ${document.cookie}`; 8229 const parts = value.split(`; ${name}=`); 8230 if (parts.length === 2) return parts.pop().split(';').shift(); 8231 return null; 8232} 8233 8234function normalizeLanguageCode(langCode) { 8235 if (!langCode) return null; 8236 8237 // Convert to lowercase and replace underscores with hyphens 8238 let normalized = langCode.toLowerCase().replace(/_/g, '-'); 8239 8240 // Return both the full code and just the language part (before the first hyphen) 8241 const parts = normalized.split('-'); 8242 return { 8243 full: normalized, // e.g., 'fr-fr' 8244 language: parts[0], // e.g., 'fr' 8245 original: langCode // e.g., 'fr_fr' 8246 }; 8247} 8248 8249function languageCodesMatch(cookieCode, dbCode) { 8250 if (!cookieCode || !dbCode) return false; 8251 8252 // Direct exact match (most likely to work) 8253 if (cookieCode === dbCode) return true; 8254 8255 const cookie = normalizeLanguageCode(cookieCode); 8256 const db = normalizeLanguageCode(dbCode); 8257 8258 // Normalized match 8259 if (cookie.full === db.full) return true; 8260 if (cookie.original.toLowerCase() === dbCode.toLowerCase()) return true; 8261 if (cookieCode.toLowerCase() === dbCode.toLowerCase()) return true; 8262 8263 // Language part match (e.g., 'fr' matches 'fr-FR') 8264 if (cookie.language === db.language) return true; 8265 if (cookie.language === db.full) return true; 8266 if (cookie.full === db.language) return true; 8267 8268 return false; 8269} 8270 8271async function loadLanguageDropdown() { 8272 try { 8273 // Check if language dropdown exists 8274 const languageDropdown = document.getElementById('select_language'); 8275 if (!languageDropdown) { 8276 console.warn('Language dropdown not found - skipping language loading'); 8277 return; 8278 } 8279 8280 // Get language code from cookie (read only, don't set) 8281 const savedLanguageCode = getDGCookie('lang'); 8282 console.log('Saved language code from cookie:', savedLanguageCode); 8283 8284 // Prepare request data 8285 const data = { sid: getSid() }; 8286 8287 // Make the API request to get languages using your existing endpoint 8288 const response = await postRequest(endpoint + '/languages', data); 8289 8290 if (!response.ok) { 8291 throw new Error('Failed to fetch languages: ' + response.status); 8292 } 8293 8294 const responseData = await response.json(); 8295 8296 if (responseData.statusCode !== 0) { 8297 console.error('Error fetching languages:', responseData.statusMessage); 8298 // Don't throw error, just log it and continue without languages 8299 return; 8300 } 8301 8302 // Clear existing options 8303 languageDropdown.innerHTML = ''; 8304 8305 // Add languages to dropdown 8306 if (responseData.languages && Array.isArray(responseData.languages) && responseData.languages.length > 0) { 8307 let defaultLanguageId = 2; // Fallback default (English) 8308 let savedLanguageFound = false;
8309 8310 // Debug: Log all available languages and their codes 8311 //console.log('Available languages:'); 8312 //responseData.languages.forEach(lang => { 8313 // console.log(`- ${lang.name}: ${lang.shortCode} (ID: ${lang.id})`); 8314 //}); 8315 8316 responseData.languages.forEach(language => { 8317 const option = document.createElement('option'); 8318 option.value = language.id; 8319 option.text = language.name; // This will show "Français (French)" 8320 8321 // Store language code as data attribute for future reference 8322 option.dataset.languageCode = language.shortCode; 8323 8324 languageDropdown.appendChild(option); 8325 8326 // Check if this language matches the saved cookie using flexible matching 8327 if (savedLanguageCode && languageCodesMatch(savedLanguageCode, language.shortCode)) { 8328 defaultLanguageId = language.id; 8329 savedLanguageFound = true; 8330 console.log(`â Found matching language for cookie '${savedLanguageCode}':`, language.name, `(shortCode: '${language.shortCode}', ID: ${language.id})`); 8331 } 8332 8333 // Check specific cases based on your data structure 8334 // English (ID: 2) as fallback default 8335 //if (!savedLanguageFound && language.id === 2) { 8336 // Keep English as fallback if no cookie match found 8337 //} 8338 }); 8339 8340 // Set the selected language 8341 languageDropdown.value = defaultLanguageId; 8342 8343 // Store current language ID in window object for easy access 8344 window.currentLanguageId = parseInt(defaultLanguageId); 8345 8346 // Add event listener for language change (without setting cookies) 8347 languageDropdown.addEventListener('change', function() { 8348 // Update global language ID 8349 window.currentLanguageId = parseInt(this.value); 8350 8351 // Get the selected language info 8352 const selectedOption = this.options[this.selectedIndex]; 8353 const selectedLanguageName = selectedOption.text; 8354 const selectedLanguageCode = selectedOption.dataset.languageCode; 8355 8356 console.log(`Language changed to: ${selectedLanguageName} (ID: ${this.value}, shortCode: ${selectedLanguageCode})`); 8357 }); 8358 8359 // Log final result 8360 if (savedLanguageFound) { 8361 console.log('â Successfully loaded saved language from cookie'); 8362 } else if (savedLanguageCode) { 8363 console.warn(`â Saved language code '${savedLanguageCode}' not found in available languages. Using default English (ID: 2).`); 8364 } else { 8365 console.log('No language cookie found. Using default English (ID: 2).'); 8366 } 8367 8368 console.log('Languages loaded successfully. Selected language ID:', defaultLanguageId); 8369 } else { 8370 console.warn('No languages returned from server'); 8371 // Add a default English option 8372 const defaultOption = document.createElement('option'); 8373 defaultOption.value = 2; 8374 defaultOption.text = 'English'; 8375 defaultOption.dataset.languageCode = 'en_gb'; 8376 languageDropdown.appendChild(defaultOption); 8377 window.currentLanguageId = 2; 8378 } 8379 } catch (error) { 8380 console.error('Error loading languages:', error); 8381 // Don't prevent the rest of the initialization from continuing 8382 // Just set a default language 8383 window.currentLanguageId = 2; 8384 } 8385} 8386 8387 8388 8389// Cookie Manager Implementation 8390 8391async function loadCookieManager() { 8392 if (checkIfLoginRequired()) 8393 return; 8394 8395 // Check if required DOM elements exist - UPDATED: Changed to select_domain 8396 const domainDropdown = document.getElementById('select_domain'); 8397 const languageDropdown = document.getElementById('select_language'); 8398 const container = document.getElementById('cookie_manager_container'); 8399 8400 if (!domainDropdown) { 8401 console.error('Domain dropdown not found - make sure element with ID "select_domain" exists'); 8402 return; 8403 } 8404 8405 if (!languageDropdown) { 8406 console.error('Language dropdown not found - make sure element with ID "select_language" exists'); 8407 return; 8408 } 8409 8410 if (!container) { 8411 console.error('Cookie manager container not found - make sure element with ID "cookie_manager_container" exists'); 8412 return; 8413 } 8414 8415 try { 8416 await loadLanguageDropdown(); 8417 } catch (error) { 8418 console.error('Error loading languages:', error); 8419 } 8420 8421 // UPDATED: Load domains instead of projects 8422 try { 8423 let domains = null; 8424 const data = { sid: getSid(), projectTypeId: 2, method: 1 };
8425 const response = await postRequest(endpoint + '/project-domain/list', data); 8426 8427 if (response.ok) { 8428 const responseBody = await response.json(); 8429 if (responseBody.statusCode === 0) { 8430 domains = responseBody.domains; 8431 } else { 8432 console.error('Error loading domains: ' + responseBody.statusMessage); 8433 displayAlertCookie('Failed to load domains: ' + responseBody.statusMessage, 'error'); 8434 return; 8435 } 8436 } else { 8437 console.error('Error: ' + response.statusText); 8438 displayAlertCookie('Failed to load domains: ' + response.statusText, 'error'); 8439 return; 8440 } 8441 8442 // Clear existing options first to prevent duplicates 8443 domainDropdown.innerHTML = ''; 8444 8445 // Add domain options 8446 let firstValidDomain = null; 8447 if (domains && domains.length > 0) { 8448 domains.forEach(domain => { 8449 const option = document.createElement('option'); 8450 option.value = domain.id; 8451 option.text = domain.domainName; 8452 domainDropdown.appendChild(option); 8453 8454 // Store first domain for default selection 8455 if (!firstValidDomain) { 8456 firstValidDomain = domain; 8457 } 8458 }); 8459 } else { 8460 console.error('No domains available'); 8461 displayAlertCookie('No domains available', 'error'); 8462 return; 8463 } 8464 8465 // Set first domain as default if no URL parameter is provided 8466 const selectedDomainId = new URLSearchParams(window.location.search).get("id"); 8467 if (selectedDomainId) { 8468 domainDropdown.value = selectedDomainId; 8469 } else if (firstValidDomain) { 8470 domainDropdown.value = firstValidDomain.id; 8471 } 8472 8473 // UPDATED: Changed event listener to use domain instead of project 8474 domainDropdown.addEventListener('change', function() { 8475 domainDropdown.classList.remove('d-none'); 8476 loadCookiesForDomain(domainDropdown.value); 8477 setCookieScanUrl(domainDropdown.options[domainDropdown.selectedIndex].text,1); 8478 }); 8479 8480 domainDropdown.dispatchEvent(new Event('change')); 8481 8482 } catch (error) { 8483 console.error('Error loading domains:', error); 8484 displayAlertCookie('Failed to load domains: ' + error.message, 'error'); 8485 } 8486} 8487 8488function setCookieScanUrl(url,count){ 8489 if (count>20) 8490 return; 8491 8492 var domainDropdown = document.getElementById('select_domain'), vvx = document.getElementById('scanUrl'); 8493 //console.log("sdfsfsdfsfsdfd " + url); 8494 if (vvx) 8495 vvx.value = 'https://'+domainDropdown.options[domainDropdown.selectedIndex].text+'/'; 8496 else 8497 setTimeout(function(){setCookieScanUrl(url, count+1);},200); 8498} 8499 8500function validateCookieData(cookieData) { 8501 const errors = []; 8502 8503 // Debug: Log the data being validated 8504 console.log('Validating cookie data:', cookieData); 8505 8506 // Check required fields with more detailed logging 8507 if (!cookieData.name || cookieData.name.trim() === '') { 8508 console.log('Name validation failed:', cookieData.name); 8509 errors.push(translations['nameRequired'] || 'Cookie name is required'); 8510 } 8511 8512 if (!cookieData.type || cookieData.type.trim() === '') { 8513 console.log('Type validation failed:', cookieData.type); 8514 errors.push(translations['typeRequired'] || 'Type is required'); 8515 } 8516 8517 // Validate URL format for privacyUrl if provided 8518 if (cookieData.privacyUrl && cookieData.privacyUrl.trim() !== '') { 8519 try { 8520 new URL(cookieData.privacyUrl); 8521 } catch (e) { 8522 console.log('Privacy URL format validation failed:', cookieData.privacyUrl); 8523 errors.push('Privacy URL must be a valid URL'); 8524 } 8525 } 8526 8527 console.log('Validation errors:', errors); 8528 return errors; 8529} 8530 8531function getLanguageShortCodeById(languageId) { 8532 // Language mapping based on the provided data structure 8533 const languageMap = { 8534 '1': 'tr_TR', // Türkçe (Turkish) 8535 '2': 'en_gb', // English 8536 '3': 'fr_fr', // Français (French) 8537 '4': 'es_es', // Español (Spanish) 8538 '5': 'de', // Deutsch (German) 8539 '6': 'zh', // 䏿 (Chinese) 8540 '7': 'ar', // اÙÙØºØ© Ø§ÙØ¹Ø±Ø¨ÙØ© (Arabic) 8541 '8': 'ro', // RomânÄ (Romanian) 8542 '9': 'ru', // Ð ÑÑÑкий (Russian) 8543 '10': 'ja', // æ¥æ¬èª (Japanese) 8544 '11': 'ko', // íêµì´ (Korean) 8545 '12': 'it_it', // Italiano (Italian) 8546 '13': 'bg', // ÐÑлгаÑÑки (Bulgarian) 8547 '14': 'bs', // Bosanski (Bosnian) 8548 '15': 'ca', // Català (Catalan) 8549 '16': 'cs', // Äesky (Czech) 8550 '17': 'cy', // Cymraeg (Welsh) 8551 '18': 'da', // Dansk (Danish) 8552 '19': 'el', // Îλληνική (Greek) 8553 '20': 'et', // Eesti (Estonian) 8554 '21': 'eu', // Euskara (Basque)
8555 '22': 'fi', // Suomalainen (Finnish) 8556 '23': 'gl', // Galego (Galician) 8557 '24': 'he', // ×¢Ö´×רִ×ת (Hebrew) 8558 '25': 'hr', // Hrvatski (Croatian) 8559 '26': 'hu', // Magyar (Hungarian) 8560 '27': 'id', // Bahasa Indonesia (Indonesian) 8561 '28': 'lt', // Lietuvių kalba (Lithuanian) 8562 '29': 'lv', // latviski (Latvian) 8563 '30': 'mk', // македонÑки (Macedonian) 8564 '31': 'ms', // Melayu (Malay) 8565 '32': 'mt', // Malti (Malteze) 8566 '33': 'nl', // Nederlands (Dutch) 8567 '34': 'nb', // Norsk (Norwegian) 8568 '35': 'pl', // Polski (Polish) 8569 '36': 'pt-br', // Português - Brasil (Portuguese) 8570 '37': 'pt-pt', // Português - Portugal (Portuguese) 8571 '38': 'sk', // Slovenská (Slovak) 8572 '39': 'sl', // Slovenski (Slovenian) 8573 '40': 'sr', // СÑпÑки (Serbian) 8574 '41': 'sv', // Svenska (Swedish) 8575 '42': 'uk', // УкÑаÑнÑÑка (Ukrainian) 8576 '43': 'fl' // Filipino (Tagalog) 8577 }; 8578 8579 return languageMap[languageId] || null; 8580} 8581 8582function getLanguageNameByShortCode(shortCode) { 8583 const languageNames = { 8584 'tr_TR': 'TR', 8585 'en_gb': 'EN', 8586 'fr_fr': 'FR', 8587 'es_es': 'ES', 8588 'de': 'DE', 8589 'zh': 'ZH', 8590 'ar': 'AR', 8591 'ro': 'RO', 8592 'ru': 'RU', 8593 'ja': 'JA', 8594 'ko': 'KO', 8595 'it_it': 'IT', 8596 'bg': 'BG', 8597 'bs': 'BS', 8598 'ca': 'CA', 8599 'cs': 'CS', 8600 'cy': 'CY', 8601 'da': 'DA', 8602 'el': 'EL', 8603 'et': 'ET', 8604 'eu': 'EU', 8605 'fi': 'FI', 8606 'gl': 'GL', 8607 'he': 'HE', 8608 'hr': 'HR', 8609 'hu': 'HU', 8610 'id': 'ID', 8611 'lt': 'LT', 8612 'lv': 'LV', 8613 'mk': 'MK', 8614 'ms': 'MS', 8615 'mt': 'MT', 8616 'nl': 'NL', 8617 'nb': 'NB', 8618 'pl': 'PL', 8619 'pt-br': 'PT-BR', 8620 'pt-pt': 'PT-PT', 8621 'sk': 'SK', 8622 'sl': 'SL', 8623 'sr': 'SR', 8624 'sv': 'SV', 8625 'uk': 'UK', 8626 'fl': 'FL' 8627 }; 8628 8629 return languageNames[shortCode] || shortCode.toUpperCase(); 8630} 8631 8632function createEnhancedCookieModal(title, cookieData, onSave, domainId) { 8633 const modal = document.createElement('div'); 8634 modal.classList.add('modal', 'fade'); 8635 modal.tabIndex = -1; 8636 8637 // Store domainId on the modal element for debugging 8638 modal.setAttribute('data-domain-id', domainId); 8639 console.log('Creating modal with domainId:', domainId); 8640 8641 // Get selected language shortCode for description 8642 const languageDropdown = document.getElementById('select_language'); 8643 let selectedLanguageCode = 'en_gb'; // default fallback to English 8644 8645 if (languageDropdown && languageDropdown.selectedOptions.length > 0) { 8646 const selectedOption = languageDropdown.selectedOptions[0]; 8647 const selectedId = selectedOption.value; 8648 8649 // Try to get shortCode from data attribute first, then lookup by ID 8650 selectedLanguageCode = selectedOption.dataset.shortcode || 8651 selectedOption.getAttribute('data-shortcode') || 8652 getLanguageShortCodeById(selectedId) || 8653 'en_gb'; 8654 } 8655 8656 console.log('Modal created with language code:', selectedLanguageCode); 8657 8658 // Get description for selected language 8659 let currentDescription = ''; 8660 if (cookieData.description) { 8661 if (typeof cookieData.description === 'object') { 8662 currentDescription = cookieData.description[selectedLanguageCode] || cookieData.description['en'] || ''; 8663 } else { 8664 // Handle legacy string descriptions 8665 currentDescription = cookieData.description; 8666 } 8667 } 8668 8669 // Get current storage type value (handle both numeric and string) 8670 const currentStorageType = parseInt(cookieData.storageType, 10) || 1; 8671 8672 modal.innerHTML = ` 8673 <div class="modal-dialog modal-lg"> 8674 <div class="modal-content"> 8675 <div class="modal-header"> 8676 <h5 class="modal-title">${title}</h5> 8677 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 8678 </div> 8679 <div class="modal-body"> 8680 <div id="validationErrors" class="alert alert-danger d-none"></div> 8681 <form id="cookieForm" novalidate> 8682 <!-- Basic Information --> 8683 <div class="row"> 8684 <div class="col-12"> 8685 <h6 class="text-primary mb-3"><i class="fas fa-info-circle me-2"></i>${translations['basicInformation'] || 'Basic Information'}</h6> 8686 </div> 8687 </div> 8688 8689 <div class="row"> 8690 <div class="col-md-6"> 8691 <div class="mb-3"> 8692 <label for="cookieName" class="form-label"> 8693 ${translations['name'] || 'Name'} <span class="text-danger">*</span> 8694 </label> 8695 <input type="text" class="form-control" id="cookieName" 8696 value="${cookieData.name || ''}" required> 8697 <div class="invalid-feedback">${translations['nameRequired'] || 'Cookie name is required'}</div> 8698 </div> 8699 </div> 8700 <div class="col-md-6"> 8701 <div class="mb-3"> 8702 <label for="cookieDisplayName" class="form-label"> 8703 ${translations['displayName'] || 'Display Name'} 8704 </label> 8705 <input type="text" class="form-control" id="cookieDisplayName" 8706 value="${cookieData.displayName || ''}" placeholder="${translations['displayNamePlaceHolder'] || 'e.g., Analytics Co
8706okie (optional - defaults to name'}"> 8707 <small class="form-text text-muted" ${translations['displayNameNote'] || 'If empty, will default to the cookie name'}></small> 8708 </div> 8709 </div> 8710 </div> 8711 8712 <div class="row"> 8713 <div class="col-md-6"> 8714 <div class="mb-3"> 8715 <label for="cookieType" class="form-label"> 8716 ${translations['type'] || 'Type'} <span class="text-danger">*</span> 8717 </label> 8718 <select class="form-select" id="cookieType" required> 8719 <option value="">${translations['selectType'] || 'Select Type'}</option> 8720 <option value="Essential" ${cookieData.type?.toLowerCase() === 'essential' ? 'selected' : ''}>${translations['essential'] || 'Essential'}</option> 8721 <option value="Personalization" ${cookieData.type?.toLowerCase() === 'personalization' ? 'selected' : ''}>${translations['personalization'] || 'Personalization'}</option> 8722 <option value="Analytics" ${cookieData.type?.toLowerCase() === 'analytics' ? 'selected' : ''}>${translations['analytics'] || 'Analytics'}</option> 8723 <option value="Marketing" ${cookieData.type?.toLowerCase() === 'marketing' ? 'selected' : ''}>${translations['marketing'] || 'Marketing'}</option> 8724 <option value="Security" ${cookieData.type?.toLowerCase() === 'security' ? 'selected' : ''}>${translations['security'] || 'Security'}</option> 8725 <option value="Other" ${cookieData.type?.toLowerCase() === 'other' ? 'selected' : ''}>${translations['other'] || 'Other'}</option> 8726 </select> 8727 <div class="invalid-feedback">${translations['typeRequired'] || 'Type is required'}</div> 8728 </div> 8729 </div> 8730 8731 <div class="col-md-6"> 8732 <div class="mb-3"> 8733 <label for="cookieRetention" class="form-label">${translations['retentionPeriod'] || 'Retention Period'} 8734 </label> 8735 <input type="text" class="form-control" id="cookieRetention" 8736 value="${cookieData.retentionPeriod || 'Session'}" 8737 placeholder="e.g., Session, 30 days, 1 year, etc."> 8738 <div class="form-text">${translations['retentionPeriodNote'] || 'Examples: Session, 30 days, 1 year, 2 years, etc.'}</div> 8739 </div> 8740 </div> 8741 </div> 8742 8743 <div class="row"> 8744 <div class="col-md-6"> 8745 <div class="mb-3"> 8746 <label for="cookieStorageType" class="form-label">${translations['storageType'] || 'Storage Type'} 8747 </label> 8748 <select class="form-select" id="cookieStorageType"> 8749 <option value="1" ${currentStorageType === 1 ? 'selected' : ''}>HTTP Cookie</option> 8750 <option value="2" ${currentStorageType === 2 ? 'selected' : ''}>Local Storage</option> 8751 <option value="3" ${currentStorageType === 3 ? 'selected' : ''}>Session Storage</option> 8752 <option value="5" ${currentStorageType === 5 ? 'selected' : ''}>Tracking Pixel</option> 8753 </select> 8754 <div class="form-text">${translations['cookieStorageNote'] || 'Select how this cookie/tracker stores data'}</div> 8755 </div> 8756 </div> 8757 </div> 8758 8759 <!-- Description and Purpose --> 8760 <div class="row"> 8761 <div class="col-12"> 8762 <h6 class="text-primary mb-3 mt-4"><i class="fas fa-file-text me-2"></i>${translations['descriptionPurpose'] || 'Description and Purpose'}</h6> 8763 </div> 8764 </div> 8765 8766 <div class="mb-3"> 8767 <label for="cookieDescription" class="form-label"> 8768 ${translations['description'] || 'Description'} <span class="badge bg-success">${getLanguageNameByShortCode(selectedLanguageCode)}</span> 8769 </label> 8770 <textarea class="form-control" id="cookieDescription" rows="3" 8771 placeholder="Describe the purpose of this cookie, what data it stores, and why it is necessary...">${currentDescription}</textarea> 8772 <small class="form-text text-muted">${translations['descriptionLanguageNote'] || 'Description will be saved for the selected language'} (${getLanguageNameByShortCode(selectedLanguageCode)})</small> 8773 </div> 8774 8775 <div class="row"> 8776 <div class="col-md-6"> 8777 <div class="mb-3"> 8778 <label for="cookieVendor" class="form-label"> 8779 ${translations['vendor'] || 'Vendor'} 8780 </label> 8781 <input type="text" class="form-control" id="cookieVendor" 8782 value="${cookieData.vendor || ''}" placeholder="Google, Facebook, etc."> 8783 </div> 8784 </div> 8785 <div class="col-md-6"> 8786 <div class="mb-3"> 8787 <label for="cookiePrivacyUrl" class="form-label"> 8788 ${translations['privacyUrl'] || 'Privacy URL'} 8789 </label> 8790 <input type="url" class="form-control" id="cookiePrivacyUrl" 8791 value="${cookieData.privacyUrl || ''}" placeholder="https://example.com/privacy"> 8792 <div class="invalid-feedback">Valid privacy URL is required</div> 8793 </div> 8794 </div> 8795 </div> 8796 8797 <!-- Technical Details --> 8798 <div class="row"> 8799 <div class="col-12"> 8800 <h6 class="text-primary mb-3 mt-4"><i class="fas fa-cogs me-2"></i>${translations['technicalDetails'] || 'Technical Details'}</h6> 8801 </div> 8802 </div> 8803 8804 <div class="mb-3"> 8805 <label for="cookieScript" class="form-label">${translations['script'] || 'Script URL'}</label> 8806 <input type="url" class="form-control" id="cookieScript" 8807 placeholder="${translations['cookieScriptPlaceholder'] || 'https://www.google-analytics.com/analytics.js'}" value="${cookieData.script || ''}"> 8808 <small class="form-text text-muted">${translations['cookieScriptNote'] || 'If no script url provided, the cookie shouldn\'t be marked as essential.'}</small> 8809 </div> 8810 8811 <div class="alert alert-warning py-2 mb-3" role="alert"> 8812 <strong>${translations['important'] || 'Important:'}</strong> 8813 <p class="mb-2 mt-1"> 8814 ${translations['importantNote'] || 'Avoid using your own website\'s URL as the Script URL, as it may disrupt your website\'s functionality.'} 8815 </p> 8816 <p class="mb-1"> 8817 ${translations['importantExample'] || 'For example, if the third-party script that sets this cookie is:'} 8818 </p> 8819 <code class="d-block bg-light p-2 mb-2 rounded"> 8820 <script async src="https://www.google-analytics.com/analytics.js"></script> 8821 </code> 8822 <p class="mb-0"> 8823 ${translations['importantConclusion'] || 'You should enter the Script URL pattern as:'} <strong>https://www.google-analytics.com/analytics.js</strong> 8824 </p> 8825 </div> 8826 </form> 8827 </div> 8828 <div class="modal-footer"> 8829 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> 8830 ${translations['cancel'] || 'Cancel'} 8831 </button> 8832 <button type="button" class="btn btn-primary" id="saveCookieBtn"> 8833 <i class="fas fa-save me-2"></i>${translations['save'] || 'Save'} 8834 </button> 8835 </div> 8836 </div> 8837 </div> 8838 `; 8839 8840 // Add save functionality with enhanced validation 8841 const saveBtn = modal.querySelector('#saveCookieBtn'); 8842 const form = modal.querySelector('#cookieForm'); 8843 const errorContainer = modal.querySelector('#validationErrors'); 8844 8845 saveBtn.addEventListener('click', () => { 8846 // Clear previous validation states 8847 form.classList.remove('was-validated'); 8848 errorContainer.classList.add('d-none'); 8849 8850 // Get form data - Updated with multilingual description support using shortCode 8851 const nameValue = modal.querySelector('#cookieName').value.trim(); 8852 const displayNameValue = modal.querySelector('#cookieDisplayName').value.trim(); 8853 const descriptionValue = modal.querySelector('#cookieDescription').value.trim(); 8854 const scriptValue = modal.querySelector('#cookieScript').value.trim(); 8855 let typeValue = modal.querySelector('#cookieType').value; 8856 8857 // Get storage type as numeric value 8858 const storageTypeValue = parseInt(modal.querySelector('#cookieStorageType').value, 10); 8859 8860 // If no script is provided and type is essential, change to other 8861 if (!scriptValue && typeValue === 'essential') { 8862 typeValue = 'other'; 8863 console.log('Script is blank and type was essential - changed to other'); 8864 } 8865 8866 // Get selected language shortCode from dropdown based on the actual language structure 8867 const languageDropdown = document.getElementById('select_language'); 8868 let selectedLanguageCode = 'en_gb'; // default fallback to English 8869 8870 if (languageDropdown && languageDropdown.selectedOptions.length > 0) { 8871 const selectedOption = languageDropdown.selectedOptions[0]; 8872 const selectedId = selectedOption.value; 8873 8874 // Try to find the language shortCode by matching the ID 8875 selectedLanguageCode = selectedOption.dataset.shortcode || 8876 selectedOption.getAttribute('data-shortcode') || 8877 getLanguageShortCodeById(selectedId) || 8878 'en_gb'; 8879 } 8880 8881 console.log('Selected language code for description:', selectedLanguageCode); 8882 8883 // Handle multilingual description 8884 let descriptionObject = {}; 8885 if (cookieData.description && typeof cookieData.description === 'object') { 8886 // Keep existing translations 8887 descriptionObject = { ...cookieData.description }; 8888 } 8889 // Update the description for the selected language 8890 descriptionObject[selectedLanguageCode] = descriptionValue; 8891 8892 const formData = { 8893 name: nameValue, 8894 displayName: displayNameValue || nameValue, // Use name if displayName is empty 8895 type: typeValue, // Use the potentially modified type 8896 retentionPeriod: modal.querySelector('#cookieRetention').value.trim() || 'Session', 8897 storageType: storageTypeValue, // Send numeric value (1-6) 8898 custom: true, // Always true for user-added/edited cookies 8899 description: descriptionObject, 8900 vendor: modal.querySelector('#cookieVendor').value.trim(), 8901 privacyUrl: modal.querySelector('#cookiePrivacyUrl').value.trim(), 8902 script: scriptValue, 8903 id: cookieData.id || generateCookieId() 8904 }; 8905 8906 // Validate form data 8907 const validationErrors = validateCookieData(formData); 8908 8909 if (validationErrors.length > 0) { 8910 errorContainer.innerHTML = translations['fixErrorMessage'] || '<strong>Please fix the following errors:</strong><ul>' + 8911 validationErrors.map(error => `<li>${error}</li>`).join('') + '</ul>'; 8912 errorContainer.classList.remove('d-none'); 8913 form.classList.add('was-validated'); 8914 return; 8915 } 8916 8917 // Check HTML5 validation 8918 if (form.checkValidity()) { 8919 onSave(formData); 8920 bootstrap.Modal.getInstance(modal).hide(); 8921 } else { 8922 form.classList.add('was-validated'); 8923 } 8924 }); 8925 8926 return modal; 8927} 8928 8929function showAddCookieModal(data, domainId) { 8930 console.log('showAddCookieModal called with domainId:', domainId); 8931 8932 const modal = createEnhancedCookieModal(translations['addCookie'] || 'Add Cookie', { 8933 name: '', 8934 displayName: '', 8935 type: '', 8936 retentionPeriod: 'Session', 8937 custom: true, 8938 description: '', 8939 vendor: '', 8940 privacyUrl: '', 8941 script: '' 8942 }, (cookieData, modalDomainId) => { 8943 console.log('Add cookie callback called with domainId:', modalDomainId || domainId); 8944 addCookie(cookieData, data, modalDomainId || domainId); 8945 }, domainId); 8946 8947 document.body.appendChild(modal); 8948 const bsModal = new bootstrap.Modal(modal); 8949 bsModal.show(); 8950 8951 modal.addEventListener('hidden.bs.modal', () => { 8952 document.body.removeChild(modal); 8953 }); 8954} 8955 8956function showEditCookieModal(cookie, data, domainId) { 8957 console.log('showEditCookieModal called with domainId:', domainId); 8958 8959 const modal = createEnhancedCookieModal('Edit Cookie', cookie, (cookieData, modalDomainId) => { 8960 console.log('Edit cookie callback called with domainId:', modalDomainId || domainId); 8961 updateCookie(cookieData, data, modalDomainId || domainId, cookie); 8962 }, domainId); 8963 8964 document.body.appendChild(modal); 8965 const bsModal = new bootstrap.Modal(modal); 8966 bsModal.show(); 8967 8968 modal.addEventListener('hidden.bs.modal', () => { 8969 document.body.removeChild(modal); 8970 }); 8971} 8972 8973function createEnhancedCookieRow(cookie, data, domainId) {
8974 const row = document.createElement('tr'); 8975 row.classList.add('cookie-row'); 8976 8977 // Display Name cell (first column) with comprehensive badges 8978 const displayNameCell = document.createElement('td'); 8979 const displayNameContainer = document.createElement('div'); 8980 8981 const displayNameSpan = document.createElement('span'); 8982 displayNameSpan.textContent = cookie.displayName || 'N/A'; 8983 displayNameSpan.classList.add('fw-bold'); 8984 displayNameContainer.appendChild(displayNameSpan); 8985 8986 // Vendor info 8987 if (cookie.vendor) { 8988 const vendorSpan = document.createElement('small'); 8989 vendorSpan.classList.add('text-muted', 'd-block'); 8990 vendorSpan.textContent = `by ${cookie.vendor}`; 8991 displayNameContainer.appendChild(vendorSpan); 8992 } 8993 8994 // Add badge for custom/system cookies 8995 if (cookie.custom) { 8996 const customBadge = document.createElement('span'); 8997 customBadge.classList.add('badge', 'bg-success', 'ms-2'); 8998 customBadge.textContent = 'Custom'; 8999 customBadge.style.fontSize = '0.7rem'; 9000 displayNameContainer.appendChild(customBadge); 9001 } else { 9002 const systemBadge = document.createElement('span'); 9003 systemBadge.classList.add('badge', 'bg-outline-secondary', 'text-secondary', 'border', 'border-secondary', 'ms-2'); 9004 systemBadge.textContent = 'System'; 9005 systemBadge.style.fontSize = '0.7rem'; 9006 systemBadge.style.backgroundColor = 'transparent'; 9007 displayNameContainer.appendChild(systemBadge); 9008 } 9009 9010 // Set by a response header (HttpOnly): the auto blocker can only refuse the request 9011 // that sets it; no script can read, veto or delete it once it is there. 9012 if (cookie.gateOnly) { 9013 const gateBadge = document.createElement('span'); 9014 gateBadge.classList.add('badge', 'bg-warning', 'text-dark', 'ms-2'); 9015 gateBadge.textContent = translations['serverSetCookie'] || 'Server-set'; 9016 gateBadge.title = translations['serverSetCookieHint'] 9017 || 'HttpOnly cookie set by a response header â blocked by refusing the request, not deletable by script'; 9018 gateBadge.style.fontSize = '0.7rem'; 9019 displayNameContainer.appendChild(gateBadge); 9020 } 9021 9022 displayNameCell.appendChild(displayNameContainer); 9023 row.appendChild(displayNameCell); 9024 9025 // Name cell (second column) 9026 const nameCell = document.createElement('td'); 9027 nameCell.textContent = cookie.name || ''; 9028 nameCell.classList.add('font-monospace', 'small'); 9029 row.appendChild(nameCell); 9030 9031 // Type 9032 const typeCell = document.createElement('td'); 9033 const typeSpan = document.createElement('span'); 9034 typeSpan.textContent = cookie.type || 'unspecified'; 9035 typeSpan.classList.add('badge', getTypeColor(cookie.type)); 9036 typeCell.appendChild(typeSpan); 9037 row.appendChild(typeCell); 9038 9039 // Retention Period 9040 const retentionCell = document.createElement('td'); 9041 retentionCell.textContent = cookie.retentionPeriod || 'Session'; 9042 if (cookie.retentionPeriod === 'Session') { 9043 retentionCell.classList.add('text-warning'); 9044 } 9045 row.appendChild(retentionCell); 9046 9047 // Actions 9048 const actionsCell = document.createElement('td'); 9049 actionsCell.classList.add('text-nowrap'); 9050
9051 // View/Details button 9052 const viewButton = document.createElement('button'); 9053 viewButton.classList.add('btn', 'btn-sm', 'btn-outline-info', 'me-1'); 9054 viewButton.innerHTML = '<i class="fa fa-eye"></i>'; 9055 viewButton.title = 'View Details'; 9056 viewButton.addEventListener('click', () => showCookieDetailsModal(cookie)); 9057 actionsCell.appendChild(viewButton); 9058 9059 const editButton = document.createElement('button'); 9060 editButton.classList.add('btn', 'btn-sm', 'btn-outline-primary', 'me-1'); 9061 editButton.innerHTML = '<i class="fa fa-edit"></i>'; 9062 editButton.title = 'Edit'; 9063 editButton.addEventListener('click', () => showEditCookieModal(cookie, data, domainId)); 9064 actionsCell.appendChild(editButton); 9065 9066 const deleteButton = document.createElement('button'); 9067 deleteButton.classList.add('btn', 'btn-sm', 'btn-outline-danger'); 9068 deleteButton.innerHTML = '<i class="fa fa-trash"></i>'; 9069 deleteButton.title = translations['delete'] || 'Delete'; 9070 deleteButton.addEventListener('click', () => deleteCookieConfirm(cookie, data, domainId)); 9071 actionsCell.appendChild(deleteButton); 9072 9073 row.appendChild(actionsCell); 9074 return row; 9075} 9076 9077function getTypeColor(type) { 9078 switch (type?.toLowerCase()) { 9079 case 'essential': return 'bg-danger'; 9080 case 'personalization': return 'bg-primary'; 9081 case 'analytics': return 'bg-info'; 9082 case 'marketing': return 'bg-warning'; 9083 case 'security': return 'bg-dark'; 9084 case 'other': return 'bg-secondary'; 9085 default: return 'bg-light text-dark'; 9086 } 9087} 9088 9089// Storage Type Helper Functions 9090const STORAGE_TYPES = { 9091 1: 'HTTP Cookie', 9092 2: 'Local Storage', 9093 3: 'Session Storage', 9094 5: 'Tracking Pixel' 9095}; 9096 9097function getStorageTypeName(storageTypeId) { 9098 const id = parseInt(storageTypeId, 10); 9099 return STORAGE_TYPES[id] || 'Unknown'; 9100} 9101 9102function getStorageTypeId(storageTypeName) { 9103 for (const [id, name] of Object.entries(STORAGE_TYPES)) { 9104 if (name === storageTypeName) { 9105 return parseInt(id, 10); 9106 } 9107 } 9108 return 1; // Default to HTTP Cookie 9109} 9110 9111function showCookieDetailsModal(cookie) { 9112 const modal = document.createElement('div'); 9113 modal.classList.add('modal', 'fade'); 9114 modal.tabIndex = -1; 9115 9116 // Get selected language shortCode for description display 9117 const languageDropdown = document.getElementById('select_language'); 9118 let selectedLanguageCode = 'en_gb'; // default fallback to English 9119 9120 if (languageDropdown && languageDropdown.selectedOptions.length > 0) { 9121 const selectedOption = languageDropdown.selectedOptions[0]; 9122 const selectedId = selectedOption.value; 9123 9124 // Try to get shortCode from data attribute first, then lookup by ID 9125 selectedLanguageCode = selectedOption.dataset.shortcode || 9126 selectedOption.getAttribute('data-shortcode') || 9127 getLanguageShortCodeById(selectedId) || 9128 'en_gb'; 9129 } 9130 9131 console.log('Details modal showing description for language:', selectedLanguageCode); 9132 9133 // Get description for selected language with fallbacks 9134 let displayDescription = 'No description provided'; 9135 if (cookie.description) { 9136 if (typeof cookie.description === 'object') { 9137 displayDescription = cookie.description[selectedLanguageCode] || 9138 cookie.description['en_gb'] || 9139 cookie.description['en'] || 9140 Object.values(cookie.description)[0] || 9141 'No description provided'; 9142 } else { 9143 // Handle legacy string descriptions 9144 displayDescription = cookie.description; 9145 } 9146 } 9147 9148 // Convert numeric storage type to display name 9149 const storageTypeDisplay = getStorageTypeName(cookie.storageType); 9150 9151 modal.innerHTML = ` 9152 <div class="modal-dialog modal-lg"> 9153 <div class="modal-content"> 9154 <div class="modal-header"> 9155 <h5 class="modal-title"> 9156 ${translations['cookieDetails'] || 'Cookie Details:'} ${cookie.name} 9157 </h5> 9158 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 9159 </div> 9160 <div class="modal-body"> 9161 <div class="row"> 9162 <div class="col-md-6"> 9163 <h6 class="text-primary">${translations['basicInformation'] || 'Basic Information'}</h6> 9164 <dl class="row"> 9165 <dt class="col-sm-5">${translations['displayName'] || 'Display Name'} :</dt> 9166 <dd class="col-sm-7">
9166${cookie.displayName || 'N/A'}</dd> 9167 <dt class="col-sm-5">${translations['name'] || 'Name'} :</dt> 9168 <dd class="col-sm-7">${cookie.name || 'N/A'}</dd> 9169 <dt class="col-sm-5">${translations['type'] || 'Type'} :</dt> 9170 <dd class="col-sm-7">${cookie.type || 'N/A'}</dd> 9171 <dt class="col-sm-5">${translations['retentionPeriod'] || 'Retention'} :</dt> 9172 <dd class="col-sm-7">${cookie.retentionPeriod || 'Session'}</dd> 9173 <dt class="col-sm-5">${translations['storageType'] || 'Storage Type'} :</dt> 9174 <dd class="col-sm-7">${storageTypeDisplay}</dd> 9175 </dl> 9176 </div> 9177 <div class="col-md-6"> 9178 <h6 class="text-primary">${translations['properties'] || 'Properties'}</h6> 9179 <dl class="row"> 9180 <dt class="col-sm-3">${translations['custom'] || 'Custom'} :</dt> 9181 <dd class="col-sm-9">${cookie.custom ? 'â Yes' : 'â No'}</dd> 9182 ${cookie.gateOnly ? `<dt class="col-sm-3">${translations['serverSetCookie'] || 'Server-set'} :</dt> 9183 <dd class="col-sm-9">${translations['serverSetCookieHint'] || 'HttpOnly cookie set by a response header â blocked by refusing the request, not deletable by script'}</dd>` : ''} 9184 <dt class="col-sm-3">ID:</dt> 9185 <dd class="col-sm-9"><small class="font-monospace">${cookie.id || 'N/A'}</small></dd> 9186 </dl> 9187 </div> 9188 </div> 9189 9190 <div class="row mt-3"> 9191 <div class="col-12"> 9192 <h6 class="text-primary">${translations['description'] || 'Description'} <span class="badge bg-success">${getLanguageNameByShortCode(selectedLanguageCode)}</span></h6> 9193 <p class="bg-light p-3 rounded">${displayDescription}</p> 9194 ${typeof cookie.description === 'object' ? 9195 `<small class="text-muted mb-3 d-block">${translations['availableLanguages'] || 'Available languages'} : ${Object.keys(cookie.description).map(code => getLanguageNameByShortCode(code)).join(', ')}</small>` : ''} 9196 </div> 9197 </div> 9198 9199 <div class="row"> 9200 <div class="col-md-6"> 9201 <h6 class="text-primary">${translations['vendorInformation'] || 'Vendor Information'}</h6> 9202 <dl class="row"> 9203 <dt class="col-sm-5">${translations['vendor'] || 'Vendor'} :</dt> 9204 <dd class="col-sm-7">${cookie.vendor || 'N/A'}</dd> 9205 <dt class="col-sm-5">${translations['privacyUrl'] || 'Privacy URL'}:</dt> 9206 <dd class="col-sm-7"> 9207 ${cookie.privacyUrl ? 9208 `<a href="${cookie.privacyUrl}" target="_blank" class="btn btn-sm btn-outline-primary"> 9209 <i class="fas fa-external-link-alt me-1"></i>${translations['viewPolicy'] || 'View Policy'} 9210 </a>` : 'N/A'} 9211 </dd> 9212 </dl> 9213 </div> 9214 </div> 9215 9216 ${cookie.script ? ` 9217 <div class="row mt-3"> 9218 <div class="col-12"> 9219 <h6 class="text-primary">${translations['associatedScript'] || 'Associated Script'}</h6> 9220 <pre class="bg-dark text-light p-3 rounded"><code>${cookie.script}</code></pre> 9221 </div> 9222 </div> 9223 ` : ''} 9224 </div> 9225 <div class="modal-footer"> 9226 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${translations['close'] || 'Close'}</button> 9227 <button type="button" class="btn btn-primary" onclick="showEditCookieModal(${JSON.stringify(cookie).replace(/"/g, '"')}, arguments[1], arguments[2])" data-bs-dismiss="modal"> 9228 <i class="fas fa-edit me-2"></i>${translations['editCookie'] || 'Edit Cookie'} 9229 </button> 9230 </div> 9231 </div> 9232 </div> 9233 `; 9234 9235 document.body.appendChild(modal); 9236 const bsModal = new bootstrap.Modal(modal); 9237 bsModal.show(); 9238 9239 modal.addEventListener('hidden.bs.modal', () => { 9240 document.body.removeChild(modal); 9241 }); 9242} 9243
9244function renderCookieTable(cookies, data, domainId, sortKey = null, sortDirection = 'asc') { 9245 const tbody = document.getElementById('cookiesTableBody'); 9246 tbody.innerHTML = ''; 9247 9248 let sortedCookies = [...cookies]; 9249 9250 if (sortKey) { 9251 sortedCookies.sort((a, b) => { 9252 let aVal = a[sortKey]; 9253 let bVal = b[sortKey]; 9254 9255 // Handle boolean values 9256 if (typeof aVal === 'boolean') { 9257 aVal = aVal ? 1 : 0; 9258 bVal = bVal ? 1 : 0; 9259 } 9260 9261 // Handle string values 9262 if (typeof aVal === 'string') { 9263 aVal = aVal.toLowerCase(); 9264 bVal = bVal ? bVal.toLowerCase() : ''; 9265 } 9266 9267 if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; 9268 if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; 9269 return 0; 9270 }); 9271 } 9272 9273 if (sortedCookies.length === 0) { 9274 const row = document.createElement('tr'); 9275 const cell = document.createElement('td'); 9276 cell.colSpan = 5; // Updated column count 9277 cell.classList.add('text-center', 'text-muted', 'py-5'); 9278 cell.innerHTML = ` 9279 <div class="d-flex flex-column align-items-center"> 9280 <i class="fas fa-cookie-bite fa-3x mb-3 text-muted"></i> 9281 <h5>${translations['noCookiesFound'] || 'No cookies found'}</h5> 9282 <p class="mb-3">${translations['addCookieNote'] || 'Click "Add Cookie" to create your first cookie entry.'} </p> 9283 <button class="btn btn-primary" onclick="showAddCookieModal(arguments[0], arguments[1])"> 9284 <i class="fa fa-plus me-2"></i>${translations['addCookie'] || 'Add Cookie'} 9285 </button> 9286 </div> 9287 `; 9288 row.appendChild(cell); 9289 tbody.appendChild(row); 9290 } else { 9291 sortedCookies.forEach(cookie => { 9292 const row = createEnhancedCookieRow(cookie, data, domainId); 9293 tbody.appendChild(row); 9294 }); 9295 } 9296} 9297 9298function filterAndSearch(allCookies, data, domainId) { 9299 const searchInput = document.querySelector('#cookie_manager_container input[placeholder="Search cookies..."]'); 9300 const filterSelect = document.querySelector('#cookie_manager_container select'); 9301 9302 if (!searchInput || !filterSelect) { 9303 console.error('Search input or filter select not found'); 9304 return; 9305 } 9306 9307 const searchTerm = searchInput.value.toLowerCase(); 9308 const filterType = filterSelect.value; 9309 9310 let filteredCookies = allCookies.filter(cookie => { 9311 // Type filter 9312 if (filterType !== 'all' && cookie.type !== filterType) { 9313 return false; 9314 } 9315 9316 // Search filter - Updated search fields with multilingual description support 9317 if (searchTerm) { 9318 const searchFields = [ 9319 cookie.name, 9320 cookie.displayName, 9321 cookie.vendor, 9322 cookie.type 9323 ].map(field => (field || '').toLowerCase()); 9324 9325 // Add description search (check all language versions) 9326 if (cookie.description) { 9327 if (typeof cookie.description === 'object') { 9328 // Search all language versions 9329 Object.values(cookie.description).forEach(desc => { 9330 if (desc) searchFields.push(desc.toLowerCase()); 9331 }); 9332 } else { 9333 // Handle legacy string descriptions 9334 searchFields.push(cookie.description.toLowerCase()); 9335 } 9336 } 9337 9338 return searchFields.some(field => field.includes(searchTerm)); 9339 } 9340 9341 return true; 9342 }); 9343 9344 renderCookieTable(filteredCookies, data, domainId); 9345} 9346 9347let currentSort = { key: null, direction: 'asc' }; 9348 9349function sortTable(key) { 9350 const direction = currentSort.key === key && currentSort.direction === 'asc' ? 'desc' : 'asc'; 9351 currentSort = { key, direction }; 9352 9353 // Update sort indicators 9354 document.querySelectorAll('th i.fa-sort, th i.fa-sort-up, th i.fa-sort-down').forEach(icon => { 9355 icon.className = 'fas fa-sort text-muted ms-1'; 9356 }); 9357 9358 const header = document.querySelector(`th:nth-child(${getColumnIndex(key) + 1}) i:last-child`); 9359 if (header) { 9360 header.className = `fas fa-sort-${direction === 'asc' ? 'up' : 'down'} text-primary ms-1`; 9361 } 9362 9363 // Re-render with sorting - Fixed selectors 9364 const searchInput = document.querySelector('#cookie_manager_container input[placeholder="Search cookies..."]'); 9365 const filterSelect = document.querySelector('#cookie_manager_container select'); 9366 9367 // Get current filtered cookies 9368 const allCookies = window.currentCookies || []; 9369 let filteredCookies = allCookies.filter(cookie => { 9370 const filterType = filterSelect ? filterSelect.value : 'all'; 9371 const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; 9372 9373 if (filterType !== 'all' && cookie.type !== filterType) { 9374 return false;
9375 } 9376 9377 if (searchTerm) { 9378 const searchFields = [ 9379 cookie.name, 9380 cookie.displayName, 9381 cookie.vendor, 9382 cookie.type 9383 ].map(field => (field || '').toLowerCase()); 9384 9385 // Add description search (check all language versions) 9386 if (cookie.description) { 9387 if (typeof cookie.description === 'object') { 9388 // Search all language versions 9389 Object.values(cookie.description).forEach(desc => { 9390 if (desc) searchFields.push(desc.toLowerCase()); 9391 }); 9392 } else { 9393 // Handle legacy string descriptions 9394 searchFields.push(cookie.description.toLowerCase()); 9395 } 9396 } 9397 9398 return searchFields.some(field => field.includes(searchTerm)); 9399 } 9400 9401 return true; 9402 }); 9403 9404 renderCookieTable(filteredCookies, window.currentData, window.currentDomainId, key, direction); 9405} 9406 9407function getColumnIndex(key) { 9408 const columns = ['displayName', 'name', 'type', 'retentionPeriod', 'actions']; 9409 return columns.indexOf(key); 9410} 9411 9412function showImportModal(data, domainId) { 9413 const modal = document.createElement('div'); 9414 modal.classList.add('modal', 'fade'); 9415 modal.tabIndex = -1; 9416 9417 modal.innerHTML = ` 9418 <div class="modal-dialog"> 9419 <div class="modal-content"> 9420 <div class="modal-header"> 9421 <h5 class="modal-title"> 9422 <i class="fa fa-upload me-2"></i>Import Cookies from JSON 9423 </h5> 9424 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 9425 </div> 9426 <div class="modal-body"> 9427 <div id="importErrors" class="alert alert-danger d-none"></div> 9428 <div id="importWarnings" class="alert alert-warning d-none"></div> 9429 <div class="mb-3"> 9430 <label for="importFile" class="form-label">Select JSON file</label> 9431 <input type="file" class="form-control" id="importFile" accept=".json"> 9432 <div class="form-text">Choose a JSON file exported from the cookie manager</div> 9433 </div> 9434 <div class="mb-3"> 9435 <div class="form-check"> 9436 <input class="form-check-input" type="checkbox" id="replaceExisting" checked> 9437 <label class="form-check-label" for="replaceExisting"> 9438 Replace existing cookies with same name 9439 </label> 9440 </div> 9441 </div> 9442 <div id="importPreview" class="d-none"> 9443 <h6>Preview:</h6> 9444 <div id="previewContent" class="bg-light p-3 rounded"> 9445 <small class="text-muted">File preview will appear here...</small> 9446 </div> 9447 </div> 9448 </div> 9449 <div class="modal-footer"> 9450 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> 9451 <button type="button" class="btn btn-primary" id="importBtn" disabled> 9452 <i class="fa fa-upload me-2"></i>Import Cookies 9453 </button> 9454 </div> 9455 </div> 9456 </div> 9457 `; 9458 9459 // Add import functionality 9460 const fileInput = modal.querySelector('#importFile'); 9461 const importBtn = modal.querySelector('#importBtn'); 9462 const errorContainer = modal.querySelector('#importErrors'); 9463 const warningContainer = modal.querySelector('#importWarnings'); 9464 const previewContainer = modal.querySelector('#importPreview'); 9465 const previewContent = modal.querySelector('#previewContent'); 9466 const replaceCheckbox = modal.querySelector('#replaceExisting'); 9467 9468 let importData = null; 9469 9470 fileInput.addEventListener('change', (event) => { 9471 const file = event.target.files[0]; 9472 if (!file) { 9473 importBtn.disabled = true; 9474 previewContainer.classList.add('d-none'); 9475 return; 9476 } 9477 9478 if (!file.name.endsWith('.json')) { 9479 showImportError('Please select a valid JSON file'); 9480 return; 9481 } 9482 9483 const reader = new FileReader(); 9484 reader.onload = (e) => { 9485 try { 9486 importData = JSON.parse(e.target.result); 9487 9488 if (!Array.isArray(importData)) { 9489 showImportError('Invalid format: JSON must contain an array of cookies'); 9490 return; 9491 } 9492 9493 // Validate cookie structure 9494 const validCookies = importData.filter(cookie => { 9495 return cookie.name && cookie.name.trim() !== '' && 9496 cookie.type && cookie.type.trim() !== ''; 9497 }); 9498 9499 const invalidCount = importData.length - validCookies.length; 9500 9501 if (validCookies.length === 0) { 9502 showImportError('No valid cookies found in the file. Cookies must have name and type.'); 9503 return; 9504 } 9505 9506 if (invalidCount > 0) { 9507 const warningMessage = `Found ${invalidCount}
9507 invalid cookie${invalidCount > 1 ? 's' : ''} that will be skipped. Valid cookies: ${validCookies.length}`; 9508 showImportWarning(warningMessage); 9509 } 9510 9511 // Show preview 9512 hideImportError(); 9513 showPreview(validCookies, invalidCount); 9514 importBtn.disabled = false; 9515 9516 // Store only valid cookies for import 9517 importData = validCookies; 9518 9519 } catch (error) { 9520 showImportError('Invalid JSON file: ' + error.message); 9521 } 9522 }; 9523 9524 reader.readAsText(file); 9525 }); 9526 9527 importBtn.addEventListener('click', async () => { 9528 if (!importData) return; 9529 9530 try { 9531 importBtn.disabled = true; 9532 importBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Importing...'; 9533 9534 await importCookies(importData, data, domainId, replaceCheckbox.checked); 9535 9536 bootstrap.Modal.getInstance(modal).hide(); 9537 displayAlertCookie(`Successfully imported ${importData.length} valid cookies!`, 'success'); 9538 9539 // Reload the cookie manager 9540 loadCookiesForDomain(domainId); 9541 9542 } catch (error) { 9543 console.error('Import error:', error); 9544 showImportError('Error importing cookies: ' + error.message); 9545 importBtn.disabled = false; 9546 importBtn.innerHTML = '<i class="fa fa-upload me-2"></i>Import Cookies'; 9547 } 9548 }); 9549 9550 function showImportError(message) { 9551 errorContainer.textContent = message; 9552 errorContainer.classList.remove('d-none'); 9553 warningContainer.classList.add('d-none'); 9554 previewContainer.classList.add('d-none'); 9555 importBtn.disabled = true; 9556 } 9557 9558 function showImportWarning(message) { 9559 warningContainer.textContent = message; 9560 warningContainer.classList.remove('d-none'); 9561 errorContainer.classList.add('d-none'); 9562 } 9563 9564 function hideImportError() { 9565 errorContainer.classList.add('d-none'); 9566 warningContainer.classList.add('d-none'); 9567 } 9568 9569 function showPreview(cookies, invalidCount = 0) { 9570 const typeCount = {}; 9571 cookies.forEach(cookie => { 9572 typeCount[cookie.type] = (typeCount[cookie.type] || 0) + 1; 9573 }); 9574 9575 const previewHtml = ` 9576 <div><strong>Found ${cookies.length} valid cookies to import:</strong></div> 9577 ${invalidCount > 0 ? `<div class="text-warning"><small>${invalidCount} invalid cookie${invalidCount > 1 ? 's' : ''} will be skipped</small></div>` : ''} 9578 <ul class="mt-2 mb-0"> 9579 ${Object.entries(typeCount).map(([type, count]) => 9580 `<li>${count} ${type} cookie${count > 1 ? 's' : ''}</li>` 9581 ).join('')} 9582 </ul> 9583 `; 9584 9585 previewContent.innerHTML = previewHtml; 9586 previewContainer.classList.remove('d-none'); 9587 } 9588 9589 document.body.appendChild(modal); 9590 const bsModal = new bootstrap.Modal(modal); 9591 bsModal.show(); 9592 9593 modal.addEventListener('hidden.bs.modal', () => { 9594 document.body.removeChild(modal); 9595 }); 9596} 9597 9598async function importCookies(cookiesData, data, domainId, replaceExisting = true) { 9599 // Get current cookies from the backend first 9600 const loadData = { 9601 sid: getSid(), 9602 domainId: parseInt(domainId) 9603 }; 9604 9605 const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData); 9606 9607 if (!loadResponse.ok) { 9608 throw new Error('Failed to load current cookies: ' + loadResponse.status); 9609 } 9610 9611 const loadResponseBody = await loadResponse.json(); 9612 if (loadResponseBody.statusCode !== 0) { 9613 throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error')); 9614 } 9615 9616 // Get current resources 9617 let resources = loadResponseBody.resources || []; 9618 9619 // Process imported cookies 9620 const processedCookies = cookiesData.map(cookie => { 9621 // Ensure required fields and set defaults 9622 const processedCookie = { 9623 name: cookie.name, 9624 displayName: cookie.displayName || cookie.name, 9625 type: cookie.type || 'other', 9626 retentionPeriod: cookie.retentionPeriod || 'Session', 9627 custom: true, // Mark imported cookies as custom 9628 description: cookie.description || {}, 9629 vendor: cookie.vendor || '', 9630 privacyUrl: cookie.privacyUrl || '', 9631 script: cookie.script || '', 9632 id: generateCookieId() // Generate new ID 9633 }; 9634 9635 return processedCookie; 9636 }); 9637 9638 // Handle replacement or addition 9639 if (replaceExisting) { 9640 // Remove existing cookies with same names 9641 const importedNames = processedCookies.map(c => c.name); 9642 resources = resources.filter(r => !importedNames.includes(r.name)); 9643 } 9644 9645 // Add imported cookies 9646 resources.push(...processedCookies); 9647 9648 // Save updated resources 9649 await saveCookieResources(resources, { value: { resources } }, domainId); 9650} 9651 9652function exportCookies(cookies) { 9653 const exportData = cookies.map(cookie => ({ 9654 name: cookie.name, 9655 displayName: cookie.displayName, 9656 type: cookie.type, 9657 retentionPeriod: cookie.retentionPeriod, 9658 custom: cookie.custom, 9659 description: cookie.description, 9660 vendor: cookie.vendor, 9661 privacyUrl: cookie.privacyUrl, 9662 script: cookie.script, 9663 id: cookie.id 9664 })); 9665 9666 const jsonString = JSON.stringify(exportData, null, 2); 9667 const blob = new Blob([jsonString], { type: 'application/json' }); 9668 const url = URL.createObjectURL(blob); 9669 9670 const link = document.createElement('a'); 9671 link.href = url; 9672 link.download = `cookies-export-${new Date().toISOString().split('T')[0]}.json`; 9673 document.body.appendChild(link); 9674 link.click(); 9675 document.body.removeChild(link); 9676 URL.revokeObjectURL(url); 9677 9678 displayAlertCookie('Cookies exported successfully!', 'success'); 9679} 9680 9681async function addCookie(cookieData, data, domainId) { 9682 try { 9683 console.log('addCookie called with:'); 9684 console.log('- cookieData:', cookieData); 9685 console.log('- domainId:', domainId); 9686 console.log('- data:', data); 9687 9688 // Validate domainId 9689 if (!domainId || domainId === 'null' || domainId === null) { 9690 console.error('Invalid domainId:', domainId); 9691 9692 // Try to get domainId from dropdown 9693 const domainDropdown = document.getElementById('select_domain'); 9694 if (domainDropdown && domainDropdown.value) { 9695 domainId = domainDropdown.value; 9696 console.log('Retrieved domainId from dropdown:', domainId); 9697 } else { 9698 displayAlertCookie('Error: No domain selected. Please select a domain first.', 'error'); 9699 return; 9700 } 9701 } 9702 9703 // Get current cookies from the backend first
9704 const loadData = { 9705 sid: getSid(), 9706 domainId: parseInt(domainId) 9707 }; 9708 9709 const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData); 9710 9711 if (!loadResponse.ok) { 9712 throw new Error('Failed to load current cookies: ' + loadResponse.status); 9713 } 9714 9715 const loadResponseBody = await loadResponse.json(); 9716 if (loadResponseBody.statusCode !== 0) { 9717 throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error')); 9718 } 9719 9720 // Get current resources 9721 let resources = loadResponseBody.resources || []; 9722 9723 // Add new cookie 9724 resources.push(cookieData); 9725 9726 // Save updated resources 9727 await saveCookieResources(resources, { value: { resources } }, domainId); 9728 displayAlertCookie(translations['cookieAddedSuccessfully'] || 'Cookie added successfully!', 'success'); 9729 9730 // Reload the cookie manager 9731 loadCookiesForDomain(domainId); 9732 } catch (error) { 9733 console.error('Error adding cookie:', error); 9734 displayAlertCookie('Error adding cookie: ' + error.message, 'error'); 9735 } 9736} 9737 9738async function updateCookie(cookieData, data, domainId, originalCookie) { 9739 try { 9740 console.log('updateCookie called with:'); 9741 console.log('- cookieData:', cookieData); 9742 console.log('- domainId:', domainId); 9743 console.log('- originalCookie:', originalCookie); 9744 9745 // Validate domainId 9746 if (!domainId || domainId === 'null' || domainId === null) { 9747 console.error('Invalid domainId:', domainId); 9748 9749 // Try to get domainId from dropdown 9750 const domainDropdown = document.getElementById('select_domain'); 9751 if (domainDropdown && domainDropdown.value) { 9752 domainId = domainDropdown.value; 9753 console.log('Retrieved domainId from dropdown:', domainId); 9754 } else { 9755 displayAlertCookie('Error: No domain selected. Please select a domain first.', 'error'); 9756 return; 9757 } 9758 } 9759 9760 // Get current cookies from the backend first 9761 const loadData = { 9762 sid: getSid(), 9763 domainId: parseInt(domainId) 9764 }; 9765 9766 const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData); 9767 9768 if (!loadResponse.ok) { 9769 throw new Error('Failed to load current cookies: ' + loadResponse.status); 9770 } 9771 9772 const loadResponseBody = await loadResponse.json(); 9773 if (loadResponseBody.statusCode !== 0) { 9774 throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error')); 9775 } 9776 9777 // Get current resources 9778 let resources = loadResponseBody.resources || []; 9779 9780 // Find and update the cookie 9781 const index = resources.findIndex(r => 9782 r.id === originalCookie.id || 9783 (r.name === originalCookie.name && r.displayName === originalCookie.displayName) 9784 ); 9785 9786 if (index !== -1) { 9787 resources[index] = { ...resources[index], ...cookieData }; 9788 } else { 9789 resources.push(cookieData); 9790 } 9791 9792 // Save updated resources 9793 await saveCookieResources(resources, { value: { resources } }, domainId); 9794 displayAlertCookie(translations['cookieUpdatedSuccessfully'] || 'Cookie updated successfully!', 'success'); 9795 9796 // Reload the cookie manager 9797 loadCookiesForDomain(domainId); 9798 } catch (error) { 9799 console.error('Error updating cookie:', error); 9800 displayAlertCookie('Error updating cookie: ' + error.message, 'error'); 9801 } 9802} 9803 9804async function cookieManagerDeleteCookie(cookie, data, domainId) { 9805 try { 9806 // Get current cookies from the backend first 9807 const loadData = { 9808 sid: getSid(), 9809 domainId: parseInt(domainId) 9810 }; 9811 9812 const loadResponse = await postRequest(endpoint + '/cmp/page-cookies', loadData); 9813 9814 if (!loadResponse.ok) { 9815 throw new Error('Failed to load current cookies: ' + loadResponse.status); 9816 } 9817 9818 const loadResponseBody = await loadResponse.json(); 9819 if (loadResponseBody.statusCode !== 0) { 9820 throw new Error('Error loading current cookies: ' + (loadResponseBody.statusMessage || 'Unknown error')); 9821 } 9822 9823 // Get current resources 9824 let resources = loadResponseBody.resources || []; 9825 9826 // Remove the cookie 9827 const index = resources.findIndex(r => r.id === cookie.id || 9828 (r.name === cookie.name && r.displayName === cookie.displayName)); 9829 9830 if (index !== -1) { 9831 resources.splice(index, 1); 9832 } 9833 9834 // Save updated resources 9835 await saveCookieResources(resources, { value: { resources } }, domainId); 9836 displayAlertCookie(translations['cookieDeletedSuccessfully'] || 'Cookie deleted successfully!', 'success'); 9837 9838 // Reload the cookie manager 9839 loadCookiesForDomain(domainId); 9840 } catch (error) { 9841 console.error('Error deleting cookie:', error); 9842 displayAlertCookie('Error deleting cookie: ' + error.message, 'error'); 9843 } 9844} 9845 9846async function deleteCookieConfirm(cookie, data, domainId) { 9847 if (await showConfirm((translations['confirmDeleteCookie'] || 'Are you sure you want to delete this cookie?') + '\n' + 9848 (translations['cookieName'] || 'Cookie Name') + ': ' + cookie.name, translations['delete'] || 'Delete')) { 9849 cookieManagerDeleteCookie(cookie, data, domainId); 9850 } 9851} 9852 9853async function saveCookieResources(resources, originalData, domainId) { 9854 console.log('saveCookieResources called with domainId:', domainId); 9855 9856 // Validate domainId 9857 if (!domainId || domainId === 'null' || domainId === null) { 9858 throw new Error('Invalid domain ID: ' + domainId); 9859 } 9860 9861 const saveData = { 9862 sid: getSid(), 9863 domainId: parseInt(domainId), // Use domainId directly 9864 value: { 9865 resources: resources, 9866 lastUpdated: new Date().getTime() 9867 } 9868 }; 9869 9870 console.log('Saving cookie data:', JSON.stringify(saveData, null, 2)); 9871 9872 const response = await postRequest(endpoint + '/cmp/page-cookies', saveData); 9873 9874 if (!response.ok) { 9875 throw new Error('Failed to save cookie data: ' + response.status); 9876 } 9877 9878 const responseBody = await response.json(); 9879 if (responseBody.statusCode !== 0) { 9880 throw new Error('Error saving cookie data: ' + (responseBody.statusMessage || 'Unknown error')); 9881 } 9882 9883 return responseBody; 9884} 9885 9886async function loadCookiesForDomain(domainId) { 9887 const container = document.getElementById('cookie_manager_container'); 9888 container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="mt-3 text-muted">Loading cookies...</p></div>'; 9889 9890 try { 9891 const data = { 9892 sid: getSid(), 9893 domainId: parseInt(domainId) // Use domainId directly for loading (no value parameter) 9894 }; 9895 9896 const response = await postRequest(endpoint + '/cmp/page-cookies', data); 9897 9898 if (response.ok) { 9899 const responseBody = await response.json(); 9900 9901 if (responseBody.statusCode === 0) { 9902 // Backend returns cookies in "resources" field directly 9903 const cookies = responseBody.resources || []; 9904 9905 // Create the data structure expected by the frontend 9906 const formattedResponse = { 9907 value: { 9908 resources: cookies 9909 } 9910 }; 9911 9912 // Store for global access 9913 window.currentCookies = cookies.filter(resource => { 9914 if (!resource) return false;
9915 const cookieTypes = ['Essential', 'Personalization', 'Analytics', 'Marketing', 'Security', 'Other']; 9916 return !resource.type || 9917 resource.type === 'cookie' || 9918 cookieTypes.includes(resource.type) || 9919 (resource.name && resource.displayName); 9920 }); 9921 9922 window.currentData = formattedResponse; 9923 window.currentDomainId = domainId; // Changed from currentProjectId to currentDomainId 9924 9925 displayEnhancedCookieManager(formattedResponse, domainId); 9926 } else { 9927 console.error('Error loading cookies:', responseBody.statusMessage); 9928 displayAlertCookie('Failed to load cookies: ' + responseBody.statusMessage, 'error'); 9929 } 9930 } else { 9931 console.error('HTTP Error: ' + response.status + ' - ' + response.statusText); 9932 displayAlertCookie('Failed to load cookies: ' + response.statusText, 'error'); 9933 } 9934 } catch (error) { 9935 console.error('Error loading cookies:', error); 9936 displayAlertCookie('Error loading cookies: ' + error.message, 'error'); 9937 } 9938} 9939 9940function generateCookieId() { 9941 return Date.now() + Math.floor(Math.random() * 1000); 9942} 9943 9944function displayAlertCookie(message, type = 'info') { 9945 // Toast, not an inline alert (see displayAlertDocument). Cookie-manager messages 9946 // carry markup, so they render as HTML like before. 9947 showToast(message, type === 'error' ? 'error' : type === 'success' ? 'success' : 'info', { html: true }); 9948} 9949 9950// Debug function to test modal form elements and domainId 9951function debugModalForm() { 9952 const modal = document.querySelector('.modal'); 9953 if (!modal) { 9954 console.log('No modal found'); 9955 return; 9956 } 9957 9958 const domainId = modal.getAttribute('data-domain-id'); 9959 const domainDropdown = document.getElementById('select_domain'); 9960 const resourceNameInput = modal.querySelector('#cookieResourceName'); 9961 const typeSelect = modal.querySelector('#cookieType'); 9962 9963 console.log('=== MODAL DEBUG INFO ==='); 9964 console.log('Modal found:', !!modal); 9965 console.log('DomainId from modal:', domainId); 9966 console.log('DomainId from dropdown:', domainDropdown ? domainDropdown.value : 'Dropdown not found'); 9967 console.log('Resource name input:', resourceNameInput); 9968 console.log('Resource name value:', resourceNameInput ? `"${resourceNameInput.value}"` : 'N/A'); 9969 console.log('Type select:', typeSelect); 9970 console.log('Type value:', typeSelect ? `"${typeSelect.value}"` : 'N/A'); 9971 console.log('Type options:', typeSelect ? Array.from(typeSelect.options).map(o => `${o.value}: ${o.text}`) : 'N/A'); 9972 9973 // List all form inputs in the modal 9974 const allInputs = modal.querySelectorAll('input, select, textarea'); 9975 console.log('All form elements in modal:'); 9976 allInputs.forEach((input, index) => { 9977 console.log(`${index}: ${input.tagName} - ID: ${input.id} - Value: "${input.value}"`); 9978 }); 9979 9980 console.log('=== END DEBUG INFO ==='); 9981} 9982 9983 9984 9985// Cookie Scanner 9986 9987function showHideIpList(){ 9988 showIpModal(); 9989} 9990 9991function showIpModal() { 9992 // Create modal element 9993 const modal = document.createElement('div'); 9994 modal.classList.add('modal', 'fade'); 9995 modal.tabIndex = -1; 9996 modal.id = 'ipListModal'; 9997 9998 modal.innerHTML = ` 9999 <div class="modal-dialog modal-lg"> 10000 <div class="modal-content"> 10001 <div class="modal-header"> 10002 <h5 class="modal-title"> 10003 <i class="fas fa-server me-2"></i>${translations['ipListTitle'] || 'Cookie Scanner IP Addresses'} 10004 </h5> 10005 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${translations['close'] || 'Close'}"></button> 10006 </div> 10007 <div class="modal-body"> 10008 <div class="mb-3"> 10009 <p class="text-muted"> 10010 ${translations['ipListDescription'] || 'Please add these IP addresses to your firewall whitelist to allow our cookie scanner to access your website:'} 10011 </p> 10012 </div> 10013 <div class="mb-3"> 10014 <label for="ipListTextarea" class="form-label"> 10015 <i class="fas fa-list me-1"></i>${translations['ipAddresses'] || 'IP Addresses'} 10016 </label> 10017 <textarea 10018 class="form-control font-monospace" 10019 id="ipListTextarea" 10020 rows="12" 10021 readonly 10022 style="font-size: 0.9em; line-height: 1.4;" 10023 > 79.137.17.189 10024 178.32.101.196 10025 51.254.152.157 10026 188.165.5.60 10027 87.98.166.59 10028 176.31.11.245 10029 94.23.146.169 10030 217.182.28.236 10031 188.165.30.212 10032 178.32.43.167 10033 188.165.128.184 10034 79.137.17.188 10035 164.132.128.233 10036 178.32.102.33 10037 164.132.128.232 10038 54.37.60.131 10039 94.23.120.181 10040 51.254.238.200 10041 178.32.40.154 10042 79.137.17.187 10043 51.210.174.175 10044 147.135.189.8 10045 178.33.37.237 10046 51.38.17.124 10047 137.74.105.15 10048 137.74.105.12 10049 193.70.78.212 10050 46.105.228.180 10051 188.165.164.148 10052 149.202.132.28 10053 46.105.246.246 10054 79.137.24.177 10055 217.182.6.61 10056 178.32.102.74 10057 51.68.74.30 10058 79.137.17.191 10059 79.137.17.185 10060 94.23.120.174 10061 94.23.120.178 10062 176.31.9.177 10063 137.74.105.13 10064 46.105.246.245 10065 141.94.41.69 10066 164.132.88.84 10067 51.210.238.3 10068 5.135.237.225 10069 5.135.20.151 10070 91.134.170.84</textarea> 10071 </div> 10072 <div class="d-flex gap-2"> 10073 <button type="button" class="btn btn-primary" id="copyIpListBtn"> 10074 <i class="fas fa-copy me-2"></i>${translations['copyToClipboard'] || 'Copy to Clipboard'} 10075 </button> 10076 10077 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> 10078 <i class="fas fa-times me-2"></i>${translations['close'] || 'Close'} 10079 </button> 10080 </div> 10081 </div> 10082 </div> 10083 `; 10084 10085 // Append modal to body 10086 document.body.appendChild(modal); 10087 10088 // Initialize Bootstrap modal 10089 const bsModal = new bootstrap.Modal(modal); 10090 10091 // Add event listeners after modal is shown 10092 modal.addEventListener('shown.bs.modal', () => { 10093 const copyBtn = document.getElementById('copyIpListBtn'); 10094 const textarea = document.getElementById('ipListTextarea'); 10095 10096 // Copy to clipboard functionality 10097 copyBtn.addEventListener('click', async () => { 10098 try { 10099 // Always copy the full textarea value, regardless of selection 10100 await navigator.clipboard.writeText(textarea.value); 10101 10102 // Show success feedback 10103 const originalText = copyBtn.innerHTML; 10104 copyBtn.innerHTML = '<i class="fas fa-check me-2"></i>' + (translations['copied'] || 'Copied!'); 10105 copyBtn.classList.remove('btn-primary'); 10106 copyBtn.classList.add('btn-success'); 10107 10108 setTimeout(() => { 10109 copyBtn.innerHTML = originalText; 10110 copyBtn.classList.remove('btn-success'); 10111 copyBtn.classList.add('btn-primary'); 10112 }, 2000); 10113 10114 } catch (err) { 10115 // Fallback for older browsers - select all then copy 10116 textarea.select(); 10117 document.execCommand('copy'); 10118 10119 // Show success feedback 10120 const originalText = copyBtn.innerHTML; 10121 copyBtn.innerHTML = '<i class="fas fa-check me-2"></i>' + (translations['copied'] || 'Copied!'); 10122 copyBtn.classList.remove('btn-primary'); 10123 copyBtn.classList.add('btn-success'); 10124 10125 setTimeout(() => { 10126 copyBtn.innerHTML = originalText; 10127 copyBtn.classList.remove('btn-success'); 10128 copyBtn.classList.add('btn-primary'); 10129 }, 2000); 10130 } 10131 }); 10132 }); 10133 10134 // Show the modal 10135 bsModal.show(); 10136 10137 // Clean up modal when closed 10138 modal.addEventListener('hidden.bs.modal', () => { 10139 document.body.removeChild(modal); 10140 }); 10141 } 10142 10143function createCookieScannerSection(domainId) { 10144 const scannerContainer = document.createElement('div'); 10145 scannerContainer.classList.add('card', 'mb-4'); 10146 scannerContainer.id = 'cookie_scanner_section'; 10147 10148 scannerContainer.innerHTML = ` 10149 <div class="card-header"> 10150 <h5 class="card-title mb-0"> 10151 <i class="fas fa-search me-2"></i>${translations['cookieScanner'] || 'Cookie Scanner'} 10152 </h5> 10153 </div> 10154 <div class="card-body"> 10155 <div class="row align-items-center"> 10156 <div class="col-md-8"> 10157 <h6 class="text-primary mb-3"> 10158 <i class="fas fa-robot me-2"></i>${translations['automaticDetection'] || 'Automatic Cookie Detection'} 10159 </h6> 10160 <p class="text-muted mb-3"> 10161 ${translations['cookieScannerDescription'] || 'Scan your website to automatically detect cookies and update your cookie list. Our advanced scanner analyzes your web pages and identifies all cookies being used, including tracking pixels, analytics cookies, and third-party integrations.'} 10162 </p> 10163 <div class="mb-2"> 10164 <h6 class="text-success mb-2"> 10165 <i class="fas fa-check-circle me-2"></i>${translations['scanFeatures'] || 'What Our Scanner Detects:'} 10166 </h6> 10167 <ul class="list-unstyled mb-0"> 10168 <li class="mb-1"> 10169 <i class="fas fa-cookie-bite text-primary me-2"></i> 10170 ${translations['feature1'] || 'Essential and functional cookies'} 10171 </li> 10172 <li class="mb-1"> 10173 <i class="fas fa-chart-line text-info me-2"></i> 10174 ${translations['feature2'] || 'Analytics and performance tracking'} 10175 </li> 10176 <li class="mb-1"> 10177 <i class="fas fa-bullhorn text-warning me-2"></i> 10178 ${translations['feature3'] || 'Marketing and advertising cookies'} 10179 </li> 10180 <li class="mb-1"> 10181 <i class="fas fa-user-cog text-secondary me-2"></i> 10182 ${translations['feature4'] || 'Personalization and preference cookies'} 10183 </li> 10184 <li class="mb-0"> 10185 <i class="fas fa-puzzle-piece text-success me-2"></i> 10186 ${translations['feature5'] || 'Third-party integrations and plugins'} 10187 </li> 10188 </ul> 10189 </div> 10190 </div> 10191 <div class="col-md-4"> 10192 <div class="text-center"> 10193 <div class="mb-4"> 10194 <th><i class="fas fa-cogs me-1"></i><strong>${translations['scannerActions'] || 'Scanner Actions'}</strong></th> 10195 </div> 10196 <div class="d-grid gap-2"> 10197 <button type="button" class="btn btn-success btn-lg" id="newScanBtn"> 10198 <i class="fas fa-play me-2"></i>${translations['startNewScan'] || 'Start New Scan'} 10199 </button> 10200 <button type="button" class="btn btn-info" id="viewStatusBtn"> 10201 <i class="fas fa-list me-2"></i>${translations['viewStatus'] || 'View Status'} 10202 </button> 10203 </div> 10204 10205 <div class="mt-3 pt-3 border-top"> 10206 <small class="text-muted"> 10207 <i class="fas fa-clock me-1"></i> 10208 ${translations['scanTime'] || 'Typical scan time: 1-3 hours.'} 10209 </small> 10210 </div> 10211 </div> 10212 </div> 10213 </div> 10214 </div> 10215 `; 10216 10217 return scannerContainer; 10218} 10219 10220function showScanUrlModal(domainId) { 10221 // Get default URL from selected domain 10222 const domainDropdown = document.getElementById('select_domain'); 10223 let defaultUrl = 'https://example.com'; 10224 if (domainDropdown && domainDropdown.selectedOptions.length > 0) { 10225 const selectedDomain = domainDropdown.options[domainDropdown.selectedIndex].text; 10226 defaultUrl = 'https://' + selectedDomain + '/'; 10227 } 10228 10229 // Store domainId globally for the inline handlers 10230 window.currentScanDomainId = domainId; 10231 10232 const modal = document.createElement('div'); 10233 modal.classList.add('modal', 'fade'); 10234 modal.tabIndex = -1; 10235 modal.id = 'scanUrlModal'; 10236 10237 modal.innerHTML = ` 10238 <div class="modal-dialog modal-lg"> 10239 <div class="modal-content"> 10240 <div class="modal-header"> 10241 <h5 class="modal-title"> 10242 <i class="fas fa-cog me-2"></i>${translations['configureScan'] || 'Configure Scan'} 10243 </h5> 10244 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${translations['close'] || 'Close'}"></button> 10245 </div> 10246 <div class="modal-body"> 10247 <div class="mb-3"> 10248 <p class="text-muted"> 10249 ${translations['scanModalDescription'] || 'Enter the URL of the page you want to scan for cookies. Our scanner will analyze the page and detect all c
10249ookies being used.'} 10250 </p> 10251 </div> 10252 <div class="mb-3"> 10253 <label for="modalScanUrl" class="form-label"> 10254 <i class="fas fa-link me-1"></i>${translations['downloadPageUrl'] || 'Download Page URL'} <span class="text-danger">*</span> 10255 </label> 10256 <input type="url" class="form-control" id="modalScanUrl" 10257 value="${defaultUrl}" 10258 placeholder="https://example.com" 10259 onkeypress="handleScanUrlKeypress(event)" 10260 oninput="validateScanUrl()" 10261 required> 10262 <div class="form-text"> 10263 ${translations['scanUrlHelper'] || 'Enter the URL of the page you want to scan for cookies'} 10264 </div> 10265 <div class="invalid-feedback"> 10266 Please enter a valid URL 10267 </div> 10268 </div> 10269 10270 <div class="alert alert-info" role="alert"> 10271 <i class="fas fa-info-circle me-2"></i> 10272 <strong>${translations['scanTip'] || 'Tip:'}</strong> ${translations['scanTipText'] || 'For best results, scan your homepage or a page that loads all your tracking scripts and cookies.'} 10273 </div> 10274 </div> 10275 <div class="modal-footer"> 10276 <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"> 10277 <i class="fas fa-times me-2"></i>${translations['cancel'] || 'Cancel'} 10278 </button> 10279 <button type="button" class="btn btn-success" id="modalScanBtn" onclick="executeScan()"> 10280 <i class="fas fa-play me-2"></i>${translations['startScan'] || 'Start Scan'} 10281 </button> 10282 </div> 10283 </div> 10284 </div> 10285 `; 10286 10287 document.body.appendChild(modal); 10288 const bsModal = new bootstrap.Modal(modal); 10289 10290 // Store modal reference globally for cleanup 10291 window.currentScanModal = bsModal; 10292 window.currentScanModalElement = modal; 10293 10294 bsModal.show(); 10295 10296 // Auto-focus and select after modal is shown 10297 modal.addEventListener('shown.bs.modal', () => { 10298 const urlInput = document.getElementById('modalScanUrl'); 10299 if (urlInput) { 10300 setTimeout(() => { 10301 urlInput.focus(); 10302 urlInput.select(); 10303 validateScanUrl(); // Initial validation 10304 }, 100); 10305 } 10306 }); 10307 10308 modal.addEventListener('hidden.bs.modal', () => { 10309 document.body.removeChild(modal); 10310 // Clean up global variables 10311 delete window.currentScanDomainId; 10312 delete window.currentScanModal; 10313 delete window.currentScanModalElement; 10314 }); 10315} 10316 10317function validateScanUrl() { 10318 const urlInput = document.getElementById('modalScanUrl'); 10319 const scanBtn = document.getElementById('modalScanBtn'); 10320 10321 if (urlInput && scanBtn) { 10322 const isValid = urlInput.value.trim() !== '' && urlInput.checkValidity(); 10323 scanBtn.disabled = !isValid; 10324 10325 if (isValid) { 10326 urlInput.classList.remove('is-invalid'); 10327 urlInput.classList.add('is-valid'); 10328 } else { 10329 urlInput.classList.remove('is-valid'); 10330 if (urlInput.value.trim() !== '') { 10331 urlInput.classList.add('is-invalid'); 10332 } 10333 } 10334 } 10335} 10336 10337function handleScanUrlKeypress(event) { 10338 if (event.key === 'Enter') { 10339 event.preventDefault(); 10340 const scanBtn = document.getElementById('modalScanBtn'); 10341 if (scanBtn && !scanBtn.disabled) { 10342 executeScan(); 10343 } 10344 } 10345} 10346 10347async function executeScan() { 10348 const urlInput = document.getElementById('modalScanUrl'); 10349 const scanBtn = document.getElementById('modalScanBtn'); 10350 10351 if (!urlInput) { 10352 console.error('URL input not found'); 10353 displayAlertCookie('Error: Form not found. Please try again.', 'error'); 10354 return; 10355 } 10356 10357 if (!scanBtn) { 10358 console.error('Scan button not found'); 10359 return; 10360 } 10361 10362 const url = urlInput.value.trim(); 10363 const domainId = window.currentScanDomainId; 10364 10365 if (!url) { 10366 urlInput.classList.add('is-invalid'); 10367 urlInput.focus(); 10368 return; 10369 } 10370 10371 // Validate URL format 10372 try { 10373 new URL(url); 10374 } catch (e) { 10375 urlInput.classList.add('is-invalid'); 10376 urlInput.focus(); 10377 displayAlertCookie('Please enter a valid URL', 'error'); 10378 return; 10379 } 10380 10381 if (!domainId) { 10382 console.error('Domain ID not found'); 10383 displayAlertCookie('Error: Domain not selected', 'error'); 10384 return; 10385 } 10386 10387 try { 10388 // Show loading state 10389 scanBtn.disabled = true; 10390 scanBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting Scan...'; 10391 10392 const data = { 10393 sid: getSid(), 10394 domainId: parseInt(domainId), 10395 url: url 10396 }; 10397 10398 console.log('Starting scan with data:', data); 10399 10400 const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data); 10401 10402 if (response.ok) { 10403 const responseBody = await response.json(); 10404 10405 if (responseBody.statusCode === 0) { 10406 // Close modal 10407 if (window.currentScanModal) { 10408 window.currentScanModal.hide(); 10409 } 10410 10411 // Show success message 10412 //displayAlertCookie('Scan started successfully! Use "View Status" to check progress.', 'success'); 10413 10414 } else { 10415 //displayAlertCookie('Failed to start scan: ' + (responseBody.statusMessage || 'Unknown error'), 'error'); 10416 } 10417 } else { 10418 //displayAlertCookie('Failed to start scan: ' + response.statusText, 'error'); 10419 } 10420 } catch (error) { 10421 console.error('Error starting scan:', error); 10422 displayAlertCookie('Error starting scan: ' + error.message, 'error'); 10423 } finally { 10424 // Reset button state 10425 if (scanBtn) { 10426 scanBtn.disabled = false;
10427 scanBtn.innerHTML = '<i class="fas fa-play me-2"></i>' + (translations['startScan'] || 'Start Scan'); 10428 } 10429 } 10430} 10431 10432function startNewScan(domainId) { 10433 showScanUrlModal(domainId); 10434} 10435 10436async function viewScanStatus(domainId) { 10437 const viewStatusBtn = document.getElementById('viewStatusBtn'); 10438 10439 try { 10440 // Show loading state 10441 viewStatusBtn.disabled = true; 10442 viewStatusBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Loading...'; 10443 10444 const data = { 10445 sid: getSid(), 10446 domainId: parseInt(domainId) 10447 }; 10448 10449 console.log('Getting scan status with data:', data); 10450 10451 const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data); 10452 10453 if (response.ok) { 10454 const responseBody = await response.json(); 10455 10456 if (responseBody.statusCode === 0) { 10457 const results = responseBody.results || []; 10458 showScanStatusModal(results); 10459 } else { 10460 displayAlertCookie('Failed to get scan status: ' + (responseBody.statusMessage || 'Unknown error'), 'error'); 10461 } 10462 } else { 10463 displayAlertCookie('Failed to get scan status: ' + response.statusText, 'error'); 10464 } 10465 } catch (error) { 10466 console.error('Error getting scan status:', error); 10467 displayAlertCookie('Error getting scan status: ' + error.message, 'error'); 10468 } finally { 10469 // Reset button state 10470 viewStatusBtn.disabled = false; 10471 viewStatusBtn.innerHTML = '<i class="fas fa-list me-2"></i>' + (translations['viewStatus'] || 'View Status'); 10472 } 10473} 10474 10475function generateScanStatusContent(results) { 10476 if (!results || results.length === 0) { 10477 return ` 10478 <div class="text-center py-5"> 10479 <i class="fas fa-search fa-3x text-muted mb-3"></i> 10480 <h5 class="text-muted">${translations['noScansFound'] || 'No scans found for this domain'}</h5> 10481 <p class="text-muted">${translations['startFirstScan'] || 'Start your first scan by entering a URL above'}</p> 10482 </div> 10483 `; 10484 } 10485 10486 // Create table for scan results 10487 return ` 10488 <div class="mb-3"> 10489 <div class="d-flex justify-content-between align-items-center"> 10490 <h6 class="mb-0">${translations['scanHistory'] || 'Scan History'}</h6> 10491 <span class="badge bg-primary">${results.length} scan${results.length !== 1 ? 's' : ''}</span> 10492 </div> 10493 </div> 10494 <div class="table-responsive"> 10495 <table class="table table-hover table-sm"> 10496 <thead class="table-light"> 10497 <tr> 10498 <th><i class="fas fa-image me-1"></i>${translations['preview'] || 'Preview'}</th> 10499 <th><i class="fas fa-link me-1"></i>${translations['url'] || 'URL'}</th> 10500 <th><i class="fas fa-flag me-1"></i>${translations['status'] || 'Status'}</th> 10501 <th><i class="fas fa-calendar me-1"></i>${translations['created'] || 'Created'}</th> 10502 <th><i class="fas fa-check me-1"></i>${translations['completed'] || 'Completed'}</th> 10503 <th><i class="fas fa-cogs me-1"></i>${translations['actions'] || 'Actions'}</th> 10504 </tr> 10505 </thead> 10506 <tbody> 10507 ${results.map(result => createScanResultRow(result)).join('')} 10508 </tbody> 10509 </table> 10510 </div> 10511 `; 10512} 10513 10514function createScanResultRow(result) { 10515 const statusBadge = getScanStatusBadge(result.status); 10516 10517 // Handle empty URL case - don't show error for empty URLs in scan history 10518 const displayUrl = result.url && result.url.trim() !== '' ? result.url : 'Auto Scan'; 10519 const truncatedUrl = displayUrl !== 'Auto Scan' && displayUrl.length > 50 ? 10520 displayUrl.substring(0, 50) + '...' : displayUrl; 10521 10522 // Create preview image cell 10523 const previewCell = createPreviewImageCell(result.previewImageUri, result.url); 10524 10525 return ` 10526 <tr> 10527 <td>${previewCell}</td> 10528 <td> 10529 ${displayUrl !== 'Auto Scan' ? 10530 `<a href="${result.url}" target="_blank" class="text-decoration-none" title="${result.url}"> 10531 ${truncatedUrl} 10532 <i class="fas fa-external-link-alt fa-xs ms-1"></i> 10533 </a>` : 10534 `<span class="text-muted fst-italic">${truncatedUrl}</span>` 10535 } 10536 </td> 10537 <td>${statusBadge}</td> 10538 <td> 10539 <small class="text-muted">${result.createdDate || 'N/A'}</small> 10540 </td> 10541 <td> 10542 <small class="text-muted">${result.completedDate || 'Pending'}</small> 10543 </td> 10544 <td> 10545 <div class="btn-group btn-group-sm"> 10546 ${result.previewImageUri && result.previewImageUri.trim() !== '' ? 10547 `<button class="btn btn-outline-primary btn-sm" onclick="showPreviewImageModal('${result.previewImageUri}','${displayUrl}')" title="View Preview"> 10548 <i class="fas fa-eye"></i> 10549 </button>` : '' 10550 } 10551 ${result.status === 1 || result.status === 2 ? 10552 `<button class="btn btn-outline-info btn-sm" onclick="refreshScanStatusModal()" title="Refresh Status"> 10553 <i class="fas fa-refresh"></i> 10554 </button>` : '' 10555 } 10556 </div> 10557 </td> 10558 </tr> 10559 `; 10560} 10561 10562function getFullImageUrl(imageUri) { 10563 if (!imageUri) return ''; 10564 // Make sure we don't append to URLs that are already absolute 10565 if (imageUri.startsWith('http')) return imageUri; 10566 return imageUri; 10567} 10568 10569function createPreviewImageCell(previewImageUri, url) { 10570 if (!previewImageUri || previewImageUri.trim() === '') { 10571 return '<span class="text-muted small">No preview</span>'; 10572 } 10573 10574 const fullImageUrl = getFullImageUrl(previewImageUri); 10575 return `<img src="${fullImageUrl}
10575" alt="Page Preview" class="img-thumbnail preview-thumbnail" 10576 onclick="showPreviewImageModal('${previewImageUri}','${url}')" 10577 style="max-height: 40px; cursor: pointer;" />`; 10578} 10579 10580function openImageInNewTab(imageUri) { 10581 if (imageUri && imageUri.trim() !== '') { 10582 window.open(imageUri, '_blank'); 10583 } 10584} 10585 10586function downloadPreviewImage(imageUri, url) { 10587 if (!imageUri || imageUri.trim() === '') { 10588 displayAlertCookie('No image to download', 'error'); 10589 return; 10590 } 10591 10592 try { 10593 // Create a temporary anchor element to trigger download 10594 const link = document.createElement('a'); 10595 link.href = imageUri; 10596 10597 // Generate filename from URL and timestamp 10598 const urlObj = new URL(url); 10599 const domain = urlObj.hostname.replace(/[^a-z0-9]/gi, '_'); 10600 const timestamp = new Date().toISOString().split('T')[0]; 10601 link.download = `preview_${domain}_${timestamp}.png`; 10602 10603 // Trigger download 10604 document.body.appendChild(link); 10605 link.click(); 10606 document.body.removeChild(link); 10607 10608 displayAlertCookie('Download started', 'success'); 10609 } catch (error) { 10610 console.error('Download error:', error); 10611 displayAlertCookie('Could not download image. You can right-click and save the image manually.', 'info'); 10612 } 10613} 10614 10615function refreshScanStatusModal() { 10616 const domainDropdown = document.getElementById('select_domain'); 10617 if (domainDropdown && domainDropdown.value) { 10618 // Close current modal 10619 const currentModal = document.querySelector('.modal.show'); 10620 if (currentModal) { 10621 bootstrap.Modal.getInstance(currentModal).hide(); 10622 } 10623 10624 // Reopen with fresh data 10625 setTimeout(() => { 10626 viewScanStatus(domainDropdown.value); 10627 }, 300); 10628 } 10629} 10630 10631function getScanStatusBadge(status) { 10632 switch (status) { 10633 case 1: 10634 return '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Waiting</span>'; 10635 case 2: 10636 return '<span class="badge bg-info"><i class="fas fa-spinner fa-spin me-1"></i>Processing</span>'; 10637 case 3: 10638 return '<span class="badge bg-success"><i class="fas fa-check me-1"></i>Finished</span>'; 10639 default: 10640 return '<span class="badge bg-secondary">Unknown</span>'; 10641 } 10642} 10643 10644function importScanResults(url) { 10645 displayAlertCookie('Scan results import functionality will be implemented soon', 'info'); 10646 // TODO: Implement scan results import 10647 // This would typically fetch the detected cookies from the scan and add them to the cookie list 10648} 10649 10650let expandedCookiesList = {}; 10651 10652async function handleViewStatus() { 10653 console.log('handleViewStatus called - redirecting to showScanStatusModal'); 10654 await showScanStatusModal(); 10655} 10656 10657function createScanStatusModal() { 10658 const modal = document.createElement('div'); 10659 modal.classList.add('modal', 'fade'); 10660 modal.tabIndex = -1; 10661 10662 modal.innerHTML = ` 10663 <div class="modal-dialog modal-lg"> 10664 <div class="modal-content"> 10665 <div class="modal-header"> 10666 <h5 class="modal-title"> 10667 <i class="fas fa-list-ul me-2"></i> 10668 ${translations['scanStatus'] || 'Scan Status'} 10669 </h5> 10670 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 10671 </div> 10672 <div class="modal-body"> 10673 <div id="scanStatusContent"> 10674 <div class="text-center py-5"> 10675 <div class="spinner-border text-primary" role="status"> 10676 <span class="visually-hidden">Loading...</span> 10677 </div> 10678 <p class="mt-3 text-muted">${translations['loadingStatus'] || 'Loading scan status...'}</p> 10679 </div> 10680 </div> 10681 </div> 10682 <div class="modal-footer"> 10683 <button type="button" class="btn btn-secondary" onclick="refreshScanStatus()" id="refreshStatusBtn"> 10684 <i class="fas fa-refresh me-2"></i>${translations['refresh'] || 'Refresh'} 10685 </button> 10686 <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"> 10687 ${translations['close'] || 'Close'} 10688 </button> 10689 </div> 10690 </div> 10691 </div> 10692 `; 10693 10694 return modal; 10695} 10696 10697function toggleCookieList(index) { 10698 const isExpanded = expandedCookiesList[index] || false; 10699 expandedCookiesList[index] = !isExpanded; 10700 10701 // Update the UI 10702 const cookieToggle = document.getElementById(`cookieToggle_${index}`); 10703 const cookieList = document.getElementById(`cookieList_${index}`); 10704 10705 if (cookieToggle && cookieList) { 10706 cookieToggle.innerHTML = `
10707 <span class="badge bg-success me-2" style="cursor: pointer;"> 10708 ${translations['cookiesFound'] || 'Cookies Found'} ${!isExpanded ? 'â¼' : 'â¶'} 10709 </span> 10710 `; 10711 cookieList.style.display = !isExpanded ? 'block' : 'none'; 10712 } 10713} 10714 10715function renderCookieInfo(result, index) { 10716 let cookieInfoHtml = ''; 10717 10718 // Only show cookie info if scan is completed (status 3) 10719 if (result.status === 3 && (result.cookiesFound || result.numCookies !== undefined)) { 10720 cookieInfoHtml = ` 10721 <div class="mb-2"> 10722 <div class="d-flex gap-2 align-items-center flex-wrap"> 10723 `; 10724 10725 // Show expandable cookie names if available 10726 if (result.cookiesFound) { 10727 const isExpanded = expandedCookiesList[index] || false; 10728 cookieInfoHtml += ` 10729 <div> 10730 <div id="cookieToggle_${index}" onclick="toggleCookieList('${index}')" style="cursor: pointer;"> 10731 <span class="badge bg-success" style="cursor: pointer;"> 10732 ${translations['cookiesFound'] || 'Cookies Found'} ${isExpanded ? 'â¼' : 'â¶'} 10733 </span> 10734 </div> 10735 <div id="cookieList_${index}" class="mt-2" style="display: ${isExpanded ? 'block' : 'none'};"> 10736 <div class="p-2 bg-light rounded border"> 10737 <small class="text-muted fw-bold">${translations['cookieNames'] || 'Cookie Names'}:</small> 10738 <div class="mt-1 d-flex flex-wrap gap-1"> 10739 ${renderCookieNames(result.cookiesFound)} 10740 </div> 10741 </div> 10742 </div> 10743 </div> 10744 `; 10745 } 10746 10747 // Show cookie count if available 10748 if (result.numCookies !== undefined) { 10749 cookieInfoHtml += ` 10750 <span class="badge bg-info"> 10751 ${result.numCookies} ${translations['cookie'] || 'cookie'}${result.numCookies !== 1 ? 's' : ''} 10752 </span> 10753 `; 10754 } 10755 10756 cookieInfoHtml += ` 10757 </div> 10758 </div> 10759 `; 10760 } 10761 10762 return cookieInfoHtml; 10763} 10764 10765function renderCookieNames(cookiesFound) {
10766 let cookieNamesHtml = ''; 10767 10768 if (Array.isArray(cookiesFound)) { 10769 // Handle array format 10770 cookiesFound.forEach((cookieName, cookieIndex) => { 10771 cookieNamesHtml += ` 10772 <span class="badge bg-info bg-opacity-75 text-dark"> 10773 ${String(cookieName)} 10774 </span> 10775 `; 10776 }); 10777 } else if (typeof cookiesFound === 'string') { 10778 // Handle string format (comma-separated) 10779 const cookieNames = cookiesFound.split(','); 10780 cookieNames.forEach((cookieName, cookieIndex) => { 10781 const trimmedName = cookieName.trim(); 10782 if (trimmedName) { 10783 cookieNamesHtml += ` 10784 <span class="badge bg-info bg-opacity-75 text-dark"> 10785 ${trimmedName} 10786 </span> 10787 `; 10788 } 10789 }); 10790 } else { 10791 cookieNamesHtml = '<small class="text-muted">No cookie names available</small>'; 10792 } 10793 10794 return cookieNamesHtml; 10795} 10796 10797function getStatusBadge(status) { 10798 switch (status) { 10799 case 1: 10800 return ` 10801 <span class="badge bg-warning d-flex align-items-center gap-1"> 10802 <i class="fas fa-hourglass-half"></i> 10803 Waiting 10804 </span> 10805 `; 10806 case 2: 10807 return ` 10808 <span class="badge bg-info d-flex align-items-center gap-1"> 10809 <div class="spinner-border spinner-border-sm" role="status"> 10810 <span class="visually-hidden">Loading...</span> 10811 </div> 10812 Processing 10813 </span> 10814 `; 10815 case 3: 10816 return ` 10817 <span class="badge bg-success d-flex align-items-center gap-1"> 10818 <i class="fas fa-check-circle"></i> 10819 Finished 10820 </span> 10821 `; 10822 default: 10823 return `<span class="badge bg-secondary">Unknown</span>`; 10824 } 10825} 10826 10827function getPreviewImageHtml(previewImageUri, url) { 10828 10829 if (previewImageUri) { 10830 return ` 10831 <div class="position-relative" style="width: 80px; height: 60px; cursor: pointer;" 10832 onclick="showPreviewImageModal('${previewImageUri}', '${url}')"> 10833 <img src="${previewImageUri}" 10834 alt="Page Preview" 10835 class="w-100 h-100 rounded border" 10836 style="object-fit: cover;" 10837 onerror="this.parentElement.innerHTML='<div class=\\'d-flex align-items-center justify-content-center w-100 h-100 bg-light rounded border\\' style=\\'color: #6c757d; font-size: 24px;\\'>ð¼ï¸</div>';"> 10838 <div class="position-absolute top-0 end-0 m-1 rounded-circle d-flex align-items-center justify-content-center" 10839 style="background: rgba(0,0,0,0.7); width: 16px; height: 16px;"> 10840 <span style="color: white; font-size: 10px;">ð</span> 10841 </div> 10842 </div> 10843 `; 10844 } else { 10845 return ` 10846 <div class="d-flex align-items-center justify-content-center bg-light rounded border" 10847 style="width: 80px; height: 60px; color: #6c757d; font-size: 24px;"> 10848 ð¼ï¸ 10849 </div> 10850 `; 10851 } 10852} 10853 10854function showPreviewImageModal(imageUri, url) { 10855 const modal = document.createElement('div'); 10856 modal.classList.add('modal', 'fade'); 10857 modal.tabIndex = -1; 10858 10859 modal.innerHTML = ` 10860 <div class="modal-dialog modal-lg"> 10861 <div class="modal-content"> 10862 <div class="modal-header"> 10863 <h5 class="modal-title"> 10864 <i class="fas fa-image me-2"></i> 10865 ${translations['pagePreview'] || 'Page Preview'} 10866 </h5> 10867 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 10868 </div> 10869 <div class="modal-body text-center"> 10870 ${url ? ` 10871 <div class="alert alert-info"> 10872 <strong>${translations['scannedUrl'] || 'Scanned URL'}:</strong> 10873 <a href="${url}" target="_blank" class="ms-2">${url}</a> 10874 </div> 10875 ` : ''} 10876 <img src="${imageUri}" 10877 alt="Page Preview" 10878 class="img-fluid rounded shadow" 10879 style="height:60vh;width:100vw;object-fit:contain;" 10880 onerror="this.parentElement.innerHTML='<div class=\\'alert alert-warning\\'>Preview image could not be loaded</div>';"> 10881 </div> 10882 <div class="modal-footer"> 10883 <button type="button" class="btn btn-primary" onclick="window.open('${imageUri}', '_blank')"> 10884 <i class="fas fa-external-link-alt me-2"></i> 10885 ${translations['openInNewTab'] || 'Open in New Tab'} 10886 </button> 10887 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> 10888 ${translations['close'] || 'Close'} 10889 </button> 10890 </div> 10891 </div> 10892 </div> 10893 `; 10894 10895 document.body.appendChild(modal); 10896 const bsModal = new bootstrap.Modal(modal); 10897 bsModal.show(); 10898 10899 modal.addEventListener('hidden.bs.modal', () => { 10900 document.body.removeChild(modal); 10901 }); 10902} 10903 10904async function refreshScanStatus() { 10905 const statusContent = document.getElementById('scanStatusContent'); 10906 const refreshBtn = document.getElementById('refreshStatusBtn'); 10907 10908 if (statusContent && refreshBtn) { 10909 // Show loading state 10910 statusContent.innerHTML = ` 10911 <div class="text-center py-5"> 10912 <div class="spinner-border text-primary" role="status">
10913 <span class="visually-hidden">Loading...</span> 10914 </div> 10915 <p class="mt-3 text-muted">${translations['loadingStatus'] || 'Loading scan status...'}</p> 10916 </div> 10917 `; 10918 10919 refreshBtn.disabled = true; 10920 10921 try { 10922 // Get domain ID 10923 const domainDropdown = document.getElementById('select_domain'); 10924 const domainId = domainDropdown ? domainDropdown.value : null; 10925 10926 if (!domainId) { 10927 throw new Error('No domain selected'); 10928 } 10929 10930 // Fetch updated results 10931 const data = { 10932 sid: getSid(), 10933 domainId: parseInt(domainId) 10934 }; 10935 10936 console.log('Refreshing scan status with data:', data); 10937 const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data); 10938 10939 if (response.ok) { 10940 const responseBody = await response.json(); 10941 console.log('Refresh scan status response:', responseBody); 10942 10943 if (responseBody.statusCode === 0) { 10944 const results = responseBody.results || []; 10945 statusContent.innerHTML = renderScanStatusResults(results); 10946 } else { 10947 throw new Error(responseBody.statusMessage || 'Unknown error'); 10948 } 10949 } else { 10950 const errorText = await response.text(); 10951 console.error('HTTP Error Response:', errorText); 10952 throw new Error('Failed to refresh scan status: ' + response.status + ' - ' + response.statusText); 10953 } 10954 } catch (error) { 10955 console.error('Error refreshing scan status:', error); 10956 statusContent.innerHTML = ` 10957 <div class="text-center py-5"> 10958 <div class="alert alert-danger"> 10959 <i class="fas fa-exclamation-triangle me-2"></i> 10960 Error refreshing scan status: ${error.message} 10961 </div> 10962 <button class="btn btn-outline-primary mt-2" onclick="refreshScanStatus()"> 10963 <i class="fas fa-retry me-2"></i>Try Again 10964 </button> 10965 </div> 10966 `; 10967 } finally { 10968 refreshBtn.disabled = false; 10969 } 10970 } 10971} 10972 10973async function showScanStatusModal() { 10974 // Create and show modal 10975 const modal = createScanStatusModal(); 10976 document.body.appendChild(modal); 10977 const bsModal = new bootstrap.Modal(modal); 10978 bsModal.show(); 10979 10980 // Clean up when modal is hidden 10981 modal.addEventListener('hidden.bs.modal', () => { 10982 document.body.removeChild(modal); 10983 expandedCookiesList = {}; // Reset expanded state when modal closes 10984 }); 10985 10986 try { 10987 // Fetch scan status results 10988 const domainDropdown = document.getElementById('select_domain'); 10989 const domainId = domainDropdown ? domainDropdown.value : null; 10990 10991 if (!domainId) { 10992 throw new Error('No domain selected'); 10993 } 10994 10995 // Use the correct endpoint and parameters based on the REST provider 10996 const data = { 10997 sid: getSid(), 10998 domainId: parseInt(domainId) 10999 // Note: url parameter is not included, so it will return scan results list 11000 }; 11001 11002 console.log('Fetching scan status with data:', data); 11003 const response = await postRequest(endpoint + '/cmp/page-cookies-scanner', data); 11004 11005 if (response.ok) { 11006 const responseBody = await response.json(); 11007 console.log('Scan status response:', responseBody); 11008 11009 if (responseBody.statusCode === 0) { 11010 const results = responseBody.results || []; 11011 11012 // Update modal content with results 11013 const statusContent = document.getElementById('scanStatusContent'); 11014 const refreshBtn = document.getElementById('refreshStatusBtn'); 11015 11016 if (statusContent && refreshBtn) { 11017 statusContent.innerHTML = renderScanStatusResults(results); 11018 refreshBtn.disabled = false;
11019 } 11020 } else { 11021 throw new Error(responseBody.statusMessage || 'Unknown error'); 11022 } 11023 } else { 11024 const errorText = await response.text(); 11025 console.error('HTTP Error Response:', errorText); 11026 throw new Error('Failed to fetch scan status: ' + response.status + ' - ' + response.statusText); 11027 } 11028 } catch (error) { 11029 console.error('Error loading scan status:', error); 11030 11031 const statusContent = document.getElementById('scanStatusContent'); 11032 const refreshBtn = document.getElementById('refreshStatusBtn'); 11033 11034 if (statusContent && refreshBtn) { 11035 statusContent.innerHTML = ` 11036 <div class="text-center py-5"> 11037 <div class="alert alert-danger"> 11038 <i class="fas fa-exclamation-triangle me-2"></i> 11039 Error loading scan status: ${error.message} 11040 </div> 11041 <button class="btn btn-outline-primary mt-2" onclick="refreshScanStatus()"> 11042 <i class="fas fa-retry me-2"></i>Try Again 11043 </button> 11044 </div> 11045 `; 11046 refreshBtn.disabled = false; 11047 } 11048 } 11049} 11050
11051function renderScanStatusResults(results) { 11052 if (!results || results.length === 0) { 11053 return ` 11054 <div class="text-center py-5"> 11055 <div class="mb-3"> 11056 <i class="fas fa-search fa-3x text-muted"></i> 11057 </div> 11058 <h5 class="mb-3">${translations['noScansFound'] || 'No scans found for this domain'}</h5> 11059 <p class="text-muted">${translations['startFirstScan'] || 'Start your first scan by clicking "Start New Scan" button.'}</p> 11060 </div> 11061 `; 11062 } 11063 11064 let resultsHtml = ` 11065 <div class="mb-3"> 11066 <div class="d-flex justify-content-between align-items-center"> 11067 <h6 class="mb-0">${translations['scanHistory'] || 'Scan History'}</h6> 11068 <span class="badge bg-info">${results.length} scan${results.length !== 1 ? 's' : ''}</span> 11069 </div> 11070 </div> 11071 <div class="border rounded overflow-hidden" style="background-color: #f6f6f7;"> 11072 `; 11073 11074 results.forEach((result, index) => { 11075 const statusBadge = getStatusBadge(result.status); 11076 const previewImageHtml = getPreviewImageHtml(result.previewImageUri, result.url); 11077 const cookieInfoHtml = renderCookieInfo(result, index); 11078 11079 // Better URL handling 11080 const urlDisplay = getUrlDisplay(result.url); 11081 11082 resultsHtml += ` 11083 <div class="p-3 bg-white ${index < results.length - 1 ? 'border-bottom' : ''}" style="border-color: #e1e5e9;"> 11084 <div class="d-flex gap-3 align-items-start"> 11085 <!-- Preview Image --> 11086 <div class="flex-shrink-0"> 11087 ${previewImageHtml} 11088 </div> 11089 11090 <!-- Scan Details --> 11091 <div class="flex-fill min-width-0"> 11092 <div class="d-flex justify-content-between align-items-start mb-2"> 11093 <div class="flex-fill min-width-0 me-3"> 11094 ${urlDisplay.titleHtml} 11095 ${urlDisplay.linkHtml} 11096 </div> 11097 <div class="flex-shrink-0"> 11098 ${statusBadge} 11099 </div> 11100 </div> 11101 11102 <!-- Cookie Information --> 11103 ${cookieInfoHtml} 11104 11105 <!-- Timestamps --> 11106 <div class="d-flex gap-3 small text-muted"> 11107 <span> 11108 <i class="fas fa-clock me-1"></i> 11109 Created: ${result.createdDate || 'N/A'} 11110 </span> 11111 ${result.completedDate ? ` 11112 <span> 11113 <i class="fas fa-check-circle me-1"></i> 11114 Completed: ${result.completedDate} 11115 </span> 11116 ` : ''} 11117 </div> 11118 </div> 11119 </div> 11120 </div> 11121 `; 11122 }); 11123 11124 resultsHtml += '</div>'; 11125 return resultsHtml; 11126} 11127 11128function getUrlDisplay(url) { 11129 if (!url || url.trim() === '') { 11130 return { 11131 titleHtml: '<div class="fw-bold">Auto Scan</div>', 11132 linkHtml: '' 11133 }; 11134 } 11135 11136 // Clean and validate the URL 11137 const cleanUrl = url.trim(); 11138 let isValidUrl = false; 11139 let displayUrl = cleanUrl; 11140 11141 try { 11142 // Validate URL 11143 new URL(cleanUrl); 11144 isValidUrl = true; 11145 } catch (e) { 11146 console.warn('Invalid URL detected:', cleanUrl); 11147 isValidUrl = false; 11148 } 11149 11150 // Create truncated version for display 11151 const maxLength = 60; 11152 let truncatedUrl = displayUrl; 11153 if (displayUrl.length > maxLength) { 11154 truncatedUrl = displayUrl.substring(0, maxLength) + '...'; 11155 } 11156 11157 // Generate unique ID for tooltip 11158 const tooltipId = `url-tooltip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; 11159 11160 const titleHtml = ` 11161 <div class="fw-bold position-relative">
11162 <span 11163 id="${tooltipId}" 11164 class="text-truncate d-inline-block" 11165 style="max-width: 100%; cursor: help;" 11166 title="${displayUrl}" 11167 data-bs-toggle="tooltip" 11168 data-bs-placement="top" 11169 > 11170 ${truncatedUrl} 11171 </span> 11172 </div> 11173 `; 11174 11175 const linkHtml = isValidUrl ? ` 11176 <div class="mt-1"> 11177 <a href="${cleanUrl}" 11178 target="_blank" 11179 rel="noopener noreferrer" 11180 class="text-decoration-none small text-muted" 11181 onclick="handleUrlClick(event, '${cleanUrl.replace(/'/g, "\\'")}')" 11182 > 11183 <i class="fas fa-external-link-alt me-1"></i>View page 11184 </a> 11185 <button 11186 class="btn btn-link btn-sm p-0 ms-2 text-muted" 11187 style="font-size: 0.75rem; text-decoration: none;" 11188 onclick="copyUrlToClipboard('${cleanUrl.replace(/'/g, "\\'")}')" 11189 title="Copy URL to clipboard" 11190 > 11191 <i class="fas fa-copy"></i> 11192 </button> 11193 </div> 11194 ` : ` 11195 <div class="mt-1"> 11196 <span class="small text-warning"> 11197 <i class="fas fa-exclamation-triangle me-1"></i>Invalid URL format 11198 </span> 11199 </div> 11200 `; 11201 11202 // Initialize tooltip after a short delay to ensure element exists 11203 setTimeout(() => { 11204 const tooltipElement = document.getElementById(tooltipId); 11205 if (tooltipElement && window.bootstrap) { 11206 new bootstrap.Tooltip(tooltipElement); 11207 } 11208 }, 100); 11209 11210 return { titleHtml, linkHtml }; 11211} 11212 11213function handleUrlClick(event, url) { 11214 try { 11215 // Validate URL before opening 11216 new URL(url); 11217 // Let the default behavior proceed (opening in new tab) 11218 return true; 11219 } catch (e) { 11220 event.preventDefault(); 11221 console.error('Invalid URL:', url); 11222 displayAlertCookie('Invalid URL format. Cannot open page.', 'error'); 11223 return false; 11224 } 11225} 11226 11227async function copyUrlToClipboard(url) { 11228 try { 11229 await navigator.clipboard.writeText(url); 11230 displayAlertCookie('URL copied to clipboard!', 'success'); 11231 } catch (err) { 11232 // Fallback for older browsers 11233 const textArea = document.createElement('textarea'); 11234 textArea.value = url; 11235 textArea.style.position = 'fixed'; 11236 textArea.style.opacity = '0'; 11237 document.body.appendChild(textArea); 11238 textArea.select(); 11239 11240 try { 11241 document.execCommand('copy'); 11242 displayAlertCookie('URL copied to clipboard!', 'success'); 11243 } catch (e) { 11244 displayAlertCookie('Failed to copy URL. Please copy manually.', 'error'); 11245 } 11246 11247 document.body.removeChild(textArea); 11248 } 11249} 11250 11251function isValidUrl(string) { 11252 try { 11253 const url = new URL(string); 11254 return url.protocol === 'http:' || url.protocol === 'https:'; 11255 } catch (_) { 11256 return false; 11257 } 11258} 11259 11260function showFullUrlModal(url, title = 'Full URL') { 11261 const modal = document.createElement('div'); 11262 modal.classList.add('modal', 'fade'); 11263 modal.tabIndex = -1; 11264 11265 modal.innerHTML = ` 11266 <div class="modal-dialog"> 11267 <div class="modal-content"> 11268 <div class="modal-header"> 11269 <h5 class="modal-title"> 11270 <i class="fas fa-link me-2"></i>${title} 11271 </h5> 11272 <button type="button" class="btn-close" data-bs-dismiss="modal"></button> 11273 </div> 11274 <div class="modal-body"> 11275 <div class="mb-3"> 11276 <label class="form-label small text-muted">Complete URL:</label> 11277 <div class="border rounded p-2 bg-light" style="word-break: break-all; font-family: monospace; font-size: 0.9em;"> 11278 ${url} 11279 </div> 11280 </div> 11281 <div class="d-flex gap-2"> 11282 <button type="button" class="btn btn-primary btn-sm" onclick="copyUrlToCl
11282ipboard('${url.replace(/'/g, "\\'")}')"> 11283 <i class="fas fa-copy me-1"></i>Copy URL 11284 </button> 11285 ${isValidUrl(url) ? ` 11286 <a href="${url}" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm"> 11287 <i class="fas fa-external-link-alt me-1"></i>Open Page 11288 </a> 11289 ` : ''} 11290 </div> 11291 </div> 11292 <div class="modal-footer"> 11293 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> 11294 </div> 11295 </div> 11296 </div> 11297 `; 11298 11299 document.body.appendChild(modal); 11300 const bsModal = new bootstrap.Modal(modal); 11301 bsModal.show(); 11302 11303 modal.addEventListener('hidden.bs.modal', () => { 11304 document.body.removeChild(modal); 11305 }); 11306} 11307 11308function addCookieDeclarationButton(buttonsContainer) { 11309 const declarationButton = document.createElement('button'); 11310 declarationButton.classList.add('btn', 'btn-info'); 11311 declarationButton.innerHTML = '<i class="fas fa-code me-2"></i>' + (translations['cookieDeclarationScript'] || 'Cookie Declaration Script'); 11312 declarationButton.addEventListener('click', () => showCookieDeclarationModal()); 11313 buttonsContainer.appendChild(declarationButton); 11314} 11315 11316document.addEventListener('DOMContentLoaded', function() { 11317 // Add event delegation for copy button 11318 document.addEventListener('click', async function(e) { 11319 if (e.target && e.target.id === 'copyScriptBtn') { 11320 const textarea = document.getElementById('scriptCodeArea'); 11321 const copyBtn = e.target; 11322 11323 try { 11324 await navigator.clipboard.writeText(textarea.value); 11325 11326 // Show success feedback 11327 const originalText = copyBtn.innerHTML; 11328 copyBtn.innerHTML = `<i class="fas fa-check me-2"></i>${translations['copied'] || 'Copied!'}`; 11329 copyBtn.classList.remove('btn-primary'); 11330 copyBtn.classList.add('btn-success'); 11331 11332 setTimeout(() => { 11333 copyBtn.innerHTML = originalText; 11334 copyBtn.classList.remove('btn-success'); 11335 copyBtn.classList.add('btn-primary'); 11336 }, 2000); 11337 11338 } catch (err) { 11339 // Fallback 11340 textarea.select(); 11341 document.execCommand('copy'); 11342 11343 const originalText = copyBtn.innerHTML; 11344 copyBtn.innerHTML = `<i class="fas fa-check me-2"></i>${translations['copied'] || 'Copied!'}`; 11345 copyBtn.classList.remove('btn-primary'); 11346 copyBtn.classList.add('btn-success'); 11347 11348 setTimeout(() => { 11349 copyBtn.innerHTML = originalText; 11350 copyBtn.classList.remove('btn-success'); 11351 copyBtn.classList.add('btn-primary'); 11352 }, 2000); 11353 } 11354 } 11355 }); 11356 11357 // Handle tab switching to preview 11358 document.addEventListener('shown.bs.tab', function (e) { 11359 if (e.target.id === 'preview-tab') { 11360 loadPreview(); 11361 } 11362 }); 11363}); 11364 11365function findProjectIdByDomainId(domainId, projects) { 11366 if (!domainId || !projects || !Array.isArray(projects)) { 11367 return null; 11368 } 11369 11370 // Convert domainId to number for comparison 11371 const targetDomainId = parseInt(domainId); 11372 11373 // Search through all projects 11374 for (const project of projects) { 11375 if (project.domains && Array.isArray(project.domains)) { 11376 // Check if this project contains the domain 11377 const foundDomain = project.domains.find(domain => 11378 parseInt(domain.id) === targetDomainId 11379 ); 11380 11381 if (foundDomain) { 11382 return project.id; 11383 } 11384 } 11385 } 11386 11387 return null; // Domain not found in any project 11388} 11389 11390function debugProjectDomainMapping() { 11391 getProjects().then(projects => { 11392 console.log('=== Project-Domain Mapping ===');
11393 projects.forEach(project => { 11394 console.log(`Project: ${project.projectName} (ID: ${project.id})`); 11395 if (project.domains) { 11396 project.domains.forEach(domain => { 11397 console.log(` - Domain: ${domain.domainName} (ID: ${domain.id})`); 11398 }); 11399 } 11400 }); 11401 console.log('==============================='); 11402 }); 11403} 11404 11405async function loadPreviewFromScript(scriptCode, projectId) { 11406 const previewContainer = document.getElementById('previewContainer'); 11407 if (!previewContainer) return; 11408 11409 try { 11410 // Extract script URL from the script tag (e.g., <script src="..."></script>) 11411 const scriptUrlMatch = scriptCode.match(/src=["']([^"']+)["']/); 11412 if (!scriptUrlMatch) { 11413 throw new Error(translations['couldNotExtractURL'] || 'Could not extract script URL'); 11414 } 11415 11416 const scriptUrl = scriptUrlMatch[1]; 11417 11418 // Clear the preview container and prepare it for script execution 11419 previewContainer.innerHTML = '<div id="cookie-declaration"></div>'; 11420 11421 // Create and load the script dynamically 11422 const script = document.createElement('script'); 11423 script.src = scriptUrl; 11424 script.async = true; 11425 11426 // Handle script load success 11427 script.onload = function() { 11428 console.log('Cookie declaration script loaded successfully'); 11429 // The script should automatically render in the cookie-declaration div 11430 }; 11431 11432 // Handle script load error 11433 script.onerror = function() { 11434 console.error('Failed to load cookie declaration script'); 11435 previewContainer.innerHTML = ` 11436 <div class="alert alert-warning"> 11437 <i class="fas fa-exclamation-triangle me-2"></i> 11438 <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['previewUnavailableDesc'] || 'Unable to load the script in preview mode due to CORS restrictions.'} 11439 <br><br> 11440 <div class="bg-light p-3 rounded mt-2"> 11441 <strong>${translations['yourScript'] || 'Your Script'}:</strong><br> 11442 <code>${scriptCode}</code> 11443 </div> 11444 <br> 11445 <small class="text-muted"> 11446 ${translations['scriptWorksOnWebsite'] || 'The script will work perfectly when implemented on your website. This preview limitation doesn\'t affect the actual functionality.'} 11447 </small> 11448 </div> 11449 `; 11450 }; 11451 11452 // Append script to document body to execute it 11453 document.body.appendChild(script); 11454 11455 // Clean up script after some time 11456 setTimeout(() => { 11457 if (document.body.contains(script)) { 11458 document.body.removeChild(script); 11459 } 11460 }, 10000); 11461 11462 } catch (error) { 11463 console.error('Error loading preview:', error); 11464 previewContainer.innerHTML = ` 11465 <div class="alert alert-info"> 11466 <i class="fas fa-info-circle me-2"></i> 11467 <strong>${translations['preview'] || 'Preview'}:</strong> ${translations['scriptReadyToUse'] || 'Your cookie declaration script is ready to use.'} 11468 <br><br> 11469 <div class="bg-light p-3 rounded mt-2"> 11470 <strong>${translations['yourScript'] || 'Your Script'}:</strong><br> 11471 <code>${scriptCode}</code> 11472 </div> 11473 <br> 11474 <small class="text-muted"> 11475 ${translations['scriptDisplaysTablesDesc'] || 'When implemented on your website, this script will automatically display your cookie declaration tables.'} 11476 </small> 11477 </div> 11478 `; 11479 } 11480} 11481 11482document.addEventListener('shown.bs.tab', function (e) { 11483 if (e.target.id === 'preview-tab') { 11484 const previewContainer = document.getElementById('previewContainer'); 11485 if (previewContainer && previewContainer.innerHTML.includes(translations['loadingPreviewDesc'] || 'Loading cookie declaration preview')) { 11486 const scriptCodeArea = document.getElementById('scriptCodeArea'); 11487 const domainDropdown = document.getElementById('select_domain'); 11488 11489 if (scriptCodeArea && domainDropdown) { 11490 const projects = getProjects(); 11491 const projectId = findProjectIdByDomainId(domainDropdown.value, projects); 11492 loadPreviewFromScript(scriptCodeArea.value, projectId); 11493 } 11494 } 11495 } 11496}); 11497 11498 11499 11500 11501// Cookie Declaration Script 11502function createCookieDeclarationCard(domainId) { 11503 const declarationCard = document.createElement('div'); 11504 declarationCard.classList.add('card', 'mb-4'); 11505 declarationCard.id = 'cookie_declaration_section'; 11506 11507 declarationCard.innerHTML = ` 11508 <div class="card-header"> 11509 <div class="d-flex justify-content-between align-items-center"> 11510 <h5 class="card-title mb-0"> 11511 <i class="fas fa-code me-2"></i>${translations['cookieDeclarationScript'] || 'Cookie Declaration Script'} 11512 </h5> 11513 <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="collapse" data-bs-target="#declarationCardBody" aria-expanded="false" aria-controls="declarationCardBody"> 11514 <i class="fas fa-chevron-down" id="declarationToggleIcon"></i> 11515 </button> 11516 </div> 11517 </div> 11518 <div class="collapse" id="declarationCardBody"> 11519 <div class="card-body"> 11520 <div class="row align-items-center"> 11521 <div class="col-md-8"> 11522 <h6 class="text-primary mb-3"> 11523 <i class="fas fa-file-code me-2"></i>${translations['generateScriptForWebsite'] || 'Generate Script for Your Website'} 11524 </h6> 11525 <p class="text-muted mb-3"> 11526 ${translations['declarationScriptDescription'] || 'Generate a script tag that displays your cookie declaration tables on your website. This helps you comply with GDPR, CCPA, and other privacy regulations by providing transparent information about all cookies used on your site.'} 11527 </p> 11528 <div class="mb-2"> 11529 <h6 class="text-success mb-2"> 11530 <i class="fas fa-check-circle me-2"></i>${translations['scriptFeatures'] || 'Script Features:'} 11531 </h6> 11532 <ul class="list-unstyled mb-0"> 11533 <li class="mb-1"> 11534 <i class="fas fa-table text-primary me-2"></i> 11535 ${translations['feature1'] || 'Automatically organized cookie tables by category'} 11536 </li> 11537 <li class="mb-1"> 11538 <i class="fas fa-mobile-alt text-info me-2"></i> 11539 ${translations['feature2'] || 'Mobile-responsive design'} 11540 </li> 11541 <li class="mb-1"> 11542 <i class="fas fa-sync text-warning me-2"></i> 11543 ${translations['feature3'] || 'Auto-updates when you modify cookies'} 11544 </li> 11545 <li class="mb-1"> 11546 <i class="fas fa-shield-alt text-secondary me-2"></i> 11547 ${translations['feature4'] || 'GDPR and CCPA compliant formatting'} 11548 </li> 11549 <li class="mb-0"> 11550 <i class="fas fa-link text-success me-2"></i> 11551 ${translations['feature5'] || 'Ready to link from your privacy policy'} 11552 </li> 11553 </ul> 11554 </div> 11555 </div> 11556 <div class="col-md-4"> 11557 <div class="text-center"> 11558 <div class="mb-4"> 11559 <i class="fas fa-code fa-3x text-primary mb-3"></i> 11560 <h6><strong>${translations['scriptActions'] || 'Script Actions'}</strong></h6> 11561 </div> 11562 <div class="d-grid gap-2"> 11563 <button type="button" class="btn btn-primary btn-lg" id="generateScriptBtn"> 11564 <i class="fas fa-code me-2"></i>${translations['cookieDeclarationScript'] || 'Cookie Declaration Script'} 11565 </button> 11566 <button type="button" class="btn btn-success" id="previewDeclarationBtn"> 11567 <i class="fas fa-eye me-2"></i>${translations['previewDeclaration'] || 'Preview Declaration'} 11568 </button> 11569 <button type="button" class="btn btn-info" id="viewInstructionsBtn"> 11570 <i class="fas fa-info-circle me-2"></i>${translations['viewInformation'] || 'View Information'} 11571 </button> 11572 </div> 11573 <div class="mt-3 pt-3 border-top"> 11574 <small class="text-muted"> 11575 <i class="fas fa-info-circle me-1"></i> 11576 ${translations['oneTimeSetup'] || 'One-time setup, automatic updates.'} 11577 </small> 11578 </div> 11579 </div> 11580 </div> 11581 </div> 11582 11583 <!-- Script Content Area (Initially Hidden) --> 11584 <div id="scriptContentArea" class="mt-4" style="display: none;"> 11585 <div class="border-top pt-4"> 11586 <div id="scriptLoadingContent"> 11587 <div class="text-center py-4"> 11588 <div class="spinner-border text-primary" role="status">
11589 <span class="visually-hidden">${translations['loading'] || 'Loading...'}</span> 11590 </div> 11591 <p class="mt-3 text-muted">${translations['loadingScript'] || 'Loading script...'}</p> 11592 </div> 11593 </div> 11594 </div> 11595 </div> 11596 </div> 11597 </div> 11598 `; 11599 11600 return declarationCard; 11601} 11602 11603function displayEnhancedCookieManager(data, domainId) { 11604 console.log('displayEnhancedCookieManager called'); 11605 11606 const container = document.getElementById('cookie_manager_container'); 11607 11608 // Check if scanner section already exists 11609 const existingScanner = document.getElementById('cookie_scanner_section'); 11610 const existingDeclaration = document.getElementById('cookie_declaration_section'); 11611 11612 if (existingScanner && existingDeclaration) { 11613 console.log('Scanner and Declaration sections already exist, not recreating'); 11614 // Don't clear the container if both exist, just update the cookie list part 11615 const existingCookieCard = container.querySelector('.card:not(#cookie_scanner_section):not(#cookie_declaration_section)'); 11616 if (existingCookieCard) { 11617 existingCookieCard.remove(); 11618 } 11619 } else { 11620 console.log('Creating new sections'); 11621 container.innerHTML = ''; 11622 11623 // Add cookie scanner section first (Collapsed by default) 11624 const scannerSection = createCookieScannerSection(domainId); 11625 container.appendChild(scannerSection); 11626 11627 // Add event listeners for scanner buttons 11628 const newScanBtn = document.getElementById('newScanBtn'); 11629 const viewStatusBtn = document.getElementById('viewStatusBtn'); 11630 11631 if (newScanBtn) { 11632 newScanBtn.addEventListener('click', () => startNewScan(domainId)); 11633 } 11634 11635 if (viewStatusBtn) { 11636 viewStatusBtn.addEventListener('click', () => viewScanStatus(domainId)); 11637 } 11638 11639 // Add cookie declaration card (Collapsed by default) 11640 const declarationCard = createCookieDeclarationCard(domainId); 11641 container.appendChild(declarationCard); 11642 11643 // Add event listeners for declaration card 11644 const generateScriptBtn = document.getElementById('generateScriptBtn'); 11645 const previewDeclarationBtn = document.getElementById('previewDeclarationBtn'); 11646 const viewInstructionsBtn = document.getElementById('viewInstructionsBtn'); 11647 11648 if (generateScriptBtn) { 11649 generateScriptBtn.addEventListener('click', () => loadCookieDeclarationScript(domainId)); 11650 } 11651 11652 if (previewDeclarationBtn) { 11653 previewDeclarationBtn.addEventListener('click', () => showCookieDeclarationPreview(domainId)); 11654 } 11655 11656 if (viewInstructionsBtn) { 11657 viewInstructionsBtn.addEventListener('click', () => showInstructionsInCard()); 11658 } 11659 11660 // Add toggle icon rotation for collapse/expand 11661 setupCollapseToggle('declarationCardBody', 'declarationToggleIcon'); 11662 setupCollapseToggle('scannerCardBody', 'scannerToggleIcon'); 11663 setupCollapseToggle('cookieListCardBody', 'cookieListToggleIcon'); 11664 } 11665 11666 // Create main cookie list card 11667 createCookieListCard(container, data, domainId); 11668} 11669 11670function setupCollapseToggle(bodyId, iconId) { 11671 const cardBody = document.getElementById(bodyId); 11672 const toggleIcon = document.getElementById(iconId); 11673 11674 if (cardBody && toggleIcon) { 11675 cardBody.addEventListener('show.bs.collapse', function () { 11676 toggleIcon.classList.remove('fa-chevron-down'); 11677 toggleIcon.classList.add('fa-chevron-up'); 11678 }); 11679 11680 cardBody.addEventListener('hide.bs.collapse', function () { 11681 toggleIcon.classList.remove('fa-chevron-up'); 11682 toggleIcon.classList.add('fa-chevron-down'); 11683 }); 11684 } 11685} 11686 11687function createCookieListCard(container, data, domainId) { 11688 const card = document.createElement('div'); 11689 card.classList.add('card', 'shadow-sm'); 11690 container.appendChild(card); 11691 11692 // Cookie List Card Header 11693 const cardHeader = document.createElement('div'); 11694 cardHeader.classList.add('card-header'); 11695 card.appendChild(cardHeader); 11696 11697 const headerContainer = document.createElement('div'); 11698 headerContainer.classList.add('d-flex', 'justify-content-between', 'align-items-center'); 11699 cardHeader.appendChild(headerContainer); 11700 11701 const headerTitle = document.createElement('h5'); 11702 headerTitle.classList.add('card-title', 'mb-0'); 11703 headerTitle.innerHTML = '<i class="fas fa-list me-2"></i>' + (translations['cookieList'] || 'Cookie List'); 11704 headerContainer.appendChild(headerTitle); 11705 11706 const headerToggleBtn = document.createElement('button'); 11707 headerToggleBtn.type = 'button'; 11708 headerToggleBtn.classList.add('btn', 'btn-outline-secondary', 'btn-sm'); 11709 headerToggleBtn.setAttribute('data-bs-toggle', 'collapse'); 11710 headerToggleBtn.setAttribute('data-bs-target', '#cookieListCardBody'); 11711 headerToggleBtn.setAttribute('aria-expanded', 'false'); 11712 headerToggleBtn.setAttribute('aria-controls', 'cookieListCardBody'); 11713 headerToggleBtn.innerHTML = '<i class="fas fa-chevron-down" id="cookieListToggleIcon"></i>'; 11714 headerContainer.appendChild(headerToggleBtn); 11715 11716 // Cookie List Collapsible Body 11717 const cardBodyCollapse = document.createElement('div'); 11718 cardBodyCollapse.classList.add('collapse'); 11719 cardBodyCollapse.id = 'cookieListCardBody'; 11720 card.appendChild(cardBodyCollapse); 11721 11722 const cardBody = document.createElement('div'); 11723 cardBody.classList.add('card-body'); 11724 cardBodyCollapse.appendChild(cardBody); 11725 11726 // Get cookies with proper filtering
11727 let cookies = []; 11728 if (data && data.value && Array.isArray(data.value.resources)) { 11729 cookies = data.value.resources.filter(resource => { 11730 if (!resource) return false; 11731 const cookieTypes = ['Essential', 'Personalization', 'Analytics', 'Marketing', 'Security', 'Other']; 11732 return !resource.type || 11733 resource.type === 'cookie' || 11734 cookieTypes.includes(resource.type) || 11735 (resource.name && resource.displayName); 11736 }); 11737 } 11738 11739 // Add stats badges 11740 const statsContainer = document.createElement('div'); 11741 statsContainer.classList.add('d-flex', 'gap-2', 'mb-4'); 11742 11743 const totalBadge = document.createElement('span'); 11744 totalBadge.classList.add('badge', 'bg-primary', 'fs-6'); 11745 totalBadge.textContent = cookies.length + ' Total'; 11746 statsContainer.appendChild(totalBadge); 11747 11748 const systemCount = cookies.filter(c => !c.custom).length; 11749 if (systemCount > 0) { 11750 const systemBadge = document.createElement('span'); 11751 systemBadge.classList.add('badge', 'bg-outline-secondary', 'text-secondary', 'border', 'border-secondary', 'fs-6'); 11752 systemBadge.textContent = systemCount + ' System'; 11753 systemBadge.style.backgroundColor = 'transparent'; 11754 statsContainer.appendChild(systemBadge); 11755 } 11756 11757 const customCount = cookies.filter(c => c.custom).length; 11758 if (customCount > 0) { 11759 const customBadge = document.createElement('span'); 11760 customBadge.classList.add('badge', 'bg-success', 'fs-6'); 11761 customBadge.textContent = customCount + ' Custom'; 11762 statsContainer.appendChild(customBadge); 11763 } 11764 11765 cardBody.appendChild(statsContainer); 11766 11767 // Action buttons and search container 11768 const actionsContainer = document.createElement('div'); 11769 actionsContainer.classList.add('d-flex', 'justify-content-between', 'align-items-center', 'mb-4'); 11770 11771 const buttonsContainer = document.createElement('div'); 11772 buttonsContainer.classList.add('d-flex', 'gap-2'); 11773 11774 const addButton = document.createElement('button'); 11775 addButton.classList.add('btn', 'btn-primary'); 11776 addButton.innerHTML = '<i class="fa fa-plus me-2"></i>' + (translations['addCookie'] || 'Add Cookie'); 11777 addButton.addEventListener('click', () => showAddCookieModal(data, domainId)); 11778 buttonsContainer.appendChild(addButton); 11779 11780 const refreshButton = document.createElement('button'); 11781 refreshButton.classList.add('btn', 'btn-outline-secondary'); 11782 refreshButton.innerHTML = '<i class="fa fa-refresh me-2"></i>' + (translations['refresh'] || 'Refresh'); 11783 refreshButton.addEventListener('click', () => loadCookiesForDomain(domainId)); 11784 buttonsContainer.appendChild(refreshButton); 11785 11786 actionsContainer.appendChild(buttonsContainer); 11787 11788 // Search container 11789 const searchContainer = document.createElement('div'); 11790 searchContainer.classList.add('d-flex', 'gap-2'); 11791 11792 const searchInput = document.createElement('input'); 11793 searchInput.type = 'text'; 11794 searchInput.classList.add('form-control'); 11795 searchInput.placeholder = 'Search cookies...'; 11796 searchInput.style.width = '250px'; 11797 searchContainer.appendChild(searchInput); 11798 11799 const filterSelect = document.createElement('select'); 11800 filterSelect.classList.add('form-select'); 11801 filterSelect.style.width = '150px'; 11802 filterSelect.innerHTML = ` 11803 <option value="all">${translations['allTypes'] || 'All Types'}</option> 11804 <option value="Essential">${translations['essential'] || 'Essential'}</option> 11805 <option value="Personalization">${translations['personalization'] || 'Personalization'}</option> 11806 <option value="Analytics">${translations['analytics'] || 'Analytics'}</option> 11807 <option value="Marketing">${translations['marketing'] || 'Marketing'}</option> 11808 <option value="Security">${translations['security'] || 'Security'}</option> 11809 <option value="Other">${translations['other'] || 'Other'}</option> 11810 `; 11811 searchContainer.appendChild(filterSelect); 11812 11813 actionsContainer.appendChild(searchContainer); 11814 cardBody.appendChild(actionsContainer); 11815 11816 // Create table 11817 const tableContainer = document.createElement('div'); 11818 tableContainer.classList.add('table-responsive'); 11819 cardBody.appendChild(tableContainer); 11820 11821 const table = document.createElement('table'); 11822 table.classList.add('table', 'table-hover', 'cookie-manager-table', 'mb-0'); 11823 table.id = 'cookiesTable'; 11824 tableContainer.appendChild(table); 11825 11826 const thead = document.createElement('thead'); 11827 thead.classList.add('table-dark'); 11828 table.appendChild(thead); 11829 const headerRow = document.createElement('tr'); 11830 thead.appendChild(headerRow); 11831 11832 const headers = [ 11833 { key: 'displayName', text: 'Display Name', icon: 'fa-file-code', sortable: true }, 11834 { key: 'name', text: 'Name', icon: 'fa-tag', sortable: true }, 11835 { key: 'type', text: 'Type', icon: 'fa-layer-group', sortable: true }, 11836 { key: 'retentionPeriod', text: 'Retention', icon: 'fa-clock', sortable: true }, 11837 { key: 'actions', text: 'Actions', icon: 'fa-cogs', sortable: false } 11838 ]; 11839
11840 headers.forEach(header => { 11841 const th = document.createElement('th'); 11842 th.classList.add('user-select-none'); 11843 if (header.sortable) { 11844 th.classList.add('cursor-pointer'); 11845 th.style.cursor = 'pointer'; 11846 } 11847 11848 const content = `<i class="fa ${header.icon} me-2"></i>` + (translations[header.key] || header.text); 11849 if (header.sortable) { 11850 th.innerHTML = content + ' <i class="fas fa-sort text-muted ms-1"></i>'; 11851 th.addEventListener('click', () => sortTable(header.key)); 11852 } else { 11853 th.innerHTML = content; 11854 } 11855 11856 headerRow.appendChild(th); 11857 }); 11858 11859 const tbody = document.createElement('tbody'); 11860 tbody.id = 'cookiesTableBody'; 11861 table.appendChild(tbody); 11862 11863 // Store cookies globally for search/filter 11864 window.currentCookies = cookies; 11865 11866 // Render initial table 11867 renderCookieTable(cookies, data, domainId); 11868 11869 // Add search and filter functionality 11870 let searchTimeout; 11871 searchInput.addEventListener('input', () => { 11872 clearTimeout(searchTimeout); 11873 searchTimeout = setTimeout(() => { 11874 filterAndSearch(cookies, data, domainId); 11875 }, 300); 11876 }); 11877 11878 filterSelect.addEventListener('change', () => { 11879 filterAndSearch(cookies, data, domainId); 11880 }); 11881} 11882 11883async function loadCookieDeclarationScript(domainId) { 11884 const scriptContentArea = document.getElementById('scriptContentArea'); 11885 const scriptLoadingContent = document.getElementById('scriptLoadingContent'); 11886 11887 if (!scriptContentArea || !scriptLoadingContent) { 11888 console.error('Script content area not found'); 11889 return; 11890 } 11891 11892 scriptContentArea.style.display = 'block'; 11893 11894 try { 11895 const domainDropdown = document.getElementById('select_domain'); 11896 if (!domainDropdown || !domainDropdown.value) { 11897 throw new Error(translations['noDomainSelected'] || 'No domain selected'); 11898 } 11899 11900 const currentDomainId = domainDropdown.value; 11901 const projects = await getProjects(); 11902 11903 if (!projects || !projects.length) { 11904 throw new Error(translations['noProjectsAvailable'] || 'No projects available'); 11905 } 11906 11907 const projectId = findProjectIdByDomainId(currentDomainId, projects); 11908 if (!projectId) { 11909 throw new Error(translations['projectNotFound'] || 'Project not found for selected domain'); 11910 } 11911 11912 const data = { 11913 sid: getSid(), 11914 projectId: projectId 11915 }; 11916 11917 const response = await postRequest(endpoint + '/scriptcode', data); 11918 11919 if (response && response.ok) { 11920 const responseBody = await response.json(); 11921 if (responseBody.statusCode === 0) { 11922 scriptLoadingContent.innerHTML = generateScriptCardContent(responseBody.cookiesScriptCode); 11923 window.currentCookieScript = responseBody.cookiesScriptCode; 11924 } else { 11925 throw new Error(responseBody.statusMessage || 'Server error'); 11926 } 11927 } else { 11928 throw new Error(translations['failedToLoadScript'] || 'Failed to load script'); 11929 } 11930 } catch (error) { 11931 console.error('Error loading cookie declaration script:', error); 11932 scriptLoadingContent.innerHTML = ` 11933 <div class="alert alert-danger"> 11934 <i class="fas fa-exclamation-triangle me-2"></i> 11935 <strong>${translations['error'] || 'Error'}:</strong> ${error.message} 11936 </div> 11937 `; 11938 } 11939} 11940 11941function generateScriptCardContent(scriptCode) { 11942 return ` 11943 <div class="mb-4"> 11944 <div class="d-flex justify-content-between align-items-center mb-3"> 11945 <h6 class="mb-0 text-primary"> 11946 <i class="fas fa-code me-2"></i>${translations['scriptTagForWebsite'] || 'Script Tag for Your Website'} 11947 </h6> 11948 <button type="button" class="btn btn-primary btn-sm" onclick="copyScriptToClipboard()"> 11949 <i class="fas fa-copy me-2"></i>${translations['copyScriptTag'] || 'Copy Script Tag'} 11950 </button> 11951 </div> 11952 11953 <div class="alert alert-info mb-3"> 11954 <i class="fas fa-info-circle me-2"></i> 11955 <strong>${translations['simpleIntegration'] || 'Simple Integration'}:</strong> 11956 ${translations['simpleIntegrationDesc'] || 'Just copy this single script tag and paste it into your website\'s HTML head section. This will automatically generate the cookie declaration tables on your page.'} 11957 </div> 11958 11959 <div class="bg-light p-3 rounded border"> 11960 <label class="form-label small text-muted">${translations['htmlScriptTag'] || 'HTML Script Tag'}:</label> 11961 <textarea 11962 class="form-control font-monospace" 11963 id="scriptCodeArea" 11964 rows="3" 11965 readonly 11966 style="font-size: 0.9em; line-height: 1.4; background: white;" 11967 >${scriptCode}</textarea> 11968 </div> 11969 11970 <div class="mt-4"> 11971 <h6 class="fw-bold mb-2">${translations['howToAddToWebsite'] || 'How to Add to Your Website'}:</h6> 11972 <div class="bg-light p-3 rounded"> 11973 <p class="mb-2 small"><strong>${translations['option1AddToHead'] || 'Option 1 - Add to HTML Head'}:</strong></p> 11974 <pre class="mb-3" style="font-size: 0.85em;"><code><html> 11975<head> 11976 <title>Your Website</title> 11977 ${scriptCode.replace(/</g, '<').replace(/>/g, '>')} 11978</head> 11979<body> 11980 <div id="cookie-declaration"></div> 11981</body> 11982</html></code></pre> 11983 11984 <p class="mb-2 small"><strong>${translations['option2AddToPage'] || 'Option 2 - Add to Specific Page'}:</strong></p> 11985 <pre style="font-size: 0.85em;"><code><div id="cookie-declaration"> 11986 ${scriptCode.replace(/</g, '<').replace(/>/g, '>')} 11987</div></code></pre> 11988 </div> 11989 </div> 11990 11991 <div class="mt-3"> 11992 <small class="text-muted"> 11993 <i class="fas fa-shield-alt me-1"></i> 11994 ${translations['scriptSecurityDesc'] || 'This script is hosted securely and will automatically display your current cookie configuration. It updates automatically when you modify cookies in this manager.'} 11995 </small> 11996 </div> 11997 </div> 11998 `; 11999} 12000 12001async function copyScriptToClipboard() { 12002 const textarea = document.getElementById('scriptCodeArea'); 12003 if (!textarea) { 12004 console.error('Script code area not found'); 12005 return; 12006 } 12007 12008 try { 12009 await navigator.clipboard.writeText(textarea.value); 12010 displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success'); 12011 } catch (err) { 12012 // Fallback for older browsers 12013 textarea.select(); 12014 document.execCommand('copy'); 12015 displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success'); 12016 } 12017} 12018 12019function loadScriptInPreview(scriptCode, containerId) { 12020 const container = document.getElementById(containerId); 12021 if (!container) { 12022 console.error('Preview container not found'); 12023 return; 12024 } 12025 12026 try { 12027 // Extract script URL 12028 const scriptUrlMatch = scriptCode.match(/src=["']([^"']+)["']/); 12029 12030 if (!scriptUrlMatch) { 12031 container.innerHTML = ` 12032 <div class="alert alert-warning"> 12033 <i class="fas fa-exclamation-triangle me-2"></i> 12034 <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['couldNotExtractURL'] || 'Could not extract script URL'} 12035 </div> 12036 `; 12037 return; 12038 } 12039 12040 const scriptUrl = scriptUrlMatch[1]; 12041 12042 // Try to load the script via iframe for better isolation 12043 const iframe = document.createElement('iframe'); 12044 iframe.style.width = '100%'; 12045 iframe.style.height = '600px'; 12046 iframe.style.border = 'none'; 12047 iframe.srcdoc = ` 12048 <!DOCTYPE html> 12049 <html> 12050 <head> 12051 <style> 12052 body { font-family: Arial, sans-serif; padding: 20px; } 12053 table { width: 100%; border-collapse: collapse; margin: 20px 0; } 12054 th, td { padding: 10px; text-align: left; border: 1px solid #ddd; } 12055 th { background-color: #f5f5f5; } 12056 </style> 12057 </head> 12058 <body> 12059 <div id="cookie-declaration"></div> 12060 ${scriptCode} 12061 </body> 12062 </html> 12063 `; 12064 12065 // Add error handling for iframe 12066 iframe.onerror = function() { 12067 // If iframe fails, try direct script loading 12068 container.innerHTML = '<div id="cookie-declaration"></div>'; 12069 12070 const script = document.createElement('script'); 12071 script.src = scriptUrl; 12072 script.async = true; 12073 12074 script.onload = function() { 12075 console.log('Cookie declaration script loaded successfully'); 12076 }; 12077 12078 script.onerror = function() { 12079 console.error('Failed to load cookie declaration script'); 12080 container.innerHTML = ` 12081 <div class="alert alert-warning"> 12082 <i class="fas fa-exclamation-triangle me-2"></i> 12083 <strong>${translations['previewUnavailable'] || 'Preview Unavailable'}:</strong> ${translations['previewUnavailableDesc'] || 'Unable to load the script in preview mode due to CORS restrictions.'} 12084 <br><br> 12085 <div class="bg-light p-3 rounded mt-2"> 12086 <strong>${translations['yourScript'] || 'Your Script'}:</strong><br> 12087 <code>${escapeHtml(scriptCode)}</code> 12088 </div> 12089 <br> 12090 <small class="text-muted"> 12091 ${translations['scriptWorksOnWebsite'] || 'The script will work perfectly when implemented on your website. This preview limitation doesn\'t affect the actual functionality.'} 12092 </small> 12093 </div> 12094 `; 12095 }; 12096 12097 document.body.appendChild(script); 12098 12099 // Clean up script after some time 12100 setTimeout(() => { 12101 if (document.body.contains(script)) { 12102 document.body.removeChild(script); 12103 } 12104 }, 10000); 12105 }; 12106 12107 container.innerHTML = ''; 12108 container.appendChild(iframe); 12109 12110 } catch (error) { 12111 console.error('Error loading preview:', error); 12112 container.innerHTML = ` 12113 <div class="alert alert-info"> 12114 <i class="fas fa-info-circle me-2"></i> 12115 <strong>${translations['preview'] || 'Preview'}:</strong> ${translations['scriptReadyToUse'] || 'Your cookie declaration script is ready to use.'} 12116 <br><br> 12117 <div class="bg-light p-3 rounded mt-2"> 12118 <strong>${translations['yourScript'] || 'Your Script'}:</strong><br> 12119 <code>${escapeHtml(scriptCode)}</code> 12120 </div> 12121 <br> 12122 <small class="text-muted"> 12123 ${translations['scriptDisplaysTablesDesc'] || 'When implemented on your website, this script will automatically display your cookie declaration tables.'} 12124 </small> 12125 </div> 12126 `; 12127 } 12128} 12129 12130function showInstructionsInCard() { 12131 const scriptContentArea = document.getElementById('scriptContentArea'); 12132 const scriptLoadingContent = document.getElementById('scriptLoadingContent'); 12133 12134 if (!scriptContentArea || !scriptLoadingContent) { 12135 console.error('Script content area not found'); 12136 return; 12137 } 12138 12139 scriptContentArea.style.display = 'block'; 12140 12141 scriptLoadingContent.innerHTML = ` 12142 <div class="row"> 12143 <div class="col-md-8"> 12144 <div class="alert alert-info mb-4"> 12145 <h6 class="alert-heading"> 12146 <i class="fas fa-info-circle me-2"></i>${translations['whyNeedThis'] || 'Why Do You Need This?'} 12147 </h6> 12148 <p class="mb-2">${translations['legalComplianceDesc'] || 'Legal compliance regulations like GDPR, CCPA, and other privacy laws require websites to provide transparent information about all cookies they use. This script generates professional cookie declaration tables that:'} 12149 </p> 12150 <ul class="mb-0 small"> 12151 <li>${translations['listAllCookies'] || 'Lists all cookies used on your website in organized tables'}</li> 12152 <li>${translations['showCookiePurposes'] || 'Shows cookie purposes, retention periods, and providers'}</li> 12153 <li>${translations['helpComplyTransparency'] || 'Helps you comply with transparency requirements'}</li> 12154 <li>${translations['linkFromPrivacyPolicy'] || 'Can be linked from your privacy policy or cookie policy'}</li> 12155 <li>${translations['updatesAutomatically'] || 'Updates automatically as you modify your cookie configuration'}</li> 12156 </ul> 12157 </div> 12158 12159 <h6 class="text-primary mb-3"> 12160 <i class="fas fa-rocket me-2"></i>${translations['howToAddDeclaration'] || 'How to Add Cookie Declaration to Your Website'} 12161 </h6> 12162 12163 <div class="mb-4"> 12164 <h6 class="fw-bold mb-2">${translations['step1CopyScript'] || 'Step 1: Copy the Script Tag'}</h6> 12165 <p class="text-muted">${translations['step1Desc'] || 'Copy the script tag from the "Script Co
12165de" tab. This is a simple one-line HTML tag that you\'ll add to your website.'}</p> 12166 </div> 12167 12168 <div class="mb-4"> 12169 <h6 class="fw-bold mb-2">${translations['step2AddToHTML'] || 'Step 2: Add to Your Website\'s HTML'}</h6> 12170 <p class="text-muted">${translations['step2Desc'] || 'Paste the script tag into your Cookie Declaration HTML where you want the tables to appear:'}</p> 12171 <pre class="bg-light p-3 rounded"><code><!DOCTYPE html> 12172<html> 12173<head> 12174 <title>Cookie Declaration</title> 12175</head> 12176<body> 12177 <h1>Our Cookie Declaration</h1> 12178 <div id="cookie-declaration"> 12179 <!-- Script will be inserted here --> 12180 </div> 12181</body> 12182</html></code></pre> 12183 </div> 12184 12185 <div class="mb-4"> 12186 <h6 class="fw-bold mb-2">${translations['step3LinkFromPolicy'] || 'Step 3: Link from Your Privacy Policy'}</h6> 12187 <p class="text-muted">${translations['step3Desc'] || 'Add a link to your cookie declaration page from your privacy policy, cookie policy, or website footer:'}</p> 12188 <pre class="bg-light p-3 rounded"><code><a href="/cookies.html">View Our Cookie Declaration</a></code></pre> 12189 </div> 12190 12191 <div class="mb-4"> 12192 <h6 class="fw-bold mb-2">${translations['step4AutoUpdates'] || 'Step 4: Automatic Updates'}</h6> 12193 <p class="text-muted">${translations['step4Desc'] || 'The script automatically updates when you modify your cookie configuration in this manager. No need to manually update your website - it stays current automatically!'}</p> 12194 </div> 12195 </div> 12196 12197 <div class="col-md-4"> 12198 <div class="card bg-light"> 12199 <div class="card-body"> 12200 <h6 class="card-title text-success"> 12201 <i class="fas fa-shield-check me-2"></i>${translations['legalComplianceBenefits'] || 'Legal Compliance Benefits'} 12202 </h6> 12203 <ul class="list-unstyled mb-3 small"> 12204 <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['gdprCompliance'] || 'GDPR Article 13 compliance'}</li> 12205 <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['ccpaCompliance'] || 'CCPA transparency requirements'}</li> 12206 <li class="mb-2"><i class="fas fa-check text-success me-2"></i>${translations['cookieLawCompliance'] || 'Cookie law disclosure obligations'}</li> 12207 <li class="mb-0"><i class="fas fa-check text-success me-2"></i>${translations['isoCompliance'] || 'ISO 27001 documentation standards'}</li> 12208 </ul> 12209 12210 <h6 class="card-title text-info mt-4"> 12211 <i class="fas fa-star me-2"></i>${translations['features'] || 'Features'} 12212 </h6> 12213 <ul class="list-unstyled mb-0 small"> 12214 <li class="mb-2"><i class="fas fa-mobile-alt text-primary me-2"></i>${translations['mobileResponsive'] || 'Mobile responsive design'}</li> 12215 <li class="mb-2"><i class="fas fa-palette text-info me-2"></i>${translations['professionalStyling'] || 'Professional styling'}</li> 12216 <li class="mb-2"><i class="fas fa-layer-group text-warning me-2"></i>${translations['organizedByCategories'] || 'Organized by categories'}</li> 12217 <li class="mb-2"><i class="fas fa-link text-success me-2"></i>${translations['privacyPolicyLinks'] || 'Privacy policy links'}</li> 12218 <li class="mb-0"><i class="fas fa-sync text-secondary me-2"></i>${translations['autoUpdatesWithChanges'] || 'Auto-updates with changes'}</li> 12219 </ul> 12220 </div> 12221 </div> 12222 12223 <div class="alert alert-warning mt-3"> 12224 <i class="fas fa-lightbulb me-2"></i> 12225 <strong>${translations['important'] || 'Important'}:</strong> ${translations['scriptGeneratesTablesDesc'] || 'This script generates tables based on your current cookie configuration.'} 12226 </div> 12227 12228 <div class="alert alert-info mt-3"> 12229 <i class="fas fa-users me-2"></i> 12230 <strong>${translations['userFriendly'] || 'User-Friendly'}:</strong> ${translations['userFriendlyDesc'] || 'Your website visitors will see clean, organized tables showing exactly what cookies your site uses and why - building trust and meeting legal requirements.'} 12231 </div> 12232 </div> 12233 </div> 12234 `; 12235} 12236 12237function escapeHtml(text) { 12238 const map = { 12239 '&': '&', 12240 '<': '<', 12241 '>': '>', 12242 '"': '"', 12243 "'": ''' 12244 }; 12245 return text.replace(/[&<>"']/g, m => map[m]); 12246} 12247 12248function hideCookiePreview() { 12249 const scriptContentArea = document.getElementById('scriptContentArea'); 12250 if (scriptContentArea) { 12251 scriptContentArea.style.display = 'none'; 12252 } 12253} 12254 12255function copyScriptFromPreview() { 12256 const scriptCode = window.currentCookieScript; 12257 12258 if (!scriptCode) {
12259 displayAlertCookie(translations['noScriptToCopy'] || 'No script to copy', 'error'); 12260 return; 12261 } 12262 12263 navigator.clipboard.writeText(scriptCode).then(() => { 12264 displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success'); 12265 }).catch(() => { 12266 // Fallback copy method 12267 const textarea = document.createElement('textarea'); 12268 textarea.value = scriptCode; 12269 textarea.style.position = 'fixed'; 12270 textarea.style.opacity = '0'; 12271 document.body.appendChild(textarea); 12272 textarea.select(); 12273 document.execCommand('copy'); 12274 document.body.removeChild(textarea); 12275 displayAlertCookie(translations['scriptCopied'] || 'Script copied to clipboard!', 'success'); 12276 }); 12277} 12278 12279async function showCookieDeclarationPreview(domainId) { 12280 const scriptContentArea = document.getElementById('scriptContentArea'); 12281 const scriptLoadingContent = document.getElementById('scriptLoadingContent'); 12282 12283 if (!scriptContentArea || !scriptLoadingContent) { 12284 console.error('Script content area not found'); 12285 return; 12286 } 12287 12288 scriptContentArea.style.display = 'block'; 12289 12290 // Show loading state 12291 scriptLoadingContent.innerHTML = ` 12292 <div class="text-center py-4"> 12293 <div class="spinner-border text-primary" role="status"> 12294 <span class="visually-hidden">${translations['loading'] || 'Loading...'}</span> 12295 </div> 12296 <p class="mt-3 text-muted">${translations['loadingPreview'] || 'Loading preview...'}</p> 12297 </div> 12298 `; 12299 12300 try { 12301 const domainDropdown = document.getElementById('select_domain'); 12302 if (!domainDropdown || !domainDropdown.value) { 12303 throw new Error(translations['noDomainSelected'] || 'No domain selected'); 12304 } 12305 12306 const currentDomainId = domainDropdown.value; 12307 const projects = await getProjects(); 12308 12309 if (!projects || !projects.length) { 12310 throw new Error(translations['noProjectsAvailable'] || 'No projects available'); 12311 } 12312 12313 const projectId = findProjectIdByDomainId(currentDomainId, projects); 12314 if (!projectId) { 12315 throw new Error(translations['projectNotFound'] || 'Project not found for selected domain'); 12316 } 12317 12318 const data = { 12319 sid: getSid(), 12320 projectId: projectId 12321 }; 12322 12323 const response = await postRequest(endpoint + '/scriptcode', data); 12324 12325 if (response && response.ok) { 12326 const responseBody = await response.json(); 12327 if (responseBody.statusCode === 0) { 12328 // Store both scripts globally 12329 window.currentCookieScript = responseBody.cookiesScriptCode; 12330 window.currentPageCode = responseBody.pageCode; 12331 12332 // Display the preview with both scripts 12333 scriptLoadingContent.innerHTML = generatePreviewContent( 12334 responseBody.cookiesScriptCode, 12335 responseBody.pageCode 12336 ); 12337 12338 } else { 12339 throw new Error(responseBody.statusMessage || 'Server error'); 12340 } 12341 } else { 12342 throw new Error(translations['failedToLoadScript'] || 'Failed to load script'); 12343 } 12344 } catch (error) { 12345 console.error('Error loading preview:', error); 12346 scriptLoadingContent.innerHTML = ` 12347 <div class="alert alert-danger"> 12348 <i class="fas fa-exclamation-triangle me-2"></i> 12349 <strong>${translations['error'] || 'Error'}:</strong> ${error.message} 12350 </div> 12351 `; 12352 } 12353} 12354 12355function generatePreviewContent(scriptCode, pageCode) { 12356 const uniquePreviewId = 'cookie-declaration-preview-' + Date.now(); 12357 12358 const previewHtml = ` 12359 <div class="mb-4"> 12360 <div class="d-flex justify-content-between align-items-center mb-3"> 12361 <h6 class="mb-0 text-primary"> 12362 <i class="fas fa-eye me-2"></i>${translations['livePreviewTitle'] || 'Live Preview - Exactly How It Will Look on Your Website'} 12363 </h6> 12364 <button type="button" class="btn btn-sm btn-outline-primary" onclick="hideCookiePreview()"> 12365 <i class="fas fa-times me-2"></i>${translations['closePreview'] || 'Close Preview'} 12366 </button> 12367 </div> 12368 12369 <div class="alert alert-info mb-3"> 12370 <i class="fas fa-info-circle me-2"></i> 12371 <strong>${translations['previewNote'] || 'Preview Note'}:</strong> 12372 ${translations['livePreviewDesc'] || 'This preview shows exactly how the cookie declaration will appear on your website.'} 12373 </div> 12374
12375 <!-- Script Code Display --> 12376 <div class="mb-3"> 12377 <div class="d-flex justify-content-between align-items-center mb-2"> 12378 <h6 class="text-muted mb-0"> 12379 <i class="fas fa-code me-1"></i>${translations['yourScriptCode'] || 'Your Script Code:'} 12380 </h6> 12381 <button class="btn btn-sm btn-primary" onclick="copyScriptFromPreview()"> 12382 <i class="fas fa-copy me-1"></i>${translations['copyScript'] || 'Copy Script'} 12383 </button> 12384 </div> 12385 <div class="bg-light p-3 rounded border"> 12386 <code class="d-block" style="white-space: pre-wrap; word-break: break-all; font-size: 0.85em;"> 12387 ${escapeHtml(scriptCode)} 12388 </code> 12389 </div> 12390 </div> 12391 12392 <!-- Preview Container --> 12393 <div class="border rounded p-4 bg-white" style="max-height: 600px; overflow-y: auto;"> 12394 <div id="${uniquePreviewId}"> 12395 <div class="text-center py-4"> 12396 <div class="spinner-border text-primary" role="status"> 12397 <span class="visually-hidden">${translations['loadingPreview'] || 'Loading preview...'}</span> 12398 </div> 12399 <p class="mt-2 text-muted">${translations['loadingPreviewDesc'] || 'Loading cookie declaration preview...'}</p> 12400 </div> 12401 </div> 12402 </div> 12403 12404 <div class="mt-3"> 12405 <div class="alert alert-success"> 12406 <i class="fas fa-check-circle me-2"></i> 12407 <strong>${translations['readyToUse'] || 'Ready to Use'}:</strong> ${translations['readyToUseDesc'] || 'This is exactly what your website visitors will see.'} 12408 </div> 12409 </div> 12410 </div> 12411 `; 12412 12413 // Load the complex script after a short delay 12414 setTimeout(() => { 12415 loadComplexScriptPreview(scriptCode, pageCode, uniquePreviewId); 12416 }, 100); 12417 12418 return previewHtml; 12419} 12420 12421function loadComplexScriptPreview(cookiesScriptCode, pageCode, containerId) { 12422 const container = document.getElementById(containerId); 12423 if (!container) { 12424 console.error('Preview container not found'); 12425 return; 12426 } 12427 12428 try { 12429 // Always treat as complex script 12430 const iframe = document.createElement('iframe'); 12431 iframe.style.width = '100%'; 12432 iframe.style.minHeight = '500px'; 12433 iframe.style.border = 'none'; 12434 iframe.style.backgroundColor = 'white'; 12435 12436 // Use minimal sandboxing to allow the scripts to work 12437 iframe.sandbox = 'allow-scripts allow-same-origin allow-popups allow-forms'; 12438 12439 // Build the complete HTML with both pageCode and cookiesScriptCode 12440 const iframeContent = ` 12441 <!DOCTYPE html> 12442 <html> 12443 <head> 12444 <meta charset="UTF-8"> 12445 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 12446 <style> 12447 * { 12448 margin: 0; 12449 padding: 0; 12450 box-sizing: border-box; 12451 } 12452 body { 12453 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 12454 padding: 20px; 12455 background: white; 12456 color: #333; 12457 line-height: 1.6; 12458 } 12459 .cookie-declaration-wrapper { 12460 max-width: 1200px; 12461 margin: 0 auto; 12462 } 12463 table { 12464 width: 100%; 12465 border-collapse: collapse; 12466 margin: 20px 0; 12467 background: white; 12468 box-shadow: 0 1px 3px rgba(0,0,0,0.1); 12469 } 12470 th, td { 12471 padding: 12px 15px; 12472 text-align: left; 12473 border: 1px solid #e0e0e0; 12474 } 12475 th { 12476 background-color: #f8f9fa; 12477 font-weight: 600; 12478 color: #2c3e50; 12479 text-transform: uppercase; 12480 font-size: 0.85em; 12481 letter-spacing: 0.5px; 12482 } 12483 tr:nth-child(even) { 12484 background-color: #fafafa; 12485 } 12486 tr:hover { 12487 background-color: #f0f0f0; 12488 } 12489 .cookie-category { 12490 margin: 30px 0; 12491 } 12492 .category-title { 12493 font-size: 1.4em; 12494 font-weight: 600; 12495 margin-bottom: 15px; 12496 color: #2c3e50; 12497 border-bottom: 2px solid #3498db; 12498 padding-bottom: 10px; 12499 } 12500 .cookie-description { 12501 margin: 10px 0; 12502 color: #666; 12503 font-size: 0.95em; 12504 } 12505 /* Specific styles for cookie declaration elements */ 12506 #cookie-declaration { 12507 min-height: 100px; 12508 } 12509 .cookie-table-container { 12510 margin: 20px 0; 12511 } 12512 .cookie-policy-link { 12513 color: #3498db; 12514 text-decoration: none; 12515 } 12516 .cookie-policy-link:hover { 12517 text-decoration: underline; 12518 } 12519 /* Loading state */ 12520 .loading { 12521 text-align: center; 12522 padding: 40px; 12523 color: #999; 12524 } 12525 /* Error state */ 12526 .error-message { 12527 background: #fee; 12528 border: 1px solid #fcc; 12529 color: #c33; 12530 padding: 15px; 12531 border-radius: 4px; 12532 margin: 20px 0; 12533 } 12534 </style> 12535 ${pageCode ? ` 12536 <!-- Page Code (Additional Scripts/Styles) --> 12537 ${pageCode} 12538 ` : ''} 12539 </head> 12540 <body> 12541 <div class="cookie-declaration-wrapper"> 12542 <div id="cookie-declaration"> 12543 <!-- Cookie tables will be injected here --> 12544 </div> 12545 </div> 12546 12547 <!-- Cookie Declaration Script --> 12548 ${cookiesScriptCode} 12549 12550 <!-- Fallback detection script --> 12551 <script> 12552 // Check if content was loaded after 3 seconds 12553 setTimeout(function() { 12554 var container = document.getElementById('cookie-declaration'); 12555 if (container && container.innerHTML.trim() === '') { 12556 container.innerHTML = '<div class="error-message">Cookie declaration is loading... If this message persists, please check your browser console for errors.</div>'; 12557 } 12558 }, 3000); 12559 12560 // Auto-adjust iframe height 12561 function adjustHeight() { 12562 if (window.parent && window.parent.postMessage) { 12563 var height = document.body.scrollHeight; 12564 window.parent.postMessage({ 12565 type: 'adjustHeight', 12566 height: height 12567 }, '*'); 12568 } 12569 } 12570 12571 // Adjust height on load and after content changes 12572 window.addEventListener('load', adjustHeight); 12573 setTimeout(adjustHeight, 1000); 12574 setTimeout(adjustHeight, 2000); 12575 setTimeout(adjustHeight, 5000); 12576 12577 // Watch for DOM changes 12578 var observer = new MutationObserver(adjustHeight); 12579 observer.observe(document.body, { 12580 childList: true, 12581 subtree: true 12582 }); 12583 </script> 12584 </body> 12585 </html> 12586 `; 12587 12588 iframe.srcdoc = iframeContent; 12589 12590 // Listen for height adjustment messages 12591 window.addEventListener('message', function(e) { 12592 if (e.data && e.data.type === 'adjustHeight') { 12593 iframe.style.height = (e.data.height + 50) + 'px'; 12594 } 12595 }); 12596 12597 iframe.onload = function() { 12598 console.log('Preview iframe loaded successfully'); 12599 }; 12600 12601 iframe.onerror = function() { 12602 console.error('Preview iframe failed to load'); 12603 showFallbackPreview(container, cookiesScriptCode); 12604 }; 12605 12606 container.innerHTML = ''; 12607 container.appendChild(iframe); 12608 12609 } catch (error) { 12610 console.error('Error loading preview:', error); 12611 showFallbackPreview(container, cookiesScriptCode); 12612 } 12613} 12614 12615function showFallbackPreview(container, scriptCode) { 12616 container.innerHTML = ` 12617 <div class="alert alert-warning"> 12618 <h6 class="alert-heading"> 12619 <i class="fas fa-exclamation-triangle me-2"></i>${translations['previewLimitations'] || 'Preview Limitations'} 12620 </h6> 12621 <p>${translations['previewCannotBeDisplayed'] || 'The preview cannot be displayed due to browser security restrictions, but your script is ready and will work perfectly on your website.'}</p> 12622 <div class="bg-light p-3 rounded mt-3"> 12623 <strong>${translations['yourGeneratedScript'] || 'Your Generated Script'}:</strong> 12624 <pre class="mb-0 mt-2" style="white-space: pre-wrap; word-break: break-all;"> 12625 <code>${escapeHtml(scriptCode)}</code> 12626 </pre> 12627 </div> 12628 <hr> 12629 <div class="d-flex justify-content-between align-items-center"> 12630 <small class="text-muted"> 12631 <i class="fas fa-shield-alt me-1"></i> 12632 ${translations['browserSecurityFeature'] || 'This is a browser security feature and doesn\'t affect the actual functionality.'} 12633 </small> 12634 <button class="btn btn-sm btn-primary" onclick="copyScriptFromPreview()"> 12635 <i class="fas fa-copy me-1"></i>${translations['copyScript'] || 'Copy Script'} 12636 </button> 12637 </div> 12638 </div> 12639 `; 12640} 12641 12642// ââ Banner copy experiment ââââââââââââââââââââââââââââââââââââââââââââââââââ 12643// Two arms only in the UI: A is the project's own text, B is a variant. The 12644// server's model allows more, but a merchant comparing three wordings on consent 12645// copy will not reach a usable sample on any of them, and the report would spend 12646// its life saying "collecting". 12647function fcLoadExperiment(exp) { 12648 const field = document.getElementById('experiment_field'); 12649 const weight = document.getElementById('experiment_weight'); 12650 const running = document.getElementById('experiment_running'); 12651 const valueB = document.getElementById('experiment_value_b'); 12652 if (!field || !weight || !running || !valueB) return; 12653 12654 exp = exp || {}; 12655 const arms = exp.arms || []; 12656 const b = arms.length > 1 ? arms[1] : null; 12657 field.value = exp.field === 'homeDescription' ? 'homeDescription' : 'homeTitle'; 12658 running.checked = exp.status === 'running'; 12659 valueB.value = b && b.value != null ? b.value : ''; 12660 // Round to the nearest offered split rather than showing a value the select 12661 // cannot represent â an unrepresentable <select> value posts "" (the bug the 12662 // consent-mode normalizer exists for). 12663 const pct = b && arms[0] ? Math.round((b.weight * 100) / (b.weight + arms[0].weight)) : 50; 12664 weight.value = [10, 25, 50].reduce((a, c) => (Math.abs(c - pct) < Math.abs(a - pct) ? c : a), 50); 12665 // Keep the id across saves so the stat rows already collected stay comparable; 12666 // mint one only when there is no test yet. 12667 fcExperimentId = exp.id || ''; 12668} 12669 12670let fcExperimentId = ''; 12671 12672function fcCollectExperiment() { 12673 const field = document.getElementById('experiment_field'); 12674 const weight = document.getElementById('experiment_weight'); 12675 const running = document.getElementById('experiment_running'); 12676 const valueB = document.getElementById('experiment_value_b'); 12677 if (!field || !weight || !running || !valueB) return undefined; 12678 12679 const b = parseInt(weight.value, 10) || 50; 12680 const text = valueB.value.trim();
12681 // A running test with no variant text is not a test â B would be identical to A 12682 // and the report would compare a wording with itself. 12683 if (!text) return {}; 12684 if (!fcExperimentId) fcExperimentId = 'exp-' + Date.now().toString(36); 12685 return { 12686 id: fcExperimentId, 12687 field: field.value, 12688 status: running.checked ? 'running' : 'stopped', 12689 arms: [{ id: 'a', weight: 100 - b, value: null }, { id: 'b', weight: b, value: text }] 12690 }; 12691} 12692 12693// Report. Renders only what the server was willing to state: an arm below the 12694// visitor floor shows no rate at all, and 'no-difference' is printed as plainly as 12695// a winner would be. 12696async function loadExperimentReport(projectId, startDate, endDate) { 12697 const box = document.getElementById('experiment_report'); 12698 if (!box) return; 12699 box.innerHTML = ''; 12700 if (!projectId || projectId === '0') return; 12701 12702 const response = await postRequest(endpoint + '/cmp/data-stats-experiment', 12703 { sid: getSid(), projectId: projectId, startDate: startDate, endDate: endDate }); 12704 if (!response || !response.ok) return; 12705 const body = await response.json(); 12706 if (body.statusCode !== 0 || !body.arms || !body.arms.length) return; 12707 12708 const card = document.createElement('div'); 12709 card.classList.add('card', 'card-std'); 12710 const inner = document.createElement('div'); 12711 inner.classList.add('card-body'); 12712 card.appendChild(inner); 12713 12714 const h = document.createElement('h6'); 12715 h.textContent = translations['experimentResultsTitle'] || 'Results'; 12716 inner.appendChild(h); 12717 12718 const table = document.createElement('table'); 12719 table.classList.add('table', 'table-sm', 'mb-2'); 12720 const head = document.createElement('tr'); 12721 [translations['experimentArm'] || 'Version', 12722 translations['visitors'] || 'Visitors', 12723 translations['experimentDecided'] || 'Made a choice', 12724 translations['experimentCompletion'] || 'Completion rate'].forEach(function (t, i) { 12725 const th = document.createElement('th'); 12726 th.textContent = t; 12727 if (i > 0) th.classList.add('text-end'); 12728 head.appendChild(th); 12729 }); 12730 const thead = document.createElement('thead'); 12731 thead.appendChild(head); 12732 table.appendChild(thead); 12733 const tbody = document.createElement('tbody'); 12734 table.appendChild(tbody); 12735
12736 body.arms.forEach(function (a) { 12737 const tr = document.createElement('tr'); 12738 const name = document.createElement('td'); 12739 name.textContent = a.arm === 'a' 12740 ? (translations['experimentVersionA'] || 'A (current text)') 12741 : (translations['experimentVersionBShort'] || 'B (variant)'); 12742 tr.appendChild(name); 12743 [String(a.clients), String(a.decided), 12744 a.enoughData 12745 ? Math.round(a.completionRate * 1000) / 10 + '% ±' + Math.round(a.completionMargin * 1000) / 10 12746 : (translations['experimentNotEnough'] || 'not enough data yet') 12747 ].forEach(function (v) { 12748 const td = document.createElement('td'); 12749 td.classList.add('text-end'); 12750 td.textContent = v; 12751 tr.appendChild(td); 12752 }); 12753 tbody.appendChild(tr); 12754 }); 12755 inner.appendChild(table); 12756 12757 const verdict = document.createElement('p'); 12758 verdict.classList.add('mb-0'); 12759 const v = body.verdict || {}; 12760 if (v.state === 'leader') { 12761 verdict.textContent = (translations['experimentLeader'] || 'More visitors made a choice with version [Arm].') 12762 .replace('[Arm]', v.arm === 'a' ? 'A' : 'B'); 12763 } else if (v.state === 'no-difference') { 12764 verdict.textContent = translations['experimentNoDifference'] 12765 || 'No meaningful difference between the two versions yet.'; 12766 } else { 12767 verdict.textContent = translations['experimentCollecting'] 12768 || 'Still collecting â each version needs at least 100 visitors before a result is shown.'; 12769 } 12770 verdict.classList.add('text-muted'); 12771 verdict.style.fontSize = '.9rem'; 12772 inner.appendChild(verdict); 12773 12774 box.appendChild(card); 12775} 12776 12777// âââââââââââââââââââââââââââââââââââââââââââââââââ §9 privacy requests (DSAR) 12778// 12779// The merchant is the data controller; this is the tool that lets them answer a 12780// visitor's access or erasure request. Everything server-side is scoped to a project 12781// they own, and a key that belongs to somebody else's project answers exactly the same 12782// as a key that does not exist â so nothing here can be used to probe for one. 12783 12784function fcPrivacyBox() { 12785 return document.getElementById('privacy_result'); 12786} 12787 12788function fcPrivacyMessage(text, kind) { 12789 const box = fcPrivacyBox(); 12790 if (!box) return; 12791 box.innerHTML = ''; 12792 const div = document.createElement('div'); 12793 div.classList.add('alert', kind || 'alert-secondary', 'mb-0', 'py-2'); 12794 div.style.fontSize = '.9rem'; 12795 div.textContent = text; 12796 box.appendChild(div); 12797} 12798 12799function fcPrivacyRow(table, label, value) { 12800 if (value === null || value === undefined || value === '') return; 12801 const tr = document.createElement('tr'); 12802 const th = document.createElement('th'); 12803 th.style.width = '45%'; 12804 th.style.fontWeight = '500'; 12805 th.textContent = label; 12806 const td = document.createElement('td'); 12807 td.textContent = String(value); 12808 tr.appendChild(th); 12809 tr.appendChild(td); 12810 table.appendChild(tr); 12811} 12812 12813async function fcPrivacyLookup() { 12814 const input = document.getElementById('privacy_client_key'); 12815 // The page identifies its project by the ?id= query param (see the loader above); 12816 // there is no #project_id element to read. 12817 const projectId = new URLSearchParams(window.location.search).get('id'); 12818 if (!input) return; 12819 const key = (input.value || '').trim().toLowerCase(); 12820 input.value = key; 12821 12822 // Checked here as well as on the server, so an obvious typo costs no round trip. 12823 if (!/^[a-z0-9]{64}$/.test(key)) { 12824 fcPrivacyMessage(translations['privacyKeyInvalid'] 12825 || 'That does not look like a visitor key. It is exactly 64 characters, letters and digits.', 12826 'alert-warning'); 12827 return; 12828 } 12829 fcPrivacyMessage(translations['privacyLooking'] || 'Lookingâ¦'); 12830 12831 const response = await postRequest(endpoint + '/cmp/privacy-lookup', { sid: getSid(), projectId: projectId, clientKey: key }); 12832 if (!response || !response.ok) { 12833 fcPrivacyMessage(translations['privacyLookupFailed'] || 'Could not reach the server. Try again.', 'alert-danger'); 12834 return; 12835 } 12836 const body = await response.json(); 12837 if (body.statusCode !== 0) { 12838 fcPrivacyMessage(body.statusMessage || (translations['privacyLookupFailed'] || 'Lookup failed.'), 'alert-danger'); 12839 return; 12840 } 12841 if (!body.found) { 12842 fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary'); 12843 return; 12844 } 12845 12846 const box = fcPrivacyBox(); 12847 box.innerHTML = ''; 12848 const card = document.createElement('div'); 12849 card.classList.add('card', 'card-std'); 12850 const inner = document.createElement('div'); 12851 inner.classList.add('card-body'); 12852 card.appendChild(inner); 12853 12854 const h = document.createElement('h6'); 12855 h.textContent = translations['privacyRecordFound'] || 'Record found'; 12856 inner.appendChild(h); 12857 12858 const table = document.createElement('table'); 12859 table.classList.add('table', 'table-sm', 'mb-3'); 12860 fcPrivacyRow(table, translations['privacyFirstSeen'] || 'First seen', body.firstSeen); 12861 fcPrivacyRow(table, translations['privacyLastSeen'] || 'Last seen', body.lastSeen); 12862 fcPrivacyRow(table, translations['privacyDomains'] || 'Domains', body.domains); 12863 fcPrivacyRow(table, translations['privacyActivityDays'] || 'Days with activity', body.activityDays); 12864 fcPrivacyRow(table, translations['privacyConsentRecords'] || 'Consent records', body.consentRecords); 12865 inner.appendChild(table); 12866 12867 const actions = document.createElement('div'); 12868 actions.classList.add('d-flex', 'gap-2'); 12869 12870 const exportBtn = document.createElement('button'); 12871 exportBtn.type = 'button'; 12872 exportBtn.classList.add('btn', 'btn-sm', 'btn-outline-primary'); 12873 exportBtn.textContent = translations['privacyDownload'] || 'Download their data (JSON)'; 12874 exportBtn.addEventListener('click', function () { fcPrivacyExport(key, projectId); }); 12875 actions.appendChild(exportBtn); 12876 12877 const eraseBtn = document.createElement('button'); 12878 eraseBtn.type = 'button'; 12879 eraseBtn.classList.add('btn', 'btn-sm', 'btn-outline-danger'); 12880 eraseBtn.textContent = translations['privacyErase'] || 'Erase this record'; 12881 eraseBtn.addEventListener('click', function () { fcPrivacyErase(key, projectId); }); 12882 actions.appendChild(eraseBtn); 12883 12884 inner.appendChild(actions); 12885 box.appendChild(card); 12886} 12887 12888async function fcPrivacyExport(key, projectId) { 12889 const response = await postRequest(endpoint + '/cmp/privacy-export', { sid: getSid(), projectId: projectId, clientKey: key }); 12890 if (!response || !response.ok) { 12891 fcPrivacyMessage(translations['privacyLookupFailed'] || 'Could not reach the server. Try again.', 'alert-danger'); 12892 return; 12893 } 12894 const body = await response.json(); 12895 if (body.statusCode !== 0 || !body.found || !body.export) { 12896 fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary'); 12897 return; 12898 } 12899 // Hand the merchant a file they can forward to the person who asked. The key is in 12900 // the filename because they may be handling several requests at once, truncated 12901 // because the whole thing is not a useful filename. 12902 const blob = new Blob([JSON.stringify(body.export, null, 2)], { type: 'application/json' }); 12903 const a = document.createElement('a'); 12904 a.href = URL.createObjectURL(blob); 12905 a.download = 'visitor-data-' + key.slice(0, 12) + '.json'; 12906 document.body.appendChild(a); 12907 a.click(); 12908 document.body.removeChild(a); 12909 setTimeout(function () { URL.revokeObjectURL(a.href); }, 0); 12910} 12911 12912async function fcPrivacyErase(key, projectId) { 12913 const warning = translations['privacyEraseConfirm'] 12914 || 'Erase this visitor\'s record? Their identifier and consent choice are deleted and cannot be recovered. ' 12915 + 'Your daily reports keep their numbers.'; 12916 if (!window.confirm(warning)) return; 12917 12918 const response = await postRequest(endpoint + '/cmp/privacy-erase', { sid: getSid(), projectId: projectId, clientKey: key }); 12919 if (!response || !response.ok) { 12920 fcPrivacyMessage(translations['privacyEraseFailed'] || 'Erase failed. Nothing was deleted.', 'alert-danger'); 12921 return; 12922 } 12923 const body = await response.json(); 12924 if (body.statusCode !== 0 || !body.found) { 12925 fcPrivacyMessage(translations['privacyNotFound'] || 'No record for that key on this project.', 'alert-secondary'); 12926 return; 12927 } 12928 12929 const r = body.receipt || {}; 12930 const box = fcPrivacyBox(); 12931 box.innerHTML = ''; 12932 const div = document.createElement('div'); 12933 div.classList.add('alert', 'alert-success', 'mb-0'); 12934 div.style.fontSize = '.9rem'; 12935 12936 const strong = document.createElement('div'); 12937 strong.style.fontWeight = '600'; 12938 strong.textContent = translations['privacyErased'] || 'Record erased.'; 12939 div.appendChild(strong); 12940 12941 const counts = document.createElement('div');
12942 counts.classList.add('mt-1'); 12943 const deleted = r.rowsDeleted || {}; 12944 // The translation pipeline strips trailing whitespace from every value, so the space before 12945 // each number is added here rather than trusted to the text. 12946 counts.textContent = (translations['privacyErasedCounts'] || 'Consent records deleted:').trimEnd() + ' ' 12947 + (deleted.client_consent || 0) + ' · ' 12948 + (translations['privacyStatRowsKept'] || 'daily rows kept and now anonymous:').trimEnd() + ' ' 12949 + (r.statRowsRetained || 0); 12950 div.appendChild(counts); 12951 12952 // Said here rather than left to be discovered: the key is in the visitor's browser 12953 // and we cannot clear it. A merchant who assumes otherwise would see the "erased" 12954 // visitor reappear and conclude the erasure failed. 12955 if (r.visitorMayReturn) { 12956 const note = document.createElement('div'); 12957 note.classList.add('mt-2', 'text-muted'); 12958 note.style.fontSize = '.85rem'; 12959 note.textContent = translations['privacyVisitorMayReturn'] || r.visitorMayReturn; 12960 div.appendChild(note); 12961 } 12962 box.appendChild(div); 12963} 12964 12965// âââââââââââââââââââââââââââââââââââââââââââââââââ §10 cookie policy from the real scan 12966// 12967// The generated cookie policy used to describe a site that used cookies without ever 12968// saying which. This builds that section from what the scanner actually found, and is 12969// appended to the template output rather than requiring a {trackerList} placeholder in 12970// every one of the stored template translations â a placeholder-only design would sit 12971// inert until somebody edited all of them, which is exactly how the embed-placeholder 12972// switch stayed dead for a fortnight. 12973 12974function fcPolicyEsc(v) { 12975 return escapeHtml(String(v === null || v === undefined ? '' : v)); 12976} 12977 12978/** 12979 * Read the short code off the selected language option. The dropdown's VALUE is a numeric 12980 * language id, but each option already carries its shortCode as a data attribute, so no 12981 * id-to-code mapping has to be invented or kept in sync here. 12982 */ 12983function fcSelectedLanguageCode() { 12984 const sel = document.getElementById('select_language'); 12985 if (!sel || sel.selectedIndex < 0) return ''; 12986 const opt = sel.options[sel.selectedIndex]; 12987 return (opt && opt.dataset && opt.dataset.languageCode) ? String(opt.dataset.languageCode).toLowerCase() : ''; 12988} 12989 12990/** Fetch the composed policy for a domain. Returns null when there is nothing to add. */ 12991async function fcFetchCookiePolicy(domainId, lang) { 12992 if (!domainId) return null; 12993 const response = await postRequest(endpoint + '/cmp/cookie-policy', 12994 { sid: getSid(), domainId: domainId, lang: lang || '' }); 12995 if (!response || !response.ok) return null; 12996 const body = await response.json(); 12997 if (body.statusCode !== 0 || !body.policy) return null; 12998 return body.policy; 12999} 13000 13001/** 13002 * Render the tracker section as HTML for the document. 13003 * 13004 * Renders the freshness note in every state, including â especially â the states with no 13005 * trackers to show. A merchant whose site has never been scanned must not receive a 13006 * document that silently reads as "this site sets no cookies". 13007 */ 13008// Escaping keeps a value inside the attribute, but href="javascript:..." is still a 13009// javascript: URL once escaped. Only http and https reach an href; anything else drops the 13010// link and keeps the provider name, which is the part that carries meaning. 13011function fcPolicyHref(url) { 13012 const u = String(url || '').trim(); 13013 return /^https?:\/\//i.test(u) ? u : ''; 13014} 13015 13016function fcCookiePolicySectionHtml(policy) { 13017 if (!policy) return ''; 13018 const t = (k, d) => translations[k] || d; 13019 const f = policy.freshness || {}; 13020 let html = '<h2>' + fcPolicyEsc(t('policyTrackersHeading', 'Cookies and trackers on this site')) + '</h2>'; 13021 13022 if (f.note) { 13023 const warn = (f.state === 'never' || f.state === 'stale' || f.state === 'empty'); 13024 html += '<p style="' + (warn ? 'padding:8px;border-left:3px solid #b8860b;background:#fdf6e3;' : '') 13025 + 'font-size:.95em;">' + fcPolicyEsc(f.note) + '</p>'; 13026 } 13027 13028 const sections = policy.sections || []; 13029 for (let i = 0; i < sections.length; i++) { 13030 const s = sections[i]; 13031 html += '<h3>' + fcPolicyEsc(t('policyCategory_' + s.category, s.category)) + '</h3>'; 13032 html += '<p>' + fcPolicyEsc(s.purpose) + '</p>'; 13033 html += '<p><em>' + fcPolicyEsc(s.consentRequired 13034 ? t('policyConsentRequired', 'These run only after you agree.') 13035 : t('policyNoConsentRequired', 'These run without asking, because the site cannot work without them.')) 13036 + '</em></p>'; 13037 html += '<table border="1" cellspacing="0" cellpadding="6" style="border-collapse:collapse;width:100%;font-size:.9em;">' 13038 + '<thead><tr>' 13039 + '<th align="left">' + fcPolicyEsc(t('policyColName', 'Name')) + '</th>' 13040 + '<th align="left">' + fcPolicyEsc(t('policyColProvider', 'Provider')) + '</th>' 13041 + '<th align="left">' + fcPolicyEsc(t('policyColPurpose', 'Purpose')) + '</th>' 13042 + '<th align="left">' + fcPolicyEsc(t('policyColStorage', 'Type')) + '</th>' 13043 + '<th align="left">' + fcPolicyEsc(t('policyColRetention', 'Retention')) + '</th>' 13044 + '</tr></thead><tbody>'; 13045 const rows = s.trackers || []; 13046 for (let j = 0; j < rows.length; j++) { 13047 const r = rows[j]; 13048 let provider = fcPolicyEsc(r.provider);
13049 const href = fcPolicyHref(r.providerPrivacyUrl); 13050 if (href) 13051 provider += ' (<a href="' + fcPolicyEsc(href) + '" rel="noopener noreferrer">' 13052 + fcPolicyEsc(t('policyProviderPolicy', 'privacy policy')) + '</a>)'; 13053 let name = fcPolicyEsc(r.name); 13054 if (r.httpOnly) 13055 // Worth flagging: a visitor cannot inspect or clear these from the page. 13056 name += ' <small>' + fcPolicyEsc(t('policyHttpOnly', '(server-set)')) + '</small>'; 13057 html += '<tr><td>' + name + '</td><td>' + provider + '</td><td>' + fcPolicyEsc(r.purpose) 13058 + '</td><td>' + fcPolicyEsc(r.storage) + '</td><td>' + fcPolicyEsc(r.retention) + '</td></tr>'; 13059 } 13060 html += '</tbody></table>'; 13061 } 13062 13063 // The store covers the names it has been run over, not every tracker on every site. A 13064 // merchant told nothing publishes a half-translated document believing it finished. 13065 const lg = policy.language; 13066 if (lg && lg.note && lg.fellBackToSiteText > 0) 13067 html += '<p style="font-size:.85em;color:#555;">' + fcPolicyEsc(lg.note) + '</p>'; 13068 13069 if (policy.disclaimer) 13070 html += '<p style="font-size:.85em;color:#555;margin-top:16px;">' + fcPolicyEsc(policy.disclaimer) + '</p>'; 13071 return html; 13072} 13073 13074/** 13075 * Fold the section into the generated document: replace a {trackerList} placeholder if the 13076 * template has one, otherwise append. Appending is what keeps this working against the 13077 * templates as they stand today. 13078 */ 13079function fcApplyCookiePolicySection(documentContent, policy) { 13080 const section = fcCookiePolicySectionHtml(policy); 13081 if (!section) return documentContent; 13082 if (documentContent && documentContent.indexOf('{trackerList}') >= 0) 13083 return documentContent.split('{trackerList}').join(section); 13084 return (documentContent || '') + section; 13085} 13086 13087// âââââââââââââââââââââââââââââââââââââââââââââââââ §12 the site's own banner wording 13088// 13089// Two fields over the shared translated library. Empty means default, so clearing a box 13090// restores our wording rather than publishing a blank banner â the server stores an 13091// override that normalises to nothing as absent, and the page leaves the library alone. 13092 13093function fcLoadBannerText(bt) { 13094 const title = document.getElementById('banner_text_title'); 13095 const desc = document.getElementById('banner_text_description'); 13096 if (!title || !desc) return; 13097 const t = bt && typeof bt === 'object' ? bt : {}; 13098 title.value = t['homeTitle'] || ''; 13099 desc.value = t['homeDescription'] || ''; 13100} 13101 13102function fcCollectBannerText() { 13103 const title = document.getElementById('banner_text_title'); 13104 const desc = document.getElementById('banner_text_description'); 13105 const out = {}; 13106 // Only non-empty fields travel. The server whitelists and re-checks anyway; sending 13107 // blanks would just be asking it to store an instruction to show nothing. 13108 if (title && title.value.trim()) out['homeTitle'] = title.value.trim(); 13109 if (desc && desc.value.trim()) out['homeDescription'] = desc.value.trim(); 13110 return out; 13111}
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.