1/* HomeZero Apps CMS hydration 2 * Reads published apps from the CMS and fills the three sticky 3 * panels in the Webflow homepage. Filters by <body data-audience>. 4 */ 5(function () { 6 var SUPABASE_URL = 'https://ystckkktjcktugqoqxtd.supabase.co'; 7 var SUPABASE_KEY = 8 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlzdGNra2t0amNrdHVncW9xeHRkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgzNDQ1NDMsImV4cCI6MjA5MzkyMDU0M30.rAemX4qel2oTBLq3xHvKUC3r2Ak8kmSSr7XeQlctHUc'; 9 10 // panel id -> category, inner-tab class, active class, blur class, soon tag, soon-active 11 var PANELS = [ 12 { id: 'sales', category: 'Leads - Go - flows', tabClass: 'sales-tab', activeClass: 'active-tab', blurClass: 'ylw-blur', tagClass: 'tg-sn', tagActive: 'tg-sn-act' }, 13 { id: 'operations', category: 'Sales - Pro - CRM', tabClass: 'operat-tab', activeClass: 'active-tab', blurClass: 'blue-blur', tagClass: 'tg-op-sn', tagActive: 'tg-op-sn-act' }, 14 { id: 'services', category: 'Operatie - Ops - ERP', tabClass: 'serv-tab', activeClass: 'active-tab-ser', blurClass: 'black-blur', tagClass: 'tg-sr-sn', tagActive: 'tg-sr-sn-act' } 15 ]; 16 17 function audienceFilter(visibility) { 18 var aud = (document.body && document.body.getAttribute('data-audience')) || 'all'; 19 if (aud === 'B2C') return visibility === 'Both' || visibility === 'B2C'; 20 if (aud === 'B2B2C') return visibility === 'Both' || visibility === 'B2B2C'; 21 return true; 22 } 23 24 function fetchApps() { 25 var url = 26 SUPABASE_URL + 27 '/rest/v1/apps?select=*' + 28 '&archived=eq.false&is_draft=eq.false&published_at=not.is.null' + 29 '&order=sort_number.asc,name.asc'; 30 return fetch(url, { 31 headers: { apikey: SUPABASE_KEY, Authorization: 'Bearer ' + SUPABASE_KEY } 32 }).then(function (r) { 33 if (!r.ok) throw new Error('apps fetch ' + r.status); 34 return r.json(); 35 }); 36 } 37 38 function renderPanel(panelEl, apps, cfg) { 39 if (!panelEl || !apps.length) return; 40 // Guard: avoid re-running on the already-mutated DOM (React StrictMode 41 // double-invokes script-injection effects in dev, and SPA navigation can 42 // re-execute the IIFE). Re-querying templates from a mutated panel would 43 // pick up the previous render's stripped-down clone (e.g. no <img>), 44 // breaking apps that only have an image_url. 45 if (panelEl.__hzAppsCmsDone) return; 46 panelEl.__hzAppsCmsDone = true; 47 48 49 50 var tabsList = panelEl.querySelector('.apps-col-col'); 51 var tabTpl = panelEl.querySelector('.apps-tab-btn'); 52 var descContainer = panelEl.querySelector('.apps-desc-coll-coll'); 53 var descTpl = panelEl.querySelector('.apps-desc-item'); 54 var videoContainer = panelEl.querySelector('.mock-vid-col-col'); 55 var videoTpl = panelEl.querySelector('.video-item'); 56 57 if (!tabsList || !tabTpl || !descContainer || !descTpl) return; 58 59 var tabTemplate = tabTpl.cloneNode(true); 60 var descTemplate = descTpl.cloneNode(true); 61 var videoTemplate = videoTpl ? videoTpl.cloneNode(true) : null; 62 tabsList.innerHTML = ''; 63 descContainer.innerHTML = ''; 64 if (videoContainer) videoContainer.innerHTML = ''; 65 66 panelEl.querySelectorAll('.w-dyn-empty').forEach(function (el) { el.style.display = 'none'; }); 67 68 var tabEls = []; 69 70 apps.forEach(function (app, idx) { 71 var tab = tabTemplate.cloneNode(true); 72 tab.removeAttribute('fs-cmsfilter-value'); 73 var inner = tab.querySelector('.' + cfg.tabClass) || tab.firstElementChild; 74 if (inner) inner.setAttribute('data-app-id', app.app_id); 75 76 var label = tab.querySelector('.tab-text'); 77 if (label) { 78 label.textContent = app.name; 79 label.classList.remove('w-dyn-bind-empty'); 80 } 81 var soon = tab.querySelector('.' + cfg.tagClass); 82 if (soon) soon.style.display = app.show_soon_tag ? '' : 'none'; 83 84 // Hide the colored blur by default; we'll show it on the active tab only. 85 var blur = tab.querySelector('.' + cfg.blurClass); 86 if (blur) blur.style.display = 'none'; 87 88 tab.style.cursor = 'pointer'; 89 tab.addEventListener('click', function () { showApp(idx); }); 90 tabsList.appendChild(tab); 91 tabEls.push(tab); 92 }); 93 94 function showApp(idx) { 95 var app = apps[idx]; 96 97 // Toggle active state on tabs
98 tabEls.forEach(function (t, i) { 99 var inner = t.querySelector('.' + cfg.tabClass) || t.firstElementChild; 100 var blur = t.querySelector('.' + cfg.blurClass); 101 var soonTag = t.querySelector('.' + cfg.tagClass); 102 if (inner) inner.classList.toggle(cfg.activeClass, i === idx); 103 if (blur) blur.style.display = i === idx ? 'block' : 'none'; 104 if (soonTag) soonTag.classList.toggle(cfg.tagActive, i === idx); 105 }); 106 107 // Description 108 var desc = descTemplate.cloneNode(true); 109 var h3 = desc.querySelector('.h3-apps'); 110 var p = desc.querySelector('.p1.apps, .p1, p'); 111 if (h3) { 112 h3.textContent = app.title_description || app.name; 113 h3.classList.remove('w-dyn-bind-empty'); 114 } 115 if (p) { 116 p.textContent = app.app_description || ''; 117 p.classList.remove('w-dyn-bind-empty'); 118 } 119 if (descContainer.replaceChildren) descContainer.replaceChildren(desc); 120 else { descContainer.innerHTML = ''; descContainer.appendChild(desc); } 121 122 // Image / video â build a fresh item with ONLY the medium we need. 123 if (videoContainer && videoTemplate) { 124 var v = videoTemplate.cloneNode(true); 125 var img = v.querySelector('img'); 126 var vid = v.querySelector('video'); 127 128 if (vid && app.video_url) { 129 if (img && img.parentNode) img.parentNode.removeChild(img); 130 vid.setAttribute('src', app.video_url); 131 vid.setAttribute('data-src', app.video_url); 132 vid.style.display = 'block'; 133 try { vid.load(); var pr = vid.play(); if (pr && pr.catch) pr.catch(function(){}); } catch(e) {} 134 } else if (img && app.image_url) { 135 if (vid && vid.parentNode) vid.parentNode.removeChild(vid); 136 img.setAttribute('src', app.image_url); 137 img.removeAttribute('srcset'); 138 img.style.display = 'block'; 139 img.classList.remove('w-dyn-bind-empty'); 140 } else { 141 if (img && img.parentNode) img.parentNode.removeChild(img); 142 if (vid && vid.parentNode) vid.parentNode.removeChild(vid); 143 } 144 if (videoContainer.replaceChildren) videoContainer.replaceChildren(v); 145 else { videoContainer.innerHTML = ''; videoContainer.appendChild(v); } 146 } 147 } 148 149 150 showApp(0); 151 } 152 153 function run() { 154 fetchApps() 155 .then(function (apps) { 156 var filtered = apps.filter(function (a) { return audienceFilter(a.page_visibility); }); 157 PANELS.forEach(function (p) { 158 var el = document.getElementById(p.id); 159 var subset = filtered.filter(function (a) { return a.category === p.category; }); 160 renderPanel(el, subset, p); 161 }); 162 }) 163 .catch(function (e) { console.error('[apps-cms]', e); }); 164 } 165 166 if (document.readyState === 'loading') { 167 document.addEventListener('DOMContentLoaded', run); 168 } else { 169 run(); 170 } 171})();
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.