PageSourceSearch

https://homezero.ai/js/apps-cms.js

js homezero.ai collected 2026-10-05 16:08:55 UTC 7,204 bytes, 171 lines download raw bytes

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.