1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <link rel="icon" href="/_genesis/assets/favicon" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 8 9
10<script type="module" src="/assets/main-QGCEE7RJ.js"></script>
10 11 12 13<link rel="stylesheet" href="/assets/main-EKZ3JE57.css" /> 14 <title>Grocery Buddy â Built with Taskade</title> 15 16 <style> 17 /* Genesis spinner styles */ 18 .genesis__spinner-container { 19 position: fixed; 20 inset: 0; 21 background: #000000; 22 display: flex; 23 align-items: center; 24 justify-content: center; 25 z-index: 9999; 26 } 27 28 .genesis__spinner-ring { 29 width: 20px; 30 height: 20px; 31 border: 1.5px solid transparent; 32 border-top: 1.5px solid rgba(255, 255, 255, 0.6); 33 border-radius: 50%; 34 animation: genesis__spin 1s linear infinite; 35 } 36 37 @keyframes genesis__spin { 38 0% { transform: rotate(0deg); } 39 100% { transform: rotate(360deg); } 40 } 41 42 /* Respect reduced motion preferences */ 43 @media (prefers-reduced-motion: reduce) { 44 .genesis__spinner-ring { 45 animation: none; 46 border-top-color: rgba(255, 255, 255, 0.9); 47 } 48 } 49 </style> 50 <meta charset="utf-8" /><meta name="language" content="en-US" /><meta name="robots" content="index" /><meta name="author" content="Taskade" /><link rel="icon" sizes="32x32" href="/_genesis/assets/favicon" /><link rel="apple-touch-icon" sizes="180x180" href="/_genesis/assets/favicon" /><meta name="title" content="Grocery Buddy â Built with Taskade" /><meta property="og:title" content="Grocery Buddy â Built with Taskade" /><meta name="twitter:title" content="Grocery Buddy â Built with Taskade" /><meta name="description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta property="og:description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta name="twitter:description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta property="og:type" content="website" /><meta property="og:url" content="https://grocery-buddy-7497.taskade.app" /><meta property="og:site_name" content="Taskade" /><meta property="og:image" content="https://grocery-buddy-7497.taskade.app/_internal/screenshot" /><meta property="og:image:alt" content="Grocery Buddy â Built with Taskade - A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and w..." /><meta property="og:locale" content="en_US" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:site" content="@taskade" /><meta name="twitter:creator" content="@taskade" /><meta name="twitter:image" content="https://grocery-buddy-7497.taskade.app/_internal/screenshot" /><meta name="twitter:image:alt" content="Grocery Buddy â Built with Taskade - A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and w..." /><meta name="format-detection" content="telephone=no" /><meta name="mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-status-bar-style" content="default" /><meta name="apple-mobile-web-app-title" content="Grocery Buddy â Built with Taskade" /><meta name="application-name" content="Taskade" /><link rel="canonical" href="https://grocery-buddy-7497.taskade.app" />
50<script type="application/ld+json"> 51 { 52 "@context": "https://schema.org", 53 "@type": "WebSite", 54 "name": "Taskade", 55 "alternateName": "Grocery Buddy â Built with Taskade", 56 "description": "A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move.", 57 "image": "https://grocery-buddy-7497.taskade.app/_internal/screenshot", 58 "url": "https://grocery-buddy-7497.taskade.app", 59 "author": { 60 "@type": "Organization", 61 "name": "Taskade" 62 }, 63 "publisher": { 64 "@type": "Organization", 65 "name": "Taskade", 66 "url": "https://grocery-buddy-7497.taskade.app", 67 "logo": { 68 "@type": "ImageObject", 69 "url": "https://grocery-buddy-7497.taskade.app/_internal/screenshot" 70 } 71 } 72 } 73</script>
73<script defer src="/_genesis/vendor/flock.min.js" data-proxy-url="/_genesis/api/events" data-tenant-id="2q8n4wr162yenloc:default" data-domain="grocery-buddy-7497.taskade.app"></script>
73<script defer src="/_genesis/vendor/scroll-guard.js"></script>
73</head> 74 <body>
74<script> 75 (function() { 76 if (window.__genesisErrorCatchAll) return; 77 window.__genesisErrorCatchAll = true; 78 var esc = function(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }; 79 var errorHtml = function(msg, id) { 80 return '<div style="position:fixed;inset:0;background:#000;padding:2rem;font-family:system-ui,sans-serif;color:#fff;overflow:auto">' + 81 '<div style="max-width:600px;margin:0 auto">' + 82 '<h2 style="margin:0 0 0.5rem;font-size:1.25rem">Something went wrong</h2>' + 83 '<p style="color:#999;margin:0 0 1rem;font-size:0.95rem">The app failed to load. Try refreshing the page.</p>' + 84 '<pre style="background:#1a1a1a;padding:1rem;border-radius:8px;overflow:auto;font-size:0.8rem;color:#f87171;margin:0;white-space:pre-wrap;word-break:break-word">' + 85 esc(msg) + '<\/pre>' + 86 '<p style="color:#666;margin:1rem 0 0;font-size:0.75rem">Error ID: <code>' + esc(id) + '<\/code><\/p>' + 87 '<\/div><\/div>'; 88 }; 89 var reported = false; 90 var errorId = null; 91 function getErrorId() { 92 if (errorId != null) return errorId; 93 try { 94 if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { errorId = crypto.randomUUID().replace(/-/g, ''); return errorId; } 95 } catch (e) { /* Fall through to the time+random id below */ } 96 errorId = Date.now().toString(36) + Math.random().toString(36).slice(2, 12); 97 return errorId; 98 } 99 function reportError(code, msg, stack) { 100 if (reported) return; 101 reported = true; 102 try { 103 var logger = window.__TASKADE_APP_LIFECYCLE_LOGGER__; 104 if (typeof logger?.log === 'function') { 105 logger.log({ level: 'error', message: 'Runtime Error', data: { code: code, message: msg, stack: stack || '' } }); 106 return; 107 } 108 // Published: no logger, so beacon to the Taskade gateway instead of 109 // dropping the crash on the floor (#27626). Relative path â the 110 // Director proxy injects the gateway token server-side. Once per page 111 // session (the latch above) so a crash loop cannot flood the sink. 112 fetch('/api/taskade/telemetry/errors', { 113 method: 'POST', 114 headers: { 'Content-Type': 'application/json' }, 115 body: JSON.stringify({ 116 errorId: getErrorId(), 117 code: code, 118 message: String(msg || 'Unknown error').slice(0, 4000), 119 stack: String(stack || '').slice(0, 20000), 120 path: location.pathname.slice(0, 1000),
121 userAgent: navigator.userAgent.slice(0, 512) 122 }), 123 keepalive: true 124 // Default same-origin credentials: a password-protected app gates 125 // /api/taskade/* on its signed access cookie, so omitting them 126 // would redirect the beacon to the password page. 127 }).catch(function() {}); 128 } catch (e) { /* Swallow â never block the fallback error UI */ } 129 } 130 function showError(code, msg, stack) { 131 var root = document.getElementById('root'); 132 if (!root) return; 133 var spinner = root.querySelector('.genesis__spinner-container'); 134 if (spinner) { 135 // Scenario 1: React never mounted â replace spinner 136 reportError('error.fatal', msg, stack); 137 spinner.outerHTML = errorHtml(msg, getErrorId()); 138 } else if (!root.childElementCount) { 139 // Scenario 2: React crashed and unmounted â #root is empty 140 reportError('error.fatal', msg, stack); 141 root.innerHTML = errorHtml(msg, getErrorId()); 142 } else { 143 // React is running fine â don't interfere with the DOM, 144 // but still report with the original code for diagnostics 145 reportError(code, msg, stack); 146 } 147 } 148 window.addEventListener('error', function(e) { 149 var msg = e.message || String(e.error) || 'Unknown error'; 150 var stack = e.error && e.error.stack ? e.error.stack : (e.filename ? e.filename + ':' + e.lineno + ':' + e.colno : ''); 151 showError('error.runtime_error', msg, stack); 152 }); 153 window.addEventListener('unhandledrejection', function(e) { 154 var msg = e.reason ? String(e.reason) : 'Unhandled promise rejection'; 155 var stack = e.reason && e.reason.stack ? e.reason.stack : ''; 156 showError('error.unhandled_promise', msg, stack); 157 }); 158 // A dead <img> degrades to a neutral placeholder instead of painting its 159 // raw alt string across the page (#27730) â the symptom half of the 160 // fabricated-hot-link fix, and the only half that helps an ALREADY 161 // published app, whose images can rot long after generation. 162 // 163 // Resource load errors do not bubble, so they reach window only in the 164 // CAPTURE phase. That is why neither handler above has ever seen one, and 165 // why this cannot disturb them: a broken image must never render the fatal 166 // error overlay or spend the once-per-session crash beacon. 167 // 168 // Swapping in a 1x1 transparent GIF is what actually stops the alt text â 169 // the element becomes a SUCCESSFUL load, so the browser has no broken image 170 // to caption, while the alt attribute stays for assistive tech. Layout is 171 // untouched: the app's own width/height/aspect classes still apply. The grey 172 // is translucent so it reads correctly on a light or a dark surface without 173 // this script having to know the app's theme. 174 var deadImagePixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; 175 window.addEventListener('error', function(e) { 176 var el = e.target; 177 if (!el || el.tagName !== 'IMG') return; 178 // The swap fires a fresh load on the same element, so the latch is what 179 // keeps a failing data URI from looping. 180 if (el.getAttribute('data-genesis-image-fallback') !== null) return; 181 el.setAttribute('data-genesis-image-fallback', ''); 182 el.removeAttribute('srcset'); 183 el.src = deadImagePixel; 184 el.style.background = 'linear-gradient(135deg, rgba(127,127,127,0.16), rgba(127,127,127,0.28))'; 185 }, true); 186 187 // --------------------------------------------------------------- 188 // Dead env reads (advisory, NOT a failure) 189 // --------------------------------------------------------------- 190 // The bundle's import.meta.env / process.env shims are reporting 191 // Proxies (see genesisEnvBanner.ts). They are value-preserving: a 192 // missing key still reads undefined. This turns that read from invisible 193 // into observable, because a feature gated on one is permanently off and 194 // has no other symptom â the app builds, boots and looks healthy. 195 // 196 // Deliberately WARN, never error. The runtime cannot see whether a call 197 // site has a working fallback, so a correct 198 // VITE_API_BASE_URL || 'https://real' is reported too. Warn keeps it
199 // out of the "Fix with AI" popup set and out of FATAL_LIFECYCLE_CODES, 200 // both of which are error-level allowlists. 201 var deadEnvSeen = {}; 202 var deadEnvSent = 0; 203 // Its OWN budget and latch: sharing the 'reported' latch above would let 204 // an advisory notice spend the one crash report this page session gets. 205 var DEAD_ENV_MAX = 5; 206 function reportDeadEnv(source, key) { 207 try { 208 var signature = source + '.' + key; 209 if (deadEnvSeen[signature] || deadEnvSent >= DEAD_ENV_MAX) return; 210 deadEnvSeen[signature] = 1; 211 deadEnvSent++; 212 var msg = signature + ' is read but a Genesis build never defines it, so the value is ' + 213 'always undefined. If it gates a feature, that feature is permanently off. Route ' + 214 'secret-bearing calls through GenesisClient.proxy() with a secretAlias instead.'; 215 var logger = window.__TASKADE_APP_LIFECYCLE_LOGGER__; 216 if (typeof logger?.log === 'function') { 217 logger.log({ level: 'warn', message: 'Dead env read', data: { code: 'warn.dead_env', message: msg } }); 218 return; 219 } 220 fetch('/api/taskade/telemetry/errors', { 221 method: 'POST', 222 headers: { 223 'Content-Type': 'application/json', 224 // Lets the publish Worker pick the advisory rate-limit bucket 225 // without reading this body. Doing that at the edge means I/O 226 // of unbounded duration ahead of the limiter meant to bound it, 227 // which is the whole class of problem the header avoids. The 228 // origin still classifies from the validated body, so this is a 229 // bucket hint and never the record (#29164). 230 'X-Genesis-Telemetry-Code': 'warn.dead_env' 231 }, 232 body: JSON.stringify({ 233 errorId: 'de' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8), 234 code: 'warn.dead_env', 235 message: msg.slice(0, 4000), 236 path: location.pathname.slice(0, 1000), 237 // The source and key as STRUCTURE, not only inside the prose. 238 // The gateway fingerprints on app + code alone, because this 239 // endpoint's token ships in the bundle and free text from it 240 // cannot be trusted; these two validated fields are how the 241 // key still reaches Sentry, as a tag. 242 // Sliced to the schema's bound like every other field here: the 243 // banner filters the key's SHAPE but not its length, and an 244 // over-long key would fail validation and lose the whole 245 // report rather than just the tag. 246 envSource: source, 247 envKey: String(key).slice(0, 64) 248 }), 249 keepalive: true 250 }).catch(function() {}); 251 } catch (e) { /* Swallow â advisory telemetry must never break an app */ } 252 } 253 window.__genesisReportDeadEnv = reportDeadEnv; 254 // Drain anything the bundle's banner queued. This script is a classic 255 // inline script and the bundle is a deferred module, so in practice the 256 // reporter wins the race; the queue is what makes that not a matter of 257 // luck (and covers an app whose HTML predates this script). 258 try { 259 var queued = window.__genesisDeadEnvQueue; 260 if (queued) { 261 window.__genesisDeadEnvQueue = []; 262 for (var qi = 0; qi < queued.length; qi++) reportDeadEnv(queued[qi][0], queued[qi][1]); 263 } 264 } catch (e) { /* Ignore. */ } 265 })(); 266</script>
266 267 <div id="root"> 268 <div class="genesis__spinner-container"> 269 <div class="genesis__spinner-ring"></div> 270 </div> 271 </div> 272
272<script> 273 (function() { 274 if (typeof window === 'undefined' || typeof document === 'undefined') { 275 return; 276 } 277 if (typeof window.gtag === 'function') { 278 return; 279 } 280 window.dataLayer = window.dataLayer || []; 281 function gtag(){window.dataLayer.push(arguments);} 282 window.gtag = gtag; 283 gtag('js', new Date()); 284 gtag('config', 'G-WWJTNN1SFE'); 285 const script = document.createElement('script'); 286 script.async = true; 287 script.src = 'https://www.googletagmanager.com/gtag/js?id=G-WWJTNN1SFE'; 288 document.head.appendChild(script); 289 })(); 290</script>
290 291 292<style> 293 @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap'); 294 295 :root { 296 /* Onboarding Black Glass - optimized for dark mode */ 297 --badge-black: rgba(0, 0, 0, 0.75); 298 --badge-black-hover: rgba(0, 0, 0, 0.85); 299 --badge-text: rgba(255, 255, 255, 0.9); 300 --badge-text-hover: rgba(255, 255, 255, 1); 301 --badge-border: rgba(255, 255, 255, 0.15); 302 --badge-border-hover: rgba(255, 255, 255, 0.25); 303 --badge-shadow: rgba(0, 0, 0, 0.4); 304 305 /* Rainbow gradient colors from Taskade Upgrade button */ 306 --rainbow-color-1: #5cb2f9; /* Blue */ 307 --rainbow-color-2: #8787f7; /* Purple-Blue */ 308 --rainbow-color-3: #a774f7; /* Purple */ 309 --rainbow-color-4: #e376a8; /* Pink */ 310 --rainbow-color-5: #eb7551; /* Orange */ 311 --rainbow-color-6: #19bd59; /* Green */ 312 --rainbow-color-7: #dee325; /* Yellow-Green */ 313 --rainbow-color-8: #e32545; /* Red */ 314 315 /* Cinematic easing */ 316 --ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1); 317 --ease-cinematic-slow: cubic-bezier(0.16, 1, 0.3, 1); 318 } 319 320 [data-theme="light"] { 321 --badge-black: rgba(0, 0, 0, 0.8); 322 --badge-black-hover: rgba(0, 0, 0, 0.9); 323 --badge-text: rgba(255, 255, 255, 0.95); 324 --badge-text-hover: rgba(255, 255, 255, 1); 325 --badge-border: rgba(255, 255, 255, 0.2); 326 --badge-border-hover: rgba(255, 255, 255, 0.3); 327 --badge-shadow: rgba(0, 0, 0, 0.5); 328 } 329 330 @media (prefers-color-scheme: light) { 331 :root { 332 --badge-black: rgba(0, 0, 0, 0.8); 333 --badge-black-hover: rgba(0, 0, 0, 0.9); 334 --badge-text: rgba(255, 255, 255, 0.95); 335 --badge-text-hover: rgba(255, 255, 255, 1); 336 --badge-border: rgba(255, 255, 255, 0.2); 337 --badge-border-hover: rgba(255, 255, 255, 0.3); 338 --badge-shadow: rgba(0, 0, 0, 0.5); 339 } 340 } 341 342 /* Onboarding Black Glass Badge - Bottom Center Position */ 343 #taskade-badge { 344 position: fixed; 345 bottom: 24px; 346 left: 50%; 347 transform: translateX(-50%) scale(0.9); 348 z-index: 10001; 349 font-family: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif; 350 animation: badgeGeistFadeIn 1.2s var(--ease-cinematic) forwards; 351 opacity: 0; 352 display: inline-flex; 353 align-items: center; 354 gap: 7px; 355 padding: 7px 12px; 356 font-size: 0.8125rem; 357 font-weight: 500; 358 letter-spacing: -0.01em; 359 line-height: 1; 360 color: var(--badge-text); 361 text-decoration: none; 362 background: var(--badge-black); 363 border: 1px solid var(--badge-border); 364 border-radius: 9999px; 365 backdrop-filter: blur(20px) saturate(1.4); 366 -webkit-backdrop-filter: blur(20px) saturate(1.4); 367 cursor: pointer; 368 transition: all 0.3s var(--ease-cinematic); 369 overflow: hidden; 370 will-change: transform, box-shadow; 371 white-space: nowrap; 372 } 373 374 #taskade-badge a { 375 display: flex; 376 align-items: center; 377 gap: 7px; 378 color: inherit; 379 text-decoration: none; 380 } 381 382 /* Default state with subtle rainbow glow */ 383 #taskade-badge { 384 box-shadow: 385 0 4px 16px var(--badge-shadow), 386 0 0 8px rgba(227, 118, 168, 0.2), 387 0 0 16px rgba(163, 116, 247, 0.15); 388 } 389 390 /* Black glass hover with enhanced rainbow glow */ 391 #taskade-badge:hover { 392 background: var(--badge-black-hover); 393 border-color: var(--badge-border-hover); 394 color: var(--badge-text-hover); 395 box-shadow: 396 0 8px 24px var(--badge-shadow), 397 0 0 16px rgba(227, 118, 168, 0.4), 398 0 0 32px rgba(163, 116, 247, 0.3), 399 inset 0 1px 0 rgba(255, 255, 255, 0.12); 400 transform: translateY(-3px) scale(1.03); 401 } 402 403 /* Taskade sheep mascot - optimized for dark mode */ 404 #taskade-badge #taskade-logo { 405 width: 16px; 406 height: 16px; 407 flex-shrink: 0; 408 opacity: 0.95; 409 transition: all 0.3s var(--ease-cinematic); 410 filter: invert(1) brightness(1.1) contrast(1.05); 411 } 412 413 #taskade-badge:hover #taskade-logo { 414 opacity: 1; 415 transform: scale(1.08) rotate(-2deg); 416 filter: invert(1) brightness(1.2) contrast(1.1); 417 } 418 419 /* Light mode adjustments */ 420 [data-theme="light"] #taskade-badge #taskade-logo { 421 filter: invert(0) brightness(1.1); 422 } 423 424 [data-theme="light"] #taskade-badge:hover #taskade-logo { 425 filter: invert(0) brightness(1.2); 426 } 427 428 @media (prefers-color-scheme: light) { 429 :root #taskade-badge #taskade-logo { 430 filter: invert(0) brightness(1.1); 431 } 432 433 :root #taskade-badge:hover #taskade-logo { 434 filter: invert(0) brightness(1.2); 435 } 436 } 437 438 /* Geist typography */ 439 #taskade-badge .badge-text { 440 font-weight: 500; 441 letter-spacing: 0.005em; 442 transition: all 0.3s var(--ease-cinematic); 443 -webkit-font-smoothing: antialiased; 444 -moz-osx-font-smoothing: grayscale; 445 } 446 447 #taskade-badge.hidden { 448 display: none; 449 } 450 451 /* Geist fade-in animation */ 452 @keyframes badgeGeistFadeIn { 453 0% { 454 opacity: 0; 455 transform: translateX(-50%) translateY(8px) scale(0.85); 456 filter: blur(6px); 457 } 458 60% { 459 opacity: 0.7; 460 filter: blur(2px); 461 } 462 100% { 463 opacity: 1; 464 transform: translateX(-50%) translateY(0) scale(0.9); 465 filter: blur(0); 466 } 467 } 468 469 /* Responsive */ 470 @media (max-width: 768px) { 471 #taskade-badge { 472 bottom: 80px; /* Above mobile nav/TAA chat */ 473 left: 50%; 474 transform: translateX(-50%) scale(0.85); 475 padding: 6px 10px; 476 font-size: 0.75rem; 477 gap: 5px; 478 } 479 480 #taskade-badge #taskade-logo { 481 width: 14px; 482 height: 14px; 483 } 484 } 485 486 /* Reduced motion */ 487 @media (prefers-reduced-motion: reduce) { 488 #taskade-badge { 489 animation: none; 490 opacity: 1; 491 } 492 493 #taskade-badge a, 494 #taskade-badge #taskade-logo, 495 #taskade-badge .badge-text { 496 transition: none; 497 animation: none; 498 } 499 } 500 501 /* High contrast */ 502 @media (prefers-contrast: high) { 503 #taskade-badge a { 504 border-width: 2px; 505 border-color: currentColor; 506 } 507 } 508 509 /* Hide when printing */ 510 @media print { 511 #taskade-badge { 512 display: none !important; 513 } 514 } 515</style> 516 517<div id="taskade-badge" data-badge-mode="edit"> 518 <a href="https://www.taskade.com/app/zvblrbdj1i4g8le2?utm_source=genesis_badge&utm_medium=app_badge&utm_campaign=p
518owered_by&utm_content=zvblrbdj1i4g8le2" target="_blank" rel="noopener noreferrer"> 519 <img id="taskade-logo" src="./_assets/logo.png" alt="Taskade" /> 520 <span class="badge-text" data-community-text="Edit in Taskade" data-default-text="Powered by Taskade">Edit in Taskade</span> 521 </a> 522</div> 523
524<script> 525 (function() { 526 // Geist theme detection - matching wait.html 527 function applyTheme() { 528 const urlParams = new URLSearchParams(window.location.search); 529 let theme = urlParams.get('theme'); 530 531 if (!theme) { 532 try { 533 if (window.parent !== window) { 534 const parentTheme = window.parent.document.documentElement.getAttribute('data-theme'); 535 if (parentTheme) theme = parentTheme; 536 } 537 } catch (e) {} 538 539 if (!theme) { 540 theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; 541 } 542 } 543 544 document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark'); 545 } 546 547 // Apply theme immediately 548 applyTheme(); 549 550 // Theme change listener 551 window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => { 552 const urlParams = new URLSearchParams(window.location.search); 553 if (!urlParams.get('theme')) { 554 document.documentElement.setAttribute('data-theme', e.matches ? 'light' : 'dark'); 555 } 556 }); 557 558 // Analytics tracking and postMessage communication 559 const badgeLink = document.querySelector('#taskade-badge a'); 560 if (badgeLink) { 561 let parentOrigin = '*'; 562 let parentHostname = ''; 563 if (document.referrer) { 564 try { 565 const referrerUrl = new URL(document.referrer); 566 parentOrigin = referrerUrl.origin; 567 parentHostname = referrerUrl.hostname; 568 } catch (err) { 569 parentOrigin = '*'; 570 parentHostname = ''; 571 } 572 } 573 // Taskade itself is framing us (the share page at /app/{id}, which 574 // iframes the app). parentOrigin stays '*' when there is no usable 575 // referrer, and then the framer is unknowable. 576 const framedByTaskade = 577 parentOrigin !== '*' && 578 (parentHostname === 'taskade.com' || parentHostname.endsWith('.taskade.com')); 579 580 const badge = document.getElementById('taskade-badge'); 581 const badgeText = document.querySelector('#taskade-badge .badge-text'); 582 const communityText = badgeText ? badgeText.getAttribute('data-community-text') : null; 583 const badgeHref = badgeLink.getAttribute('href') || ''; 584 const badgeMode = badge ? badge.getAttribute('data-badge-mode') : null; 585 const isCommunityShare = 586 badgeMode === 'edit' || 587 (badgeText && 588 communityText && 589 badgeText.textContent && 590 badgeText.textContent.trim() === communityText) || 591 badgeHref.indexOf('/app/') !== -1; 592 593 if (window.parent !== window) { 594 // Hide only when embedded: on the standalone domain this badge IS the 595 // viral return path into Taskade, so it must stay visible there. 596 // Scope note, two independent reasons to hide inside a frame: 597 // 1. The Taskade share page frames the app and already renders its own 598 // Clone CTA, so ANY badge mode is a duplicate there. A buyer of a 599 // non-template listing stays on /app/{id} and sees exactly that. 600 // 2. An edit-mode badge hides in any frame, because its /app/{id} link 601 // just points back at the framing page. 602 // A powered_by badge in an unrelated third-party embed still renders - 603 // there it is the only way back to Taskade. With no referrer the 604 // framer is unknowable, so only the mode-based reason applies. 605 if ((isCommunityShare || framedByTaskade) && badge) { 606 badge.classList.add('hidden'); 607 } 608 609 const notifyParent = () => { 610 window.parent.postMessage( 611 { 612 type: 'TASKADE_BADGE_MODE', 613 source: 'taskade_badge', 614 mode: isCommunityShare ? 'edit' : 'powered_by', 615 }, 616 parentOrigin 617 ); 618 }; 619 620 try { 621 notifyParent(); 622 setTimeout(notifyParent, 500); 623 } catch (err) { 624 // Ignore postMessage failures. 625 } 626 } 627 628 badgeLink.addEventListener('click', function() { 629 // Track badge engagement 630 if (typeof window.gtag === 'function') { 631 window.gtag('event', 'genesis_badge_clicked', { 632 event_category: 'engagement', 633 event_label: 'edit_in_taskade' 634 }); 635 } 636 637 }); 638 } 639 640 })(); 641</script>
vendor: 1 bytes, line 641
641
642<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"8d4254f20c524f6fbf6ecde0b2c89fb4","spa":2}' crossorigin="anonymous"></script>
642 643</body> 644</html>
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.