PageSourceSearch

https://fakeperfection.com/js/site.js

js fakeperfection.com collected 2026-09-27 07:21:53 UTC 13,194 bytes, 331 lines download raw bytes

1// Reveal-on-scroll for elements with .reveal — no library, IntersectionObserver only.
2(function () {
3  const els = document.querySelectorAll('.reveal');
4  if (!els.length) return;
5  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6  if (!('IntersectionObserver' in window) || reduce) {
7    els.forEach((el) => el.classList.add('in'));
8    return;
9  }
10  const io = new IntersectionObserver((entries) => {
11    entries.forEach((e) => {
12      if (e.isIntersecting) {
13        e.target.classList.add('in');
14        io.unobserve(e.target);
15      }
16    });
17  }, { rootMargin: '0px 0px -10% 0px', threshold: 0.05 });
18  els.forEach((el) => io.observe(el));
19})();
20
21// Countly analytics — async loader from jsDelivr.
22(function () {
23  var Countly = window.Countly = window.Countly || {};
24  Countly.q = Countly.q || [];
25  Countly.app_key = 'b9730c43befcc41a8d78c398b083174e0ac32fd9';
26  Countly.url = 'https://master.count.ly';
27  Countly.q.push(['track_sessions']);
28  Countly.q.push(['track_pageview']);
29  Countly.q.push(['track_clicks']);
30  Countly.q.push(['track_scrolls']);
31  Countly.q.push(['track_links']);
32  Countly.q.push(['track_errors']);
33  Countly.q.push(['track_forms']);
34  Countly.q.push(['enable_apm']);
35  var s = document.createElement('script');
36  s.type = 'text/javascript';
37  s.async = true;
38  s.src = 'https://cdn.jsdelivr.net/npm/countly-sdk-web/lib/countly.min.js';
39  s.onload = function () { window.Countly.init(); };
40  document.head.appendChild(s);
41})();
42
43// Tab switcher — progressive enhancement.
44// Without JS, all [role="tabpanel"] panels stay visible (crawlers index every word).
45// With JS, the first tab's panel is shown; others are `hidden`. Click and ←/→
46// arrow keys switch panels.
47(function () {
48  document.querySelectorAll('[role="tablist"]').forEach(function (list) {
49    var tabs = Array.from(list.querySelectorAll('[role="tab"]'));
50    var panels = tabs
51      .map(function (t) { return document.getElementById(t.getAttribute('aria-controls')); })
52      .filter(Boolean);
53    if (!tabs.length || tabs.length !== panels.length) return;
54
55    // Derive a URL-friendly slug from each tab id (e.g. "tab-lyrics" -> "lyrics").
56    var slugs = tabs.map(function (t) {
57      var id = t.id || '';
58      return id.replace(/^tab-/, '');
59    });
60
61    function show(idx, updateHash) {
62      tabs.forEach(function (t, i) {
63        var sel = i === idx;
64        t.setAttribute('aria-selected', sel ? 'true' : 'false');
65        t.setAttribute('tabindex', sel ? '0' : '-1');
66      });
67      panels.forEach(function (p, i) { p.hidden = (i !== idx); });
68      if (updateHash && history && history.replaceState) {
69        // First tab is the default — drop the hash for a clean URL.
70        var newUrl = idx === 0
71          ? location.pathname + location.search
72          : location.pathname + location.search + '#' + slugs[idx];
73        history.replaceState(null, '', newUrl);
74      }
75    }
76
77    function indexForHash() {
78      var h = (location.hash || '').replace(/^#/, '');
79      if (!h) return -1;
80      return slugs.indexOf(h);
81    }
82
83    var fromHash = indexForHash();
84    var initial = fromHash >= 0
85      ? fromHash
86      : tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; });
87    if (initial < 0) initial = 0;
88    show(initial, false);
89
90    tabs.forEach(function (t, i) {
91      t.addEventListener('click', function () { show(i, true); });
92      t.addEventListener('keydown', function (e) {
93        var next = i;
94        if (e.key === 'ArrowRight') next = (i + 1) % tabs.length;
95        else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
96        else if (e.key === 'Home') next = 0;
97        else if (e.key === 'End') next = tabs.length - 1;
98        else return;
99        e.preventDefault();
100        show(next, true);
101        tabs[next].focus();
102      });
103    });
104
105    // React to back/forward or to a URL pasted directly into the address bar.
106    window.addEventListener('hashchange', function () {
107      var idx = indexForHash();
108      if (idx >= 0) show(idx, false);
109    });
110  });
111})();
112
113// Diptych toggle (mobile-only segmented control on /songs/my-idol/).
114// Flips body class diptych-active-neon ↔ diptych-active-shadow which the CSS
115// uses to show/hide the inactive half on narrow viewports. On desktop the
116// toggle is hidden and both halves are always visible — no JS effect needed.
117(function () {
118  var toggle = document.querySelector('.dip-toggle');
119  if (!toggle) return;
120  var buttons = Array.from(toggle.querySelectorAll('button[data-side]'));
121  if (!buttons.length) return;
122
123  function setActive(side) {
124    document.body.classList.remove('diptych-active-neon', 'diptych-active-shadow');
125    document.body.classList.add('diptych-active-' + side);
126    buttons.forEach(function (b) {
127      b.setAttribute('aria-pressed', b.dataset.side === side ? 'true' : 'false');
128    });
129  }
130
131  // Default to neon on load.
132  setActive('neon');
133
134  buttons.forEach(function (b) {
135    b.addEventListener('click', function () { setActive(b.dataset.side); });
136    b.addEventListener('keydown', function (e) {
137      if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
138      e.preventDefault();
139      var idx = buttons.indexOf(b);
140      var next = (idx + (e.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length;
141      setActive(buttons[next].dataset.side);
142      buttons[next].focus();
143    });
144  });
145})();
146
147// Share button — `[data-action="share"]` tries the Web Share API first
148// (native OS share sheet on mobile and many desktops). On user-dismissed:
149// silent (they chose). On any other failure, or if Web Share isn't here,
150// falls back to clipboard copy with a "Link copied" confirmation. One
151// button, always does something visible if the OS sheet doesn't appear.
152(function () {
153  document.addEventListener("click", function (e) {
154    var btn = e.target.closest && e.target.closest('[data-action="share"]');
155    if (!btn) return;
156    e.preventDefault();
157    var url = location.href;
158    var title = document.title;
159
160    function flashCopied() {
161      var orig = btn.textContent;
162      btn.textContent = "Link copied";
163      btn.classList.add("copied");
164      setTimeout(function () {
165        btn.textContent = orig;
166        btn.classList.remove("copied");
167      }, 1500);
168    }
169
170    function copyLink() {
171      if (navigator.clipboard && navigator.clipboard.writeText) {
172        navigator.clipboard.writeText(url).then(flashCopied, function () {});
173        return;
174      }
175      // very old fallback
176      var ta = document.createElement("textarea");
177      ta.value = url;
178      ta.style.position = "fixed";
179      ta.style.opacity = "0";
180      document.body.appendChild(ta);
181      ta.select();
182      try { document.execCommand("copy"); flashCopied(); } catch (_) {}
183      document.body.removeChild(ta);
184    }
185
186    if (typeof navigator.share === "function") {
187      navigator.share({ title: title, url: url }).then(
188        function () { /* user completed share — sheet did its thing */ },
189        function (err) {
190          // AbortError = user dismissed. Anything else: fall back to copy.
191          if (err && err.name === "AbortError") return;
192          copyLink();
193        }
194      );
195      return;
196    }
197    copyLink();
198  });
199})();
200
201// Copy-link buttons — `.btn-share[data-action="copy"]` copies the page URL to clipboard.
202(function () {
203  document.addEventListener("click", function (e) {
204    var btn = e.target.closest && e.target.closest('.btn-share[data-action="copy"]');
205    if (!btn) return;
206    e.preventDefault();
207    var url = location.href;
208    var orig = btn.textContent;
209    var done = function () {
210      btn.textContent = "Copied";
211      btn.classList.add("copied");
212      setTimeout(function () {
213        btn.textContent = orig;
214        btn.classList.remove("copied");
215      }, 1500);
216    };
217    if (navigator.clipboard && navigator.clipboard.writeText) {
218      navigator.clipboard.writeText(url).then(done, function () {});
219    } else {
220      // very old fallback
221      var ta = document.createElement("textarea");
222      ta.value = url;
223      ta.style.position = "fixed";
224      ta.style.opacity = "0";
225      document.body.appendChild(ta);
226      ta.select();
227      try { document.execCommand("copy"); done(); } catch (_) {}
228      document.body.removeChild(ta);
229    }
230  });
231})();
232
233// Click-to-load YouTube embeds — swap thumbnail for iframe on first click.
234// Avoids loading the YouTube player until the user wants to watch, and lets us
235// use the high-resolution maxresdefault thumbnail (the iframe-served preview is grainy).
236(function () {
237  function activate(box) {
238    if (!box || box.classList.contains('is-playing')) return;
239    var vid = box.getAttribute('data-video');
240    if (!vid) return;
241    var title = box.getAttribute('data-title') || 'Music video';
242    var iframe = document.createElement('iframe');
243    iframe.src = 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(vid) + '?autoplay=1&rel=0';
244    iframe.title = title;
245    iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
246    iframe.setAttribute('allowfullscreen', '');
247    iframe.referrerPolicy = 'strict-origin-when-cross-origin';
248    box.appendChild(iframe);
249    box.classList.add('is-playing');
250  }
251  document.addEventListener('click', function (e) {
252    activate(e.target.closest && e.target.closest('.video-embed[data-video]'));
253  });
254  document.addEventListener('keydown', function (e) {
255    if (e.key !== 'Enter' && e.key !== ' ') return;
256    var box = e.target.closest && e.target.closest('.video-embed[data-video]');
257    if (!box) return;
258    e.preventDefault();
259    activate(box);
260  });
261})();
262
263// Custom-event tracking for outbound links (streaming, social, listen, press kit, medium).
264// Fires Countly add_event with segmentation, on top of Countly.track_links auto-tracking.
265(function () {
266  if (!('addEventListener' in document)) return;
267
268  var TONEDEN_TO_SLUG = {
269    's4a': 'smile-for-the-algorithm',
270    'fp_ol': 'one-life',
271    'fp-nmw': 'not-my-war',
272    'fp_idc': 'i-dont-care'
273  };
274
275  function track(key, segmentation) {
276    var Countly = window.Countly = window.Countly || {};
277    Countly.q = Countly.q || [];
278    Countly.q.push(['add_event', { key: key, count: 1, segmentation: segmentation }]);
279  }
280
281  function classify(url) {
282    var u;
283    try { u = new URL(url); } catch (e) { return null; }
284    var host = u.hostname.replace(/^www\./, '');
285    var page = window.location.pathname;
286
287    // Toneden listen buttons (per-song).
288    if (host === 'fanlink.tv' || host === 'fp.fanlink.tv') {
289      var slug = u.pathname.split('/').filter(Boolean)[0] || '';
290      return { event: 'listen_click', segmentation: { song: TONEDEN_TO_SLUG[slug] || slug, page: page } };
291    }
292
293    // Streaming platforms.
294    if (host === 'open.spotify.com')   return { event: 'streaming_click', segmentation: { platform: 'spotify',        page: page } };
295    if (host === 'music.apple.com')    return { event: 'streaming_click', segmentation: { platform: 'apple_music',    page: page } };
296    if (host === 'music.youtube.com')  return { event: 'streaming_click', segmentation: { platform: 'youtube_music',  page: page } };
297    if (host === 'music.amazon.com')   return { event: 'streaming_click', segmentation: { platform: 'amazon_music',   page: page } };
298    if (host === 'deezer.com')         return { event: 'streaming_click', segmentation: { platform: 'deezer',         page: page } };
299    if (host === 'soundcloud.com')     return { event: 'streaming_click', segmentation: { platform: 'soundcloud',     page: page } };
300
301    // Social.
302    if (host === 'tiktok.com')         return { event: 'social_click', segmentation: { platform: 'tiktok',    page: page } };
303    if (host === 'instagram.com')      return { event: 'social_click', segmentation: { platform: 'instagram', page: page } };
304    if (host === 'youtube.com')        return { event: 'social_click', segmentation: { platform: 'youtube',   page: page } };
305    if (host === 'x.com')              return { event: 'social_click', segmentation: { platform: 'x',         page: page } };
306    if (host === 'facebook.com')       return { event: 'social_click', segmentation: { platform: 'facebook',  page: page } };
307
308    // Medium "Read the full story" buttons on song pages.
309    if (host === 'medium.com')         return { event: 'medium_click',   segmentation: { page: page } };
310
311    // Press kit Drive folder.
312    if (host === 'drive.google.com')   return { event: 'press_kit_click', segmentation: { page: page } };
313
314    return null;
315  }
316
317  document.addEventListener('click', function (e) {
318    var a = e.target.closest && e.target.closest('a[href]');
319    if (!a) return;
320    var href = a.href;
321    if (!href || !/^https?:\/\//.test(href)) return;
322    // Press-mention links carry their own outlet slug for clean segmentation.
323    var press = a.getAttribute('data-press-mention');
324    if (press) {
325      track('press_mention_click', { outlet: press, page: window.location.pathname });
326      return;
327    }
328    var classified = classify(href);
329    if (classified) track(classified.event, classified.segmentation);
330  }, true);
331})();

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.