1<!DOCTYPE html> 2 3<html lang="en" 4 dir="ltr"> 5 6<head> 7 8 <link rel="stylesheet" data-navigate-track 9 href="https://lave.live/css/localization.css?direction=ltr&v=3"> 10 <meta name="csrf-token" content="79B8yq4jw64o4Bw286nioXvdhTpv0lXhgw6jxEaX"> 11 <meta charset="utf-8"> 12 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" /> 13 14 15 <title>Business Plan Pricing | Lave</title> 16<meta name="description" content="View Lave Business plan pricing for online stores, payments, custom domains, staff access, marketing tools, and business management."> 17<link rel="canonical" href="https://lave.live/pricing"> 18<meta property="og:type" content="website"> 19<meta property="og:locale" content="en_US"> 20<meta property="og:site_name" content="Lave"> 21<meta property="og:url" content="https://lave.live/pricing"> 22<meta property="og:title" content="Business Plan Pricing | Lave"> 23<meta property="og:description" content="See the Lave Business plan price and the tools included for selling online and managing your business."> 24<meta property="og:image" content="https://fls-a04cfec4-d445-4c41-8c2c-0bc04adc61a3.laravel.cloud/main_assets/opengraph/index.jpg"> 25<meta name="twitter:card" content="summary_large_image"> 26<meta name="twitter:site" content="@laveappofficial"> 27<meta name="twitter:title" content="Business Plan Pricing | Lave"> 28<meta name="twitter:description" content="View Lave Business plan pricing for online stores, payments, custom domains, staff access, marketing tools, and business management."> 29<meta name="twitter:image" content="https://fls-a04cfec4-d445-4c41-8c2c-0bc04adc61a3.laravel.cloud/main_assets/opengraph/index.jpg"> 30<link rel="alternate" hreflang="x-default" href="https://lave.live/pricing"> 31<link rel="alternate" hreflang="en" href="https://lave.live/pricing"> 32<link rel="alternate" hreflang="fr" href="https://lave.live/fr/pricing"> 33<link rel="alternate" hreflang="ar" href="https://lave.live/ar/pricing"> 34<meta name="keywords" content="Lave pricing, Lave Business plan, sell online Africa, mobile money ecommerce, Lave Ghana pricing, payment fees Africa, Lave store cost, online store pricing Africa">
35<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","name":"Lave","url":"https://lave.live"}</script>
vendor: 1 bytes, line 35
35
36<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","name":"Lave","url":"https://lave.live","logo":"https://lave.live/images/icons/icon-512x512.png"}</script>
vendor: 1 bytes, line 36
36
37<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","name":"Business Plan Pricing | Lave","description":"See the Lave Business plan price and the tools included for selling online and managing your business.","url":"https://lave.live/pricing"}</script>
37 38 <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon/favicon-32x32.png?v=1.0"> 39 <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon/favicon-16x16.png?v=1.0"> 40 <link rel="shortcut icon" href="/images/favicon/favicon-32x32.png?v=1.0"> 41 <link rel="preconnect" href="https://im.zoho.com"> 42 <link rel="preconnect" href="https://static.zohocdn.com"> 43 <link rel="preconnect" href="https://desk.zoho.com"> 44 <link rel="dns-prefetch" href="//im.zoho.com"> 45 <link rel="dns-prefetch" href="//static.zohocdn.com"> 46 <link rel="dns-prefetch" href="//desk.zoho.com"> 47 48 49 50 51 <!-- Web Application Manifest --> 52 53 <link rel="manifest" href="/manifest.json"> 54 55 <!-- Chrome for Android theme color --> 56 <meta name="theme-color" content="white"> 57 58 <!-- Add to homescreen for Chrome on Android --> 59 <meta name="mobile-web-app-capable" content="yes"> 60 <meta name="application-name" content="Lave"> 61 <link rel="icon" sizes="512x512" href="/images/icons/icon-512x512.png?v=1.0"> 62 63 <!-- Add to homescreen for Safari on iOS --> 64 <meta name="apple-mobile-web-app-capable" content="yes"> 65 <meta name="apple-mobile-web-app-status-bar-style" content="white"> 66 <meta name="apple-mobile-web-app-title" content="Lave"> 67 <link rel="apple-touch-icon" href="/images/icons/icon-512x512.png?v=1.0"> 68 69
69<script src="https://cdn.jsdelivr.net/npm/[email protected]/cdn.min.js"></script>
69 70 71
71<script> 72 if (typeof window.iosPWASplash === 'function') { 73 window.iosPWASplash("/images/icons/icon-512x512.png?v=1.0", '#EC4899'); 74 } 75 </script>
75 76 77 <!-- Tile for Win8 --> 78 <meta name="msapplication-TileColor" content="blue"> 79 <meta name="msapplication-TileImage" content="/images/icons/icon-512x512.png?v=1.0"> 80 81 82 83 <link rel="stylesheet" 84 href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/uikit.min.css?v=3" /> 85 86
86<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit.min.js"></script>
86 87
87<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/uikit-icons.min.js"></script>
87 88 89
89<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
89 90 91 92 <link rel="stylesheet" 93 href="https://lave.live/css/tailwind-full.css?v=1790193372-3"> 94 95 96 <!-- Date Range Picker CSS --> 97 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/daterangepicker.css?v=3"> 98 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tom-select/dist/css/tom-select.css?v=3"> 99 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/driver.js/dist/driver.css?v=3"> 100 101 <link 102 href="https://fonts.googleapis.com/css2?family=Acme&family=Aladin&family=Inter&family=Poppins&family=Roboto&family=Manrope&family=Space+Grotesk&family=Plus+Jakarta+Sans&family=Sora&family=Open+Sans&family=Lato&family=Ubuntu&family=Nunito&family=Work+Sans&family=Syne&family=Montserrat&display=swap&v=3" 103 rel="stylesheet"> 104 105 <link rel="preconnect" href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600;700&display=swap" 106 rel="stylesheet"> 107 108 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css?v=3"> 109 110 111
111<script src="https://unpkg.com/[email protected]"></script>
111 112 113
113<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/hugerte.min.js"></script>
113 114 <meta name="facebook-domain-verification" content="zylazptgkc1z0arghjheromt5oq39m" /> 115 <style> 116 html { 117 overscroll-behavior: none; 118 background-color: #f9fafb; 119 scrollbar-gutter: stable; 120 } 121 122 body { 123 font-family: 'Inter', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 124 } 125 126 127 @font-face { 128 font-family: 'Nova Mono'; 129 src: url('https://lave.live/fonts/nova-mono.ttf') format('truetype'); 130 font-weight: normal; 131 font-style: normal; 132 } 133 134 .font-nova-mono { 135 font-family: 'Nova Mono', monospace !important; 136 } 137 138 /* Responsive page header spacing */ 139 .page-header { 140 padding-top: 2.5rem; 141 /* pt-10 for mobile */ 142 } 143 144 @media (min-width: 768px) { 145 .page-header { 146 padding-top: 5rem; 147 /* pt-20 for tablets and laptops */ 148 } 149 } 150 151 152 .uk-notification { 153 z-index: 100 !important; 154 } 155 </style> 156 157 <!-- Meta Pixel Code --> 158
158<script> 159 ! function(f, b, e, v, n, t, s) { 160 if (f.fbq) return; 161 n = f.fbq = function() { 162 n.callMethod ? 163 n.callMethod.apply(n, arguments) : n.queue.push(arguments) 164 }; 165 if (!f._fbq) f._fbq = n; 166 n.push = n; 167 n.loaded = !0; 168 n.version = '2.0'; 169 n.queue = []; 170 t = b.createElement(e); 171 t.async = !0; 172 t.src = v; 173 s = b.getElementsByTagName(e)[0]; 174 s.parentNode.insertBefore(t, s) 175 }(window, document, 'script', 176 'https://connect.facebook.net/en_US/fbevents.js'); 177 window.laveMetaAdvancedMatchingData = {}; 178 window.laveMetaPixelId = "950672977777178"; 179 window.laveMetaPixelOptions = {"testEventCode":"TEST52976"}; 180 window.laveApplyMetaAdvancedMatching = function(source) { 181 if (typeof fbq === 'undefined' || !window.laveMetaPixelId) { 182 return; 183 } 184 185 const data = {}; 186 187 if (source instanceof HTMLFormElement) { 188 const email = source.querySelector('[name="email"]')?.value?.trim().toLowerCase(); 189 190 if (email) { 191 data.em = email; 192 } 193 } else if (source && typeof source === 'object') { 194 Object.assign(data, source); 195 } 196 197 if (!data.em) { 198 return; 199 } 200 201 const signature = JSON.stringify(data); 202 if (window.laveMetaAdvancedMatchingSignature === signature) { 203 return; 204 } 205 206 window.laveMetaAdvancedMatchingSignature = signature; 207 window.laveMetaAdvancedMatchingData = data; 208 209 if (window.laveMetaPixelOptions) { 210 fbq('init', window.laveMetaPixelId, data, window.laveMetaPixelOptions); 211 } else { 212 fbq('init', window.laveMetaPixelId, data); 213 } 214 }; 215 fbq('init', window.laveMetaPixelId, window.laveMetaAdvancedMatchingData, window.laveMetaPixelOptions); 216 </script>
216 217 <!-- End Meta Pixel Code --> 218 219 <!-- Google Tag Manager --> 220
220<script>
vendor: 610 bytes, lines 220-234
220 221 (function(w, d, s, l, i) { 222 w[l] = w[l] || []; 223 w[l].push({ 224 'gtm.start': new Date().getTime(), 225 event: 'gtm.js' 226 }); 227 var f = d.getElementsByTagName(s)[0], 228 j = d.createElement(s), 229 dl = l != 'dataLayer' ? '&l=' + l : ''; 230 j.async = true; 231 j.src = 232 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; 233 f.parentNode.insertBefore(j, f); 234 })(window, document, 'script', 'dataLayer', '
234GTM-WFZBP7S3
vendor: 12 bytes, lines 234-235
234'); 235
235</script>
235 236 <!-- End Google Tag Manager --> 237 <!-- Livewire Styles --><style >[wire\:loading][wire\:loading], [wire\:loading\.delay][wire\:loading\.delay], [wire\:loading\.list-item][wire\:loading\.list-item], [wire\:loading\.inline-block][wire\:loading\.inline-block], [wire\:loading\.inline][wire\:loading\.inline], [wire\:loading\.block][wire\:loading\.block], [wire\:loading\.flex][wire\:loading\.flex], [wire\:loading\.table][wire\:loading\.table], [wire\:loading\.grid][wire\:loading\.grid], [wire\:loading\.inline-flex][wire\:loading\.inline-flex] {display: none;}[wire\:loading\.delay\.none][wire\:loading\.delay\.none], [wire\:loading\.delay\.shortest][wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter][wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short][wire\:loading\.delay\.short], [wire\:loading\.delay\.default][wire\:loading\.delay\.default], [wire\:loading\.delay\.long][wire\:loading\.delay\.long], [wire\:loading\.delay\.longer][wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest][wire\:loading\.delay\.longest] {display: none;}[wire\:offline][wire\:offline] {display: none;}[wire\:dirty]:not(textarea):not(input):not(select) {display: none;}:root {--livewire-progress-bar-color: rgb(231, 20, 111);}[x-cloak="x-cloak"],[x-cloak=""] {display: none !important;}[wire\:cloak="wire:cloak"],[wire\:cloak=""] {display: none !important;}dialog#livewire-error::backdrop {background-color: rgba(0, 0, 0, .6);}</style> 238 <style> 239 .animate-flash { 240 position: relative; 241 overflow: hidden; 242 isolation: isolate; 243 } 244 245 .animate-flash::before { 246 content: ''; 247 position: absolute; 248 inset: 0; 249 z-index: -1; 250 pointer-events: none; 251 background: linear-gradient(110deg, transparent 30%, currentColor 50%, transparent 70%); 252 opacity: 0.5; 253 transform: translateX(-120%); 254 animation: lave-button-shine 3s ease-in-out infinite; 255 } 256 257 @keyframes lave-button-shine { 258 0%, 40% { transform: translateX(-120%); } 259 90%, 100% { transform: translateX(120%); } 260 } 261 262 @media (prefers-reduced-motion: reduce) { 263 .animate-flash::before { animation: none; } 264 } 265 </style> 266
266<script defer src="https://lave.live/vendor/thumbmarkjs/thumbmark.umd.js"></script>
266 267
267<script defer src="https://lave.live/js/browser-fingerprint.js"></script>
267 268</head> 269 270 271<body> 272 273
273<script> 274 fbq('trackSingle', "950672977777178", 'PageView'); 275 </script>
275 276 277 <!-- Live wire progress bar --> 278 <style> 279 #nprogress .bar { 280 height: 5px; 281 background: rgb(231, 20, 111); 282 } 283 </style> 284 285 <noscript><img height="1" width="1" style="display:none" 286 src="https://www.facebook.com/tr?id=950672977777178&ev=PageView&noscript=1" /></noscript> 287 288 <!-- Google Tag Manager (noscript) -->
vendor: 76 bytes, lines 288-289
288 289 <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
289GTM-WFZBP7S3
vendor: 107 bytes, lines 289-291
289" 290 height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> 291
291<!-- End Google Tag Manager (noscript) --> 292 293
293<script src="https://cdnjs.cloudflare.com/ajax/libs/pulltorefreshjs/0.1.22/index.umd.min.js"></script>
293 294 295 296
296<script src="https://cdn.jsdelivr.net/npm/tom-select/dist/js/tom-select.complete.min.js"></script>
296 297 298 299
299<script src="https://cdn.jsdelivr.net/npm/[email protected]/tsparticles.confetti.bundle.min.js"></script>
299 300 301
301<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
301 302
302<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
302 303
303<script src="https://cdn.jsdelivr.net/npm/driver.js/dist/driver.js.iife.js"></script>
303 304 305 306 307 308 <!-- lave brand color = #E7146F --> 309 <div class="progress" role="progressbar" aria-label="Loading" style="height: 5px; background-color: white;"> 310 <div class="indeterminate" style="background-color: rgb(20, 55, 231);"></div> 311 </div> 312 313 <style> 314 .progress { 315 position: fixed; 316 top: 0; 317 height: 15px; 318 width: 100%; 319 border-radius: 8px; 320 background-clip: padding-box; 321 overflow: hidden; 322 z-index: 100; 323 } 324 325 .progress .indeterminate:before { 326 content: ""; 327 position: absolute; 328 background-color: inherit; 329 top: 0; 330 left: 0; 331 bottom: 0; 332 will-change: left, right; 333 -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 334 animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 335 z-index: 100; 336 } 337 338 .progress .indeterminate:after { 339 content: ""; 340 position: absolute; 341 background-color: inherit; 342 top: 0; 343 left: 0; 344 bottom: 0; 345 will-change: left, right; 346 -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 347 animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 348 -webkit-animation-delay: 1.15s; 349 animation-delay: 1.15s; 350 z-index: 100; 351 } 352 353 .progress { 354 display: none; 355 } 356 357 @-webkit-keyframes indeterminate { 358 0% { 359 left: -35%; 360 right: 100%; 361 } 362 363 60% { 364 left: 100%; 365 right: -90%; 366 } 367 368 100% { 369 left: 100%; 370 right: -90%; 371 } 372 } 373 374 @keyframes indeterminate { 375 0% { 376 left: -35%; 377 right: 100%; 378 } 379 380 60% { 381 left: 100%; 382 right: -90%; 383 } 384 385 100% { 386 left: 100%; 387 right: -90%; 388 } 389 } 390 391 @-webkit-keyframes indeterminate-short { 392 0% { 393 left: -200%; 394 right: 100%; 395 } 396 397 60% { 398 left: 107%; 399 right: -8%; 400 } 401 402 100% { 403 left: 107%; 404 right: -8%; 405 } 406 } 407 408 @keyframes indeterminate-short { 409 0% { 410 left: -200%; 411 right: 100%; 412 } 413 414 60% { 415 left: 107%; 416 right: -8%; 417 } 418 419 100% { 420 left: 107%; 421 right: -8%; 422 } 423 } 424 </style> 425 426 427 428 <div wire:snapshot="{"data":{"selectedCountry":"Ghana","selectedCurrency":""},"memo":{"id":"Xh1EWUpXPYNFCt2clzKT","name":"pages::landing.pricing","path":"pricing","method":"GET","release":"a-a-a","children":[],"scripts":[],"assets":[],"errors":[],"locale":"en","islands":[]},"checksum":"67c22cd347ca231a0d2dcff193129787359a08870168334191651bb41656eb6a"}" wire:effects="[]" wire:id="Xh1EWUpXPYNFCt2clzKT" wire:name="pages::landing.pricing"> 429 <div 430 class="lave-progress" 431 wire:loading 432 wire:target="selectedCountry,updateVisitorCountry" 433 role="progressbar" 434 aria-label="Loading" 435> 436 <div class="lave-progress-indeterminate"></div> 437</div> 438 439<style> 440 .lave-progress { 441 position: fixed; 442 top: 0; 443 left: 0; 444 z-index: 1000; 445 display: none; 446 width: 100%; 447 height: 5px; 448 overflow: hidden; 449 background: white; 450 } 451 452 .lave-progress-indeterminate { 453 position: absolute; 454 width: 35%; 455 height: 100%; 456 background: rgb(231, 20, 111); 457 animation: lave-progress 1.5s ease-in-out infinite; 458 } 459
460 @keyframes lave-progress { 461 0% { left: -35%; } 462 100% { left: 100%; } 463 } 464</style> 465 <div class="sticky top-0" style="z-index: 15;"> 466 467 <header class="hidden bg-white shadow-lg lg:block"> 468 <div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-3"> 469 <!-- Logo --> 470 <a href="https://lave.live" wire:navigate class="flex items-center" style="text-decoration: none;"> 471 <span class="text-4xl font-semibold" style="font-family: 'Nova Mono'; color: #eb487f;">lave</span> 472 </a> 473 474 475 <!-- Nav Links --> 476 <nav class="hidden space-x-6 rtl:space-x-reverse text-sm font-medium text-gray-700 lg:flex"> 477 <!-- Features Dropdown --> 478 <div x-data="{ dropdownOpen: false }" x-on:click.outside="dropdownOpen = false" 479 x-on:keydown.escape.window="dropdownOpen = false" class="dropdown-group relative"> 480 <button type="button" x-on:click="dropdownOpen = ! dropdownOpen" 481 x-bind:aria-expanded="dropdownOpen" aria-haspopup="true" 482 class="dropdown-btn flex items-center hover:text-pink-600"> 483 Features 484 <svg class="ms-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 485 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> 486 </svg> 487 </button> 488 <div x-show="dropdownOpen" x-cloak 489 class="dropdown-menu absolute start-0 z-20 mt-3 min-w-[300px] rounded-lg bg-white p-6 shadow-xl transition-all duration-300"> 490 <ul class="space-y-2 text-sm text-gray-700"> 491 <li><a wire:navigate href="https://lave.live/features" class="block hover:text-pink-500">All Features</a></li> 492 <li><a wire:navigate href="https://lave.live/record-sales" class="block hover:text-pink-500">Sales, Expenses & Debts</a></li> 493 <li><a wire:navigate href="https://lave.live/online-store" class="block hover:text-pink-500">Online Store</a></li> 494 <li><a wire:navigate href="https://lave.live/whatsapp-orders" class="block hover:text-pink-500">WhatsApp Orders</a></li> 495 <li><a wire:navigate href="https://lave.live/payments" class="block hover:text-pink-500">Payments</a></li> 496 <li><a wire:navigate href="https://lave.live/inventory" class="block hover:text-pink-500">Products & Inventory</a></li> 497 <li><a wire:navigate href="https://lave.live/marketing" class="block hover:text-pink-500">Marketing Tools</a></li> 498 <li><a wire:navigate href="https://lave.live/plugins" class="block hover:text-pink-500">Plugins & Integrations</a></li> 499 <li class="border-t border-gray-200 pt-3 text-xs font-bold uppercase tracking-wider text-gray-400">More from Lave</li> 500 <li><a wire:navigate href="https://lave.live/advertise" class="block hover:text-pink-500">Advertise Your Brand</a></li> 501 <li><a wire:navigate href="https://lave.live/taskers" class="block hover:text-pink-500">Become a Tasker</a></li> 502 <li><a wire:navigate href="https://lave.live/refer-and-earn" class="block hover:text-pink-500">Refer & Earn</a></li> 503 504 </ul> 505 </div> 506 </div> 507 508 <!-- Pricing --> 509 <a wire:navigate href="https://lave.live/pricing" class="hover:text-pink-600">Pricing</a> 510 511 <!-- resources Dropdown --> 512 <div x-data="{ dropdownOpen: false }" x-on:click.outside="dropdownOpen = false" 513 x-on:keydown.escape.window="dropdownOpen = false" class="dropdown-group relative"> 514 <button type="button" x-on:click="dropdownOpen = ! dropdownOpen" 515 x-bind:aria-expanded="dropdownOpen" aria-haspopup="true" 516 class="dropdown-btn flex items-center hover:text-pink-600"> 517 Resources 518 <svg class="ms-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 519 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> 520 </svg> 521 </button> 522 <div x-show="dropdownOpen" x-cloak 523 class="dropdown-menu absolute start-0 z-20 mt-3 w-48 rounded-lg bg-white p-6 shadow-xl transition-all duration-300"> 524 <ul class="space-y-2 text-sm text-gray-700"> 525 <li><a wire:navigate href="https://lave.live/blog" class="block hover:text-pink-500">Blog</a> 526 </li> 527 <li><a wire:navigate href="https://lave.live/help" class="block hover:text-pink-500">Help</a> 528 </li> 529 <li><a wire:navigate href="https://lave.live/updates" 530 class="block hover:text-pink-500">Updates</a></li> 531 <li><a wire:navigate href="https://lave.live/faqs" class="block hover:text-pink-500">FAQs</a> 532 </li> 533 <li><a href="https://www.youtube.com/@lavedotlive" target="_blank">Videos</a></li> 534 <li><a wire:navigate href="https://lave.live/contact" 535 class="block hover:text-pink-500">Contact</a></li> 536 <li><a wire:navigate href="https://lave.live/about" 537 class="block hover:text-pink-500">About</a></li> 538 </ul> 539 </div> 540 </div> 541 542 543 544 </nav> 545 546 <!-- CTA Button --> 547 <div class="hidden items-center gap-4 lg:flex"> 548 <form method="POST" action="https://lave.live/language"> 549 <input type="hidden" name="_token" value="79B8yq4jw64o4Bw286nioXvdhTpv0lXhgw6jxEaX" autocomplete="off"> <input type="hidden" name="return_to" value="/pricing"> 550 <!--[if BLOCK]><![endif]--> <label class="relative inline-flex min-h-[44px] items-center gap-2 px-3 py-2 text-sm font-bold text-gray-700 transition-colors hover:text-pink-600 focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-offset-2">
551 <span aria-hidden="true" uk-icon="icon: world; ratio: 0.8"></span> 552 <span aria-hidden="true">EN</span> 553 <span aria-hidden="true" uk-icon="icon: chevron-down; ratio: 0.8"></span> 554 <span class="sr-only">Language</span> 555 <select name="locale" class="absolute inset-0 h-full w-full cursor-pointer opacity-0" 556 onchange="this.form.requestSubmit()"> 557 <!--[if BLOCK]><![endif]--> <option value="en" selected>English</option> 558 <option value="fr" >Français</option> 559 <option value="ar" >Ø§ÙØ¹Ø±Ø¨ÙØ©</option> 560 <!--[if ENDBLOCK]><![endif]--> </select> 561 </label> 562 <!--[if ENDBLOCK]><![endif]--></form> 563 <a href="https://lave.live/register" wire:navigate> 564 <button 565 class="rounded-lg bg-pink-500 px-5 py-2 text-sm font-bold text-white transition-colors hover:bg-pink-600"> 566 Login / Register 567 </button> 568 </a> 569 </div> 570 </div> 571 </header> 572</div> 573 574<!-- Mobile Nav --> 575<div x-data="{ mobileMenuOpen: false }" x-effect="document.documentElement.classList.toggle('overflow-hidden', mobileMenuOpen)" 576 x-on:keydown.escape.window="mobileMenuOpen = false" class="sticky top-0 block lg:hidden" style="z-index: 15;"> 577 <!-- Offcanvas Sidebar Wrapper (initially hidden) --> 578 <div x-show="mobileMenuOpen" x-cloak x-transition.opacity style="z-index: 20;" class="fixed inset-0 overscroll-contain overflow-y-auto"> 579 <!-- Overlay --> 580 <div x-on:click="mobileMenuOpen = false" 581 class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 overscroll-contain overflow-y-auto"></div> 582 583 <!-- Fullscreen Sidebar --> 584 585 <div x-transition:enter="transition-transform duration-300 ease-in-out" 586 x-transition:enter-start="-translate-x-full" x-transition:enter-end="translate-x-0" 587 x-transition:leave="transition-transform duration-300 ease-in-out" 588 x-transition:leave-start="translate-x-0" x-transition:leave-end="-translate-x-full" 589 class="fixed inset-0 overflow-y-auto bg-white p-6 shadow-lg overscroll-contain" style="z-index: 20;"> 590 591 <!-- Brand --> 592 <a href="https://lave.live" wire:navigate class="text-4xl font-bold" 593 style="font-family: 'Nova Mono'; color: #eb487f;"> 594 lave 595 </a> 596 597 <!-- Close Button --> 598 <button type="button" x-on:click="mobileMenuOpen = false" 599 class="absolute end-4 top-4 text-2xl text-gray-600 hover:text-black">â</button> 600 601 <!-- Divider --> 602 <div class="my-6 flex items-center px-6"> 603 <div class="flex-grow border-t border-gray-300"></div> 604 <span class="mx-4 text-gray-400">â¯</span> 605 <div class="flex-grow border-t border-gray-300"></div> 606 </div> 607 608 609 <!-- Navigation --> 610 <nav class="mt-0 space-y-10"> 611 <ul class="uk-nav-default space-y-2 text-lg" uk-nav> 612 <!-- Features Dropdown --> 613 <li class="uk-parent"> 614 <a href="#"> 615 <span class="uk-margin-xsmall-right" uk-icon="icon: settings"></span> 616 Features 617 <span uk-nav-parent-icon></span> 618 </a> 619 <ul class="uk-nav-sub space-y-2 ps-4 text-[17px] leading-[1.75rem]"> 620 <li><a wire:navigate href="https://lave.live/features">All Features</a></li> 621 <li><a wire:navigate href="https://lave.live/record-sales">Sales, Expenses & Debts</a></li> 622 <li><a wire:navigate href="https://lave.live/online-store">Online Store</a></li> 623 <li><a wire:navigate href="https://lave.live/whatsapp-orders">WhatsApp Orders</a></li> 624 <li><a wire:navigate href="https://lave.live/payments">Payments</a></li> 625 <li><a wire:navigate href="https://lave.live/inventory">Products & Inventory</a> 626 </li> 627 <li><a wire:navigate href="https://lave.live/marketing">Marketing Tools</a></li> 628 <li><a wire:navigate href="https://lave.live/plugins">Plugins & Integrations</a></li> 629 <li class="uk-nav-divider"></li> 630 <li class="uk-nav-header">More from Lave</li> 631 <li><a wire:navigate href="https://lave.live/advertise">Advertise Your Brand</a> 632 </li> 633 <li><a wire:navigate href="https://lave.live/taskers">Become a Tasker</a></li> 634 <li><a wire:navigate href="https://lave.live/refer-and-earn">Refer & Earn</a></li> 635 636 </ul> 637 </li> 638 639 <!-- Pricing --> 640 <li> 641 <a wire:navigate href="https://lave.live/pricing">
642 <span class="uk-margin-xsmall-right" uk-icon="icon: tag"></span> 643 Pricing 644 </a> 645 </li> 646 647 <li class="uk-parent"> 648 <a href="#"> 649 <span class="uk-margin-xsmall-right" uk-icon="icon: info"></span> 650 Resources 651 <span uk-nav-parent-icon></span> 652 </a> 653 <ul class="uk-nav-sub space-y-2 ps-4 text-[17px] leading-[1.75rem]"> 654 <li><a wire:navigate href="https://lave.live/blog">Blog</a></li> 655 <li><a wire:navigate href="https://lave.live/help">Help</a></li> 656 <li><a wire:navigate href="https://lave.live/updates" 657 class="block hover:text-pink-500">Updates</a> 658 </li> 659 <li><a wire:navigate href="https://lave.live/faqs">FAQs</a></li> 660 <li><a href="https://www.youtube.com/@lavedotlive" target="_blank">Videos</a></li> 661 <li><a wire:navigate href="https://lave.live/contact">Contact</a></li> 662 <li><a wire:navigate href="https://lave.live/about">About</a></li> 663 </ul> 664 </li> 665 666 667 668 <li class="uk-nav-divider"></li> 669 670 <!-- Login/Register --> 671 <li> 672 <a href="https://lave.live/register" wire:navigate> 673 <button class="uk-button uk-button-primary uk-width-1-1 bg-pink-500"> 674 Login / Register 675 </button> 676 </a> 677 </li> 678 </ul> 679 </nav> 680 681 682 </div> 683 684 </div> 685 686 <!-- Mobile Navbar --> 687 688 <nav class="flex items-center justify-between bg-white px-4 py-3 shadow-md"> 689 <!-- Left: Logo --> 690 <div class="p-1 text-pink-500"> 691 <a href="https://lave.live" class="text-2xl font-bold" 692 style="font-family: 'Nova Mono'; color: #eb487f;" wire:navigate> 693 lave 694 </a> 695 </div> 696 697 <div class="flex items-center gap-3"> 698 <!-- Register CTA (Mobile Only) --> 699 700 701 <form method="POST" action="https://lave.live/language"> 702 <input type="hidden" name="_token" value="79B8yq4jw64o4Bw286nioXvdhTpv0lXhgw6jxEaX" autocomplete="off"> <input type="hidden" name="return_to" value="/pricing"> 703 <!--[if BLOCK]><![endif]--> <label class="relative inline-flex min-h-[44px] items-center gap-2 px-3 py-2 text-sm font-bold text-gray-700 transition-colors hover:text-pink-600 focus-within:ring-2 focus-within:ring-pink-500 focus-within:ring-offset-2"> 704 <span aria-hidden="true" uk-icon="icon: world; ratio: 0.8"></span> 705 <span aria-hidden="true">EN</span> 706 <span aria-hidden="true" uk-icon="icon: chevron-down; ratio: 0.8"></span> 707 <span class="sr-only">Language</span> 708 <select name="locale" class="absolute inset-0 h-full w-full cursor-pointer opacity-0" 709 onchange="this.form.requestSubmit()"> 710 <!--[if BLOCK]><![endif]--> <option value="en" selected>English</option> 711 <option value="fr" >Français</option> 712 <option value="ar" >Ø§ÙØ¹Ø±Ø¨ÙØ©</option> 713 <!--[if ENDBLOCK]><![endif]--> </select> 714 </label> 715 <!--[if ENDBLOCK]><![endif]--></form> 716 717 <!-- Hamburger Icon --> 718 <button type="button" x-on:click="mobileMenuOpen = true" class="text-gray-700 lg:hidden"> 719 <svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> 720 <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /> 721 </svg> 722 </button> 723 </div> 724 </nav> 725 726</div> 727 728 729 <div class="bg-white"> 730 731 <section class="bg-pink-50 py-20"> 732 <div class="uk-container mx-auto max-w-6xl px-6 text-center"> 733 <p class="text-6xl font-extrabold leading-tight text-pink-600">Pricing</p> 734 <p class="mx-auto max-w-2xl text-xl text-gray-700"> 735 Simple Affordable Pricing 736 </p> 737 </div> 738 </section> 739 740 741 <section class="uk-container uk-width-1-2@l py-10"> 742 <div class="text-center"> 743 <p>Select country</p> 744 745 <div style="display:inline;"> 746 <div wire:ignore> 747 <div uk-form-custom="target: > * > span:first-child" class="inline-flex"> 748 <select aria-label="Custom controls" wire:model.live="selectedCountry" 749 wire:change="updateVisitorCountry"> 750 <!--[if BLOCK]><![endif]--> <option value="Ghana" selected> 751 Ghana (GHS) 752 </option> 753 <option value="Kenya" > 754 Kenya (KES) 755 </option> 756 <option value="Nigeria" > 757 Nigeria (NGN) 758 </option> 759 <option value="Rwanda" > 760 Rwanda (RWF) 761 </option> 762 <option value="South Africa" > 763 South Africa (ZAR) 764 </option> 765 <option value="Tanzania" > 766 Tanzania (TZS) 767 </option> 768 <option value="Uganda" > 769 Uganda (UGX) 770 </option> 771 <option value="Zambia" > 772 Zambia (ZMW) 773 </option> 774 <!--[if ENDBLOCK]><![endif]--> </select> 775 <button class="uk-button uk-button-default uk-button-small" type="button" tabindex="-1">
776 <span></span> 777 <span uk-icon="icon: chevron-down"></span> 778 </button> 779 </div> 780 </div> 781 </div> 782 783 </div> 784 785 <div> <br> 786 <p class="uk-text-small"> With Lave, pricing depends on your plan.</p> 787 <br> 788 789 <p class="uk-text-small"> <b>Collection fee</b> - This can come in the form of payment links sales or website sales. </p><br> 790 <p class="uk-text-small"> <b>Withdrawal fee</b> - Our payment partners charge you to transfer funds to your bank account / Mobile money wallet 791 </p> 792 <br> 793 794 <p class="uk-text-small"> <b>Exchange / swap currency</b> - This is the amount charged to swap one currency to another. It is completely free</p> 795 796 <br> 797 798 <p class="uk-text-small">Cap simply means, we wont charge you more than the stated amount</p><br> 799 800 801 <P>Collection = 802 1.95% 803 <!--[if BLOCK]><![endif]--> + 804 GHS 1 805 <!--[if ENDBLOCK]><![endif]--> <!--[if BLOCK]><![endif]--> capped at GHS 806 50 807 <!--[if ENDBLOCK]><![endif]--> </P> 808 809 <P>Withrawal = 810 1% 811 812 <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <!--[if BLOCK]><![endif]--> capped at GHS 813 50 814 <!--[if ENDBLOCK]><![endif]--> </P> 815 816 817 <br> 818 <p>International Fees = Our Payment partners charge 819 7% for every international Payment processed</p> 820 821 <hr class="uk-divider-icon"> 822 823 <p class="mt-3 text-center text-lg font-bold"> 824 Get started for free. Upgrade when you are ready to go live. 825 </p> 826 827 <div class="uk-child-width-1-1 uk-grid-small uk-grid-match uk-padding-small" uk-grid> 828 829 830 831 832 833 <div class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm sm:p-8"> 834 <h2 class="m-0 text-2xl font-bold tracking-tight text-gray-900">Business plan</h2> 835 <p class="mt-1 text-sm text-gray-600">All your business tools. Choose how long you want to pay for.</p> 836 <p class="mt-2 text-sm font-semibold text-gray-700">Minimum 3 months, paid upfront.</p> 837 <div class="mt-8 grid grid-cols-1 gap-5 md:grid-cols-3" aria-label="Business billing options"> 838 <!--[if BLOCK]><![endif]--> <div class="relative flex flex-col rounded-xl border px-5 py-7 border-gray-200 bg-white"> 839 <h3 class="m-0 text-lg font-semibold text-gray-800">3 months</h3> 840 <!--[if BLOCK]><![endif]--> <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <p class="mt-5 text-3xl font-bold tracking-tight text-gray-900">GHS 150</p> 841 842 <p class="mt-2 text-sm font-semibold text-pink-600">GHS 50/month</p> 843 <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <!--[if ENDBLOCK]><![endif]--> </div> 844 <div class="relative flex flex-col rounded-xl border px-5 py-7 border-gray-200 bg-white"> 845 <h3 class="m-0 text-lg font-semibold text-gray-800">6 months</h3> 846 <!--[if BLOCK]><![endif]--> <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <p class="mt-5 text-3xl font-bold tracking-tight text-gray-900">GHS 270</p> 847 848 <p class="mt-2 text-sm font-semibold text-pink-600">GHS 45/month</p> 849 <!--[if BLOCK]><![endif]--> <div class="mt-4 flex flex-wrap items-center gap-2">
850 <span class="text-sm text-red-600 line-through" aria-label="Cost when paying every 3 months">GHS 300</span> 851 <span class="inline-flex rounded bg-green-600 px-3 py-1 text-xs font-bold text-white">Save GHS 30</span> 852 </div> 853 <!--[if ENDBLOCK]><![endif]--> <!--[if ENDBLOCK]><![endif]--> </div> 854 <div class="relative flex flex-col rounded-xl border px-5 py-7 border-pink-300 bg-pink-50"> 855 <h3 class="m-0 text-lg font-semibold text-gray-800">1 year</h3> 856 <!--[if BLOCK]><![endif]--> <!--[if BLOCK]><![endif]--> <span class="absolute -top-3 right-4 rounded-full bg-pink-600 px-3 py-1 text-xs font-bold text-white">Best value</span> 857 <!--[if ENDBLOCK]><![endif]--> <p class="mt-5 text-3xl font-bold tracking-tight text-gray-900">GHS 480</p> 858 859 <p class="mt-2 text-sm font-semibold text-pink-600">GHS 40/month</p> 860 <!--[if BLOCK]><![endif]--> <div class="mt-4 flex flex-wrap items-center gap-2"> 861 <span class="text-sm text-red-600 line-through" aria-label="Cost when paying every 3 months">GHS 600</span> 862 <span class="inline-flex rounded bg-green-600 px-3 py-1 text-xs font-bold text-white">Save GHS 120</span> 863 </div> 864 <!--[if ENDBLOCK]><![endif]--> <!--[if ENDBLOCK]><![endif]--> </div> 865 <!--[if ENDBLOCK]><![endif]--> </div> 866 <a href="https://lave.live/dashboard/store/subscriptions" wire:navigate 867 class="mt-6 inline-flex items-center justify-center rounded-lg bg-pink-600 px-6 py-3 font-semibold text-white no-underline hover:bg-pink-700 hover:text-white">Select plan</a> 868 <ul class="uk-accordion-default mt-8 border-t border-gray-200 pt-6" uk-accordion="active: 0"> 869 <li> 870 <a class="uk-accordion-title" href="#"> 871 <span class="text-base font-semibold text-blue-600">View features</span> 872 <span wire:ignore uk-accordion-icon></span> 873 </a> 874 <div class="uk-accordion-content uk-text-small"> 875 <p>âï¸ Everything you need to start selling online</p> 876 <p>âï¸ Add Variants</p> 877 <p>âï¸ Online payments</p> 878 <p>âï¸ Custom Payment (create your own payment instructions)</p> 879 <p>âï¸ Cash on delivery payment method</p> 880 <p>âï¸ Buy on Whatsapp payment method</p> 881 <p>âï¸ free <b>mystore.oursite.store</b> subdomain</p> 882 <p>âï¸ Analytics / business report</p> 883 <p>âï¸ Advanced SEO (coming soon)</p> 884 <p>âï¸ Facebook/instagram shop</p> 885 <p>âï¸ upload unlimited products</p> 886 <p>âï¸ upload Product Videos</p> 887 <p>âï¸ Product forms</p> 888 <p>âï¸ Product sections</p> 889 <p>âï¸ Multi-currency payments (12+ currencies)</p> 890 <p>âï¸ Multiple Themes</p> 891 <p>âï¸ connect domain name (eg: 892 <b>www.mystore.com</b>) 893 <span class="uk-text-small">(Domain name cost not included)</span> 894 </p> 895 <p>âï¸ Add store managers / staff</p> 896 <p>âï¸ Bundles</p> 897 <p>âï¸ Newsletter</p> 898 <p>âï¸ Product reviews </p> 899 <p>âï¸ Facebook Pixel </p> 900 <p>âï¸ Meta Conversions API </p> 901 <p>âï¸ TikTok Pixel </p> 902 <p>âï¸ Custom meta tags </p> 903 <p>âï¸ Product & Cart countdown Timer</p> 904 <p>âï¸ Abandoned cart recovery (automatically send emails to customers who added to cart but didnt complete purchase)</p> 905 <p>âï¸ Sales pages (coming soon)</p> 906 <p>âï¸ Store Branches / Location (coming soon)</p> 907 <p>âï¸ Google Tag Manager</p> 908 909 <p>âï¸ Export orders,transactions to excel, csv etc (coming soon)</p> 910 <p>âï¸ Setup your own affiliate / referral program (coming soon) </p> 911 </div> 912 </li> 913 </ul> 914 </div> 915 </div> 916 <br> 917 918 </div> 919 </section> 920 </div> 921 922 <hr> 923<footer class="bg-gray-50 px-6 pb-6 pt-12 sm:px-12"> 924 <!-- CTA Section --> 925 926 <div class="mx-auto max-w-7xl"> 927 <div class="mb-12 rounded-xl bg-pink-700 px-6 py-12 text-center text-white"> 928 <p class="mb-2 text-3xl font-bold sm:text-4xl">Run and grow your business with Lave</p> 929 <p class="mb-6 text-lg">Manage your sales, customers, payments, and online storeâall from one connected app.</p> 930 <div class="flex flex-col items-center justify-center gap-4 sm:flex-row"> 931 <a href="https://lave.live/register" wire:navigate 932 class="rounded-lg bg-white px-6 py-3 font-semibold text-pink-700 transition hover:bg-gray-100"> 933 Get Started 934 </a> 935 <a wire:navigate href="https://lave.live/download" 936 class="rounded-lg border-2 border-white px-6 py-3 font-semibold text-white transition hover:bg-white hover:text-pink-700"> 937 Download App 938 </a> 939 </div> 940 </div> 941 </div> 942 943 944 <!-- Main Footer Grid --> 945 <div class="mx-auto grid max-w-7xl grid-cols-1 gap-8 text-sm sm:grid-cols-2 md:grid-cols-4"> 946 <!-- Brand + Social --> 947 <div class="pb-4"> 948 <p class="mb-4 text-4xl font-semibold" style="font-family: 'Nova Mono'; color: #eb487f;">lave</p> 949 950 <p class="mb-4">The easiest way to create your online store and grow your business across Africa.</p> 951 <div class="mt-2 flex space-x-4 rtl:space-x-reverse"> 952 <a href="https://instagram.com/laveappofficial" target="_blank" aria-label="Instagram" 953 uk-icon="icon: instagram" class="text-xl text-gray-700 hover:text-purple-700"></a> 954 <a href="https://tiktok.com/@laveappofficial" target="_blank" aria-label="tiktok" uk-icon="icon: tiktok" 955 class="text-xl text-gray-700 hover:text-purple-700"></a> 956 <a href="https://x.com/laveappofficial" target="_blank" aria-label="Twitter" uk-icon="icon: twitter" 957 class="text-xl text-gray-700 hover:text-purple-700"></a> 958 <a href="https://facebook.com/laveappofficial" target="_blank" aria-label="Facebook" 959 uk-icon="icon: facebook" class="text-xl text-gray-700 hover:text-purple-700"></a> 960 <a href="https://www.youtube.com/@lavedotlive" target="_blank" aria-label="YouTube" 961 uk-icon="icon: youtube" class="text-xl text-gray-700 hover:text-purple-700"></a> 962 </div> 963 </div> 964 965 <!-- Company --> 966 <div> 967 <div class="mb-4 font-semibold text-gray-900">Company</div> 968 <ul class="space-y-2"> 969 <li><a wire:navigate href="https://lave.live/about" class="hover:underline">About Us</a></li> 970 <li><a wire:navigate href="https://lave.live/pricing" class="hover:underline">Pricing</a></li> 971 <li><a wire:navigate href="https://lave.live/contact" class="hover:underline">Contact</a></li> 972 </ul> 973 </div> 974 975 <!-- Resources --> 976 977 <div> 978 <div class="mb-4 font-semibold text-gray-900">Resources</div> 979 <ul class="space-y-2"> 980 981 <li><a wire:navigate href="https://lave.live/blog" class="hover:underline">Blogs</a></li> 982 <li><a wire:navigate href="https://lave.live/faqs" class="hover:underline">FAQs</a></li> 983 <li><a href="https://www.youtube.com/@lavedotlive" target="_blank" class="hover:underline">Videos</a> 984 </li> 985 </ul> 986 </div> 987 988 <!-- Legal --> 989 <div> 990 <div class="mb-4 font-semibold text-gray-900">Legal</div> 991 <ul class="space-y-2"> 992 <li><a wire:navigate href="https://lave.live/terms" class="hover:underline">Terms & Conditions</a></li> 993 <li><a wire:navigate href="https://lave.live/privacy" class="hover:underline">Privacy Policy</a></li> 994 </ul> 995 </div> 996 </div> 997 998 <div class="mx-auto mt-8 max-w-7xl"> 999 <hr class="m-0 border-gray-200"> 1000 <p class="mb-0 pt-6 text-center text-xs text-gray-500">© 2026 Lave. All rights reserved.</p> 1001 </div> 1002</footer> 1003 1004</div> 1005 1006 <div x-persist="website-assistant"> 1007 <div>
1008 <div wire:key="lw-3437951620-0-3" wire:snapshot="{"data":{"question":"","visitorToken":"4Vmexu1V2JFvmiDqrKGBVi0VcPE1GuTai3XZvHni","conversationId":null,"failure":""},"memo":{"id":"N8T34BWmoznQjlDUHMvK","name":"website-assistant","path":"pricing","method":"GET","release":"a-a-a","children":[],"scripts":["34428343-0"],"assets":[],"errors":[],"locale":"en","islands":[]},"checksum":"aeba19d946c0ba3ceac2b1b7848fda8d81b8239a536b3064218401cb70a11e44"}" wire:effects="{"scripts":{"34428343-0":"<script>\n Alpine.data('websiteAssistantChat', () => ({\n chatModalOpen: false,\n sending: false,\n stopped: false,\n cancelReply: null,\n unsubscribe: null,\n messageObserver: null,\n destroyed: false,\n init() {\n let visible = false;\n this.messageObserver = new ResizeObserver(([entry]) => {\n const nowVisible = entry.contentRect.height > 0 && entry.contentRect.width > 0;\n if (nowVisible && !visible) this.scrollToLatest();\n visible = nowVisible;\n });\n this.messageObserver.observe(this.$refs.messages);\n this.unsubscribe = this.$wire.intercept('send', ({ action, onFinish }) => {\n this.stopped = false;\n this.sending = true;\n this.scrollToLatest();\n this.cancelReply = () => action.cancel();\n onFinish(() => {\n this.sending = false;\n this.cancelReply = null;\n });\n });\n },\n scrollToQuestion(pending = false) {\n this.$nextTick(() => {\n requestAnimationFrame(() => {\n if (this.destroyed) return;\n const container = this.$refs.messages;\n const question = [...container.querySelectorAll('[data-message-role=\"user\"]')].at(-1);\n if (!question) return;\n const padding = parseFloat(getComputedStyle(container).paddingTop) || 0;\n const top = container.scrollTop + question.getBoundingClientRect().top - container.getBoundingClientRect().top - padding;\n container.scrollTo({ top: Math.max(0, top), behavior: 'instant' });\n });\n });\n },\n scrollToLatest() {\n this.$nextTick(() => {\n requestAnimationFrame(() => {\n if (!this.destroyed) this.$refs.messages.scrollTo({ top: this.$refs.messages.scrollHeight, behavior: 'instant' });\n });\n });\n },\n stopReply() {\n if (!this.sending || !this.cancelReply) return;\n this.stopped = true;\n this.cancelReply();\n },\n destroy() {\n this.destroyed = true;\n this.cancelReply?.();\n this.unsubscribe?.();\n this.messageObserver?.disconnect();\n },\n }));\n<\/script>\n "}}" wire:id="N8T34BWmoznQjlDUHMvK" wire:name="website-assistant" x-data="websiteAssistantChat" x-on:assistant-support-opened="chatModalOpen = false" x-on:keydown.escape.window="chatModalOpen = false" x-on:website-assistant-answered.window="scrollToQuestion()"> 1009 <button type="button" class="fixed bottom-[88px] end-4 z-10 flex h-14 w-14 cursor-pointer items-center justify-center overflow-hidden rounded-full border-0 bg-pink-500 p-0 text-white shadow-lg hover:bg-pink-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-700" x-ref="launcher" aria-label="Need help? Open Lave AI Assistant" x-bind:aria-expanded="chatModalOpen" x-on:click="chatModalOpen = !chatModalOpen; if(chatModalOpen) $nextTick(() => $refs.question.focus())"><span uk-icon="icon: comment; ratio: 1.5" aria-hidden="true"></span></button>
1010 <section class="fixed bottom-[156px] end-4 z-40 flex h-[min(590px,calc(100dvh-165px))] min-h-[280px] w-96 max-w-[calc(100vw-24px)] flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white text-sm text-gray-700 shadow-xl max-md:inset-0 max-md:z-[1000] max-md:h-dvh max-md:min-h-0 max-md:w-full max-md:max-w-none max-md:rounded-none max-md:border-0 [&_button:disabled]:cursor-wait [&_button:disabled]:opacity-50 [&_button:focus-visible]:outline [&_button:focus-visible]:outline-2 [&_button:focus-visible]:outline-offset-2 [&_button:focus-visible]:outline-pink-700" role="dialog" aria-label="Lave AI Assistant" x-show="chatModalOpen" x-cloak> 1011 <header class="flex shrink-0 items-center justify-between gap-2 border-b border-gray-200 bg-white px-4 py-3 max-md:pt-[max(0.75rem,env(safe-area-inset-top))]"> 1012 <div class="flex min-w-0 items-center gap-2"><span class="shrink-0 text-xl text-pink-700" aria-hidden="true">â¦</span><div class="min-w-0"><strong class="block text-sm text-gray-800">Lave AI Assistant</strong><small class="block text-xs text-gray-500">A little help getting started</small></div></div> 1013 <button type="button" class="shrink-0 rounded px-2 text-2xl text-gray-600 hover:bg-gray-100" aria-label="Close assistant" x-on:click="chatModalOpen = false;
1013 $refs.launcher.focus()">Ã</button> 1014 </header> 1015 <div class="flex shrink-0 items-center justify-between gap-3 border-b border-gray-100 bg-gray-100 px-4 py-2.5"> 1016 <button type="button" class="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-gray-600 bg-gray-600 px-3 py-2 text-xs font-medium text-white transition hover:border-gray-700 hover:bg-gray-700" wire:click="startAgain" x-on:click="stopped = false" x-bind:disabled="sending" wire:loading.attr="disabled" wire:target="send,startAgain"><span aria-hidden="true" class="text-base leading-none">+</span> New chat</button> 1017 <button type="button" 1018 class="inline-flex shrink-0 items-center gap-1 rounded text-xs font-bold !no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 px-1 py-2 !text-gray-800 hover:text-gray-950 focus-visible:outline-gray-600" 1019 x-on:click="if (typeof window.openZohoChat === 'function') { window.openZohoChat($event); $dispatch('assistant-support-opened'); }" 1020> 1021 <span>Talk to a human</span> 1022</button> 1023 </div> 1024 <div class="min-h-0 min-w-0 flex-1 overflow-auto overscroll-contain px-5 py-4" x-ref="messages" wire:navigate:scroll aria-live="polite" role="log"> 1025 <!--[if BLOCK]><![endif]--> <div class="flex flex-col gap-2.5"> 1026 <h2 class="my-3 text-xl text-gray-700">What do you need help with?</h2> 1027 <p class="leading-relaxed text-gray-500">Type your question below.</p> 1028 </div> 1029 <!--[if ENDBLOCK]><![endif]--> <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <p x-show="stopped" x-cloak class="text-xs text-gray-500" role="status">Reply stopped.</p> 1030 <p x-show="sending" x-cloak class="inline-flex items-center gap-2 text-xs text-pink-700" role="status"><span class="shrink-0" uk-spinner="ratio: 0.45" aria-hidden="true"></span><span>Checking that for youâ¦</span></p> 1031 </div> 1032 <form wire:submit="send" class="shrink-0 border-t border-gray-100 bg-white p-3 max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))]"> 1033 <label class="sr-only" for="website-ai-question">Your question</label> 1034 <div class="flex items-end gap-[10px] border border-[#e5dbe9] bg-gray-50 p-[10px] focus-within:border-[#cc79a7]"><textarea class="min-h-0 min-w-0 w-full resize-none border-0 bg-transparent p-[3px] text-sm leading-[1.6] text-[#423448] outline-none focus:ring-0 max-md:text-base" id="website-ai-question" x-ref="question" wire:model="question" x-bind:readonly="sending" rows="2" maxlength="1000" placeholder="Ask about Laveâ¦" required></textarea><button x-show="!sending" type="submit" class="min-h-[40px] shrink-0 rounded-[10px] border-0 bg-[#bd3c80] px-[15px] py-[10px] text-xs text-white hover:bg-pink-700" wire:loading.attr="disabled" wire:target="send,startAgain"><span wire:loading wire:target="send" uk-spinner="ratio: 0.4"></span>Send</button><button type="button" x-show="sending" x-cloak x-on:click="stopReply" class="min-h-[40px] shrink-0 rounded-[10px] border-0 bg-[#bd3c80] px-[15px] py-[10px] text-xs text-white hover:bg-pink-700" aria-label="Stop generating reply">Stop</button></div> 1035 <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <!--[if BLOCK]><![endif]--><!--[if ENDBLOCK]><![endif]--> <small class="mt-2 block text-[10px] leading-normal text-gray-500">AI answers can be mistaken. Chats are saved for continuity.</small> 1036 </form> 1037 </section> 1038</div> 1039 1040 1041 1042 </div> 1043 </div> 1044 1045 1046
1046<script> 1047 (() => { 1048 if (window.laveCanPullToRefresh) return; 1049 1050 let gestureBlocked = false; 1051 1052 function isVisible(element) { 1053 if (element.closest('[hidden], [inert], [aria-hidden="true"], [x-cloak]')) return false; 1054 1055 const style = getComputedStyle(element); 1056 const rect = element.getBoundingClientRect(); 1057 1058 return style.display !== 'none' && style.visibility !== 'hidden' 1059 && rect.width > 0 && rect.height > 0 1060 && rect.bottom > 0 && rect.right > 0 1061 && rect.top < innerHeight && rect.left < innerWidth; 1062 } 1063 1064 function hasOpenOverlay() { 1065 if (typeof window.isPageScrollLocked === 'function' && window.isPageScrollLocked()) return true; 1066 1067 return Array.from(document.querySelectorAll( 1068 '[data-pull-refresh-overlay], [role="dialog"], [aria-modal="true"], dialog[open], ' 1069 + '.uk-modal.uk-open, .uk-offcanvas.uk-open, .fixed.inset-0' 1070 )).some(isVisible); 1071 } 1072 1073 function startsInScrollArea(target) { 1074 for (let element = target; element instanceof Element && element !== document.body 1075 && element !== document.documentElement; element = element.parentElement) { 1076 const style = getComputedStyle(element); 1077 1078 // Contained areas keep their gestures even at the top or with short content. 1079 if (['contain', 'none'].includes(style.overscrollBehaviorY) 1080 || (/auto|scroll/.test(style.overflowY) && element.scrollHeight > element.clientHeight)) { 1081 return true; 1082 } 1083 } 1084 1085 return false; 1086 } 1087 1088 document.addEventListener('touchstart', event => { 1089 gestureBlocked = event.touches.length !== 1 || hasOpenOverlay() 1090 || startsInScrollArea(event.target); 1091 }, { capture: true, passive: true }); 1092 1093 // Keep the decision through touchend and any delayed refresh callback. 1094 document.addEventListener('touchcancel', () => { gestureBlocked = true; }, { passive: true }); 1095 document.addEventListener('livewire:navigating', () => { gestureBlocked = true; }); 1096 1097 window.laveCanPullToRefresh = () => !gestureBlocked && !window.scrollY && !hasOpenOverlay(); 1098 })(); 1099 1100 function initPullToRefresh() { 1101 if (!window.PullToRefresh || typeof window.PullToRefresh.init !== 'function') { 1102 return; 1103 } 1104 1105 if (window.__lavePullToRefresh) { 1106 window.__lavePullToRefresh.destroy(); 1107 window.__lavePullToRefresh = null; 1108 } 1109 1110 const pullToRefreshDisabledPaths = [ 1111 '/dashboard/store/products/bulk-upload', 1112 '/dashboard/store/products/bulk-upload/csv', 1113 '/dashboard/store/products/bulk-edit', 1114 ]; 1115 const currentPath = window.location.pathname.replace(/\/$/, ''); 1116 1117 if (currentPath.includes('/sort') || pullToRefreshDisabledPaths.includes(currentPath)) { 1118 return; 1119 } 1120 1121 window.__lavePullToRefresh = window.PullToRefresh.init({ 1122 mainElement: 'body', 1123 shouldPullToRefresh: function() { 1124 1125 return window.laveCanPullToRefresh(); 1126 }, 1127 onRefresh: function() { 1128 if (window.laveCanPullToRefresh()) window.location.reload(true); 1129 } 1130 }); 1131 } 1132 1133 if (document.readyState === 'loading') { 1134 document.addEventListener('DOMContentLoaded', initPullToRefresh, { 1135 once: true 1136 }); 1137 } else { 1138 initPullToRefresh(); 1139 } 1140 1141 document.addEventListener('livewire:navigated', initPullToRefresh); 1142 </script>
1142 1143 1144 1145 1146
1146<script> 1147 function goback() { 1148 if (window.history.length > 1) { 1149 window.history.back(); 1150 } else { 1151 window.location.href = "/home/admin"; 1152 } 1153 } 1154 </script>
1154 1155 1156 1157 1158 <!-- ALL Faq script --> 1159
1159<script> 1160 function bindFaqToggleEvents() { 1161 const toggles = document.querySelectorAll('.faq-toggle'); 1162 1163 toggles.forEach(toggle => { 1164 if (!toggle.dataset.bound) { 1165 toggle.addEventListener('click', () => { 1166 const item = toggle.closest('.faq-item'); 1167 const content = item.querySelector('.faq-content'); 1168 const icon = toggle.querySelector('.plus'); 1169 1170 // Collapse other items 1171 document.querySelectorAll('.faq-content').forEach(c => { 1172 if (c !== content) { 1173 c.style.maxHeight = null; 1174 c.parentElement.classList.remove('active'); 1175 c.previousElementSibling.querySelector('.plus').textContent = '+'; 1176 } 1177 }); 1178 1179 // Toggle current 1180 if (content.style.maxHeight) { 1181 content.style.maxHeight = null; 1182 icon.textContent = '+'; 1183 } else { 1184 content.style.maxHeight = content.scrollHeight + "px"; 1185 icon.textContent = 'â'; 1186 } 1187 }); 1188 1189 toggle.dataset.bound = "true"; // Prevent duplicate bindings 1190 } 1191 }); 1192 } 1193 1194 // Initial bind 1195 document.addEventListener('DOMContentLoaded', bindFaqToggleEvents); 1196 document.addEventListener('livewire:navigated', bindFaqToggleEvents); 1197 </script>
1197 1198 1199 1200 1201
1201<script> 1202 if ('serviceWorker' in navigator) { 1203 let refreshing = false; 1204 let lastUpdateCheckAt = 0; 1205 const minUpdateIntervalMs = 30 * 60 * 1000; // 30 minutes 1206 1207 const maybeUpdate = (reg, reason) => { 1208 const now = Date.now(); 1209 1210 if (document.hidden) { 1211 return; 1212 } 1213 1214 if (now - lastUpdateCheckAt < minUpdateIntervalMs) { 1215 return; 1216 } 1217 1218 lastUpdateCheckAt = now; 1219 1220 if (reason) { 1221 console.log(`[SW] ${reason} - checking for updates...`); 1222 } 1223 1224 reg.update(); 1225 }; 1226 1227 // Register service worker 1228 navigator.serviceWorker.register('/serviceworker.js', { 1229 updateViaCache: 'none' // Always check for updates, never use cached service worker 1230 }) 1231 .then(reg => { 1232 console.log('[SW] Service worker registered:', reg.scope); 1233 1234 // IMMEDIATE: Check for updates on every page load 1235 maybeUpdate(reg, 'App load'); 1236 1237 // Check for updates when page becomes visible (app opened) 1238 document.addEventListener('visibilitychange', () => { 1239 if (!document.hidden) { 1240 if (Date.now() - lastUpdateCheckAt < minUpdateIntervalMs) { 1241 return; 1242 } 1243 1244 lastUpdateCheckAt = Date.now(); 1245 console.log('[SW] App visible - checking for updates...'); 1246 reg.update(); 1247 } 1248 }); 1249 1250 // Check for updates when app comes to foreground (PWA) 1251 window.addEventListener('focus', () => { 1252 if (Date.now() - lastUpdateCheckAt < minUpdateIntervalMs) { 1253 return; 1254 } 1255 1256 lastUpdateCheckAt = Date.now(); 1257 console.log('[SW] App focused - checking for updates...'); 1258 reg.update(); 1259 }); 1260 1261 // Check for updates every 24 hours (background safety check) 1262 // Most updates are caught by: app load, visibility change, and focus events above 1263 setInterval(() => { 1264 maybeUpdate(reg, 'Background check'); 1265 }, 24 * 60 * 60 * 1000); // 24 hours 1266 1267 // Listen for new service worker found 1268 reg.addEventListener('updatefound', () => { 1269 const newWorker = reg.installing; 1270 if (newWorker) { 1271 console.log('[SW] New service worker version detected!'); 1272 newWorker.addEventListener('statechange', () => { 1273 if (newWorker.state === 'installed') { 1274 if (navigator.serviceWorker.controller) { 1275 // New service worker is ready 1276 console.log('[SW] New version installed, activating...'); 1277 // Service worker will auto-activate due to skipWaiting() 1278 } else { 1279 // First time installation 1280 console.log('[SW] Service worker installed for first time'); 1281 } 1282 } 1283 }); 1284 } 1285 }); 1286 }) 1287 .catch(err => console.error('[SW] Registration failed:', err)); 1288 1289 // Also check for updates if service worker is already registered (for returning users) 1290 window.addEventListener('load', () => { 1291 if (Date.now() - lastUpdateCheckAt < minUpdateIntervalMs) { 1292 return; 1293 } 1294 1295 lastUpdateCheckAt = Date.now(); 1296 navigator.serviceWorker.getRegistration().then(reg => { 1297 if (reg) { 1298 console.log('[SW] Checking for updates on app load...');
1299 reg.update(); 1300 } 1301 }); 1302 }); 1303 1304 // Handle controller change (new service worker activated) 1305 navigator.serviceWorker.addEventListener('controllerchange', () => { 1306 if (!refreshing) { 1307 refreshing = true; 1308 console.log('[SW] New version activated - reloading page...'); 1309 window.location.reload(); 1310 } 1311 }); 1312 } 1313 </script>
1313 1314 1315 1316 1317 <style> 1318 body { 1319 /* Keep horizontal safe-area support without creating a visible top gap */ 1320 padding-top: 0; 1321 padding-bottom: 0; 1322 padding-left: env(safe-area-inset-left); 1323 padding-right: env(safe-area-inset-right); 1324 } 1325 1326 1327 .safe-area { 1328 padding-bottom: max(15px, env(safe-area-inset-bottom)); 1329 z-index: 10; 1330 } 1331</style> 1332 1333 1334 <div> 1335 1336 1337 <div id="contactPickerModal" class="fixed inset-0 hidden items-center justify-center bg-gray-900 bg-opacity-50 px-4" 1338 style="z-index: 60;"> 1339 <div class="relative w-full max-w-sm overflow-hidden rounded-2xl bg-white shadow-2xl"> 1340 <!-- Header with X button --> 1341 <div class="flex items-center justify-between border-b border-gray-200 px-5 py-4"> 1342 <div id="contactNameHeader" class="truncate text-lg font-semibold text-gray-900"> 1343 Select Phone Number 1344 </div> 1345 <button id="closeModalPhone" type="button" 1346 class="flex h-8 w-8 items-center justify-center rounded-full text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"> 1347 <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1348 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 1349 d="M6 18L18 6M6 6l12 12" /> 1350 </svg> 1351 </button> 1352 </div> 1353 1354 <!-- Contact list with fixed max height --> 1355 <div id="contactList" class="max-h-96 divide-y divide-gray-100 overflow-y-auto"></div> 1356 </div> 1357 </div> 1358 1359 1360
1360<script data-navigate-once> 1361 /** 1362 * Initialize custom headers for all HTTP requests 1363 * This allows your Laravel server to detect if the request is from the Capacitor app 1364 */ 1365 async function initAppHeaders() { 1366 if (!window.Capacitor) { 1367 console.log('? Capacitor not detected - running in web browser'); 1368 return; 1369 } 1370 1371 console.log('ðµ Capacitor detected! Initializing app headers...'); 1372 1373 try { 1374 const { 1375 Device, 1376 App 1377 } = window.Capacitor.Plugins; 1378 1379 // Get app and device info 1380 const appInfo = await App.getInfo(); 1381 const deviceInfo = await Device.getInfo(); 1382 1383 // Store app info globally 1384 window._appHeaders = { 1385 'X-Capacitor-App': 'true', 1386 'X-App-Version': appInfo.version, 1387 'X-App-Build': appInfo.build, 1388 'X-App-Platform': deviceInfo.platform, // 'ios' or 'android' 1389 'X-Device-Model': deviceInfo.model, 1390 }; 1391 1392 console.log('? App headers initialized:', window._appHeaders); 1393 1394 // Intercept fetch requests to add headers 1395 const originalFetch = window.fetch; 1396 window.fetch = function(...args) { 1397 const [url, config = {}] = args; 1398 1399 // Only add headers to requests to your domain 1400 if (typeof url === 'string' && (url.includes('lave.live') || url.startsWith('/'))) { 1401 config.headers = { 1402 ...config.headers, 1403 ...window._appHeaders 1404 }; 1405 } 1406 1407 return originalFetch(url, config); 1408 }; 1409 1410 // Intercept XMLHttpRequest (for HTMX and other libraries) 1411 const originalOpen = XMLHttpRequest.prototype.open; 1412 const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; 1413 1414 XMLHttpRequest.prototype.open = function(...args) { 1415 this._url = args[1]; 1416 return originalOpen.apply(this, args); 1417 }; 1418 1419 XMLHttpRequest.prototype.setRequestHeader = function(header, value) { 1420 // Store original headers 1421 if (!this._customHeaders) { 1422 this._customHeaders = {}; 1423 } 1424 this._customHeaders[header] = value; 1425 return originalSetRequestHeader.apply(this, arguments); 1426 }; 1427 1428 // Add app headers before send 1429 const originalSend = XMLHttpRequest.prototype.send; 1430 XMLHttpRequest.prototype.send = function(...args) { 1431 // Only add headers to requests to your domain 1432 if (this._url && (typeof this._url === 'string' && (this._url.includes('lave.live') || this._url 1433 .startsWith('/')))) { 1434 for (const [header, value] of Object.entries(window._appHeaders)) { 1435 originalSetRequestHeader.call(this, header, value); 1436 } 1437 } 1438 return originalSend.apply(this, args); 1439 }; 1440 1441 console.log('? Request interceptors initialized - All requests will include app headers'); 1442 1443 } catch (error) { 1444 console.error('? Failed to initialize app headers:', error); 1445 alert('? Error initializing app headers: ' + error.message); 1446 } 1447 } 1448 1449 // Initialize as early as possible 1450 if (document.readyState === 'loading') { 1451 document.addEventListener('DOMContentLoaded', initAppHeaders); 1452 } else { 1453 initAppHeaders(); 1454 } 1455 </script>
1455 1456 1457 1458 1459 1460 1461
1461<script data-navigate-once> 1462 /** 1463 * Lock text zoom to 1.0 (100%) to prevent system font size from affecting the app 1464 * This ensures your app always uses your default font size 1465 */ 1466 async function lockTextZoom() { 1467 // Wait for Capacitor to be available 1468 if (!window.Capacitor) { 1469 setTimeout(lockTextZoom, 100); 1470 return; 1471 } 1472 1473 // Wait for TextZoom plugin to be available 1474 if (!window.Capacitor.Plugins || !window.Capacitor.Plugins.TextZoom) { 1475 setTimeout(lockTextZoom, 100); 1476 return; 1477 } 1478 1479 try { 1480 // Set text zoom to 1.0 (100% - normal size) 1481 await window.Capacitor.Plugins.TextZoom.set({ 1482 value: 1.0 1483 }); 1484 console.log('? Text zoom locked to 1.0 (100%)'); 1485 } catch (error) { 1486 console.log('Could not set text zoom:', error); 1487 } 1488 } 1489 1490 // Lock text zoom when DOM is ready 1491 if (document.readyState === 'loading') { 1492 document.addEventListener('DOMContentLoaded', lockTextZoom); 1493 } else { 1494 // DOM is already ready 1495 lockTextZoom(); 1496 } 1497 1498 // Also lock after window loads (as a fallback) 1499 window.addEventListener('load', lockTextZoom); 1500 1501 // Re-lock text zoom after HTMX content swaps 1502 document.body.addEventListener("htmx:afterSwap", lockTextZoom); 1503 </script>
1503 1504 1505
1505<script data-navigate-once> 1506 async function getAppInfo() { 1507 if (!window.Capacitor || !window.Capacitor.Plugins?.App) { 1508 // alert("Capacitor App plugin not available."); 1509 return; 1510 } 1511 1512 const App = window.Capacitor.Plugins.App; 1513 1514 try { 1515 const info = await App.getInfo(); 1516 const state = await App.getState(); 1517 1518 // alert( 1519 // "App Info:\n" + 1520 // "Name: " + info.name + "\n" + 1521 // "ID: " + info.id + "\n" + 1522 // "Version: " + info.version + "\n" + 1523 // "Build: " + info.build + "\n" + 1524 // "State: " + state.isActive 1525 // ); 1526 1527 console.log("Full App Info:", info); 1528 console.log("App State:", state); 1529 1530 } catch (err) { 1531 console.error("Error getting app info:", err); 1532 } 1533 } 1534 1535 document.addEventListener("deviceready", getAppInfo); 1536 document.addEventListener("DOMContentLoaded", getAppInfo); 1537 document.addEventListener('livewire:navigated', getAppInfo); 1538 </script>
1538 1539 1540
1540<script data-navigate-once> 1541 // Capture app launch time for grace windows 1542 const appLaunchTime = Date.now(); 1543 1544 function initCapacitorCore() { 1545 if (!window.Capacitor) return; 1546 1547 // Back Button Handler - Simple version using Capacitor's built-in canGoBack 1548 if (!window._backHandlerSet) { 1549 window._backHandlerSet = true; 1550 const { 1551 App 1552 } = window.Capacitor.Plugins; 1553 1554 // Handle back button 1555 App.addListener('backButton', async (event) => { 1556 console.log('ð± Back button pressed. canGoBack:', event.canGoBack); 1557 1558 if (event.canGoBack) { 1559 // Navigate back in web history 1560 window.history.back(); 1561 console.log('â¬ ï¸ Navigating back in history'); 1562 } else { 1563 // No history, minimize the app (move to background) 1564 console.log('ð± No more history, minimizing app...'); 1565 try { 1566 await App.minimizeApp(); 1567 console.log('? App minimized'); 1568 } catch (error) { 1569 console.error('? Failed to minimize app:', error); 1570 } 1571 } 1572 }); 1573 1574 console.log('? Back button handler initialized'); 1575 } 1576 1577 // Share Override - intercepts navigator.share() calls to use Capacitor native share 1578 if (!navigator._shareOverrideSet) { 1579 navigator._shareOverrideSet = true; 1580 navigator.share = async (data) => { 1581 return await window.Capacitor.Plugins.Share.share(data); 1582 }; 1583 } 1584 1585 // Network Monitoring - Simple and reliable 1586 if (!window._networkMonitorSet) { 1587 window._networkMonitorSet = true; 1588 1589 async function saveUrlAndGoOffline() { 1590 // Grace period after launch to avoid false offline on cold start 1591 const sinceLaunch = Date.now() - appLaunchTime; 1592 if (sinceLaunch < 4000) { 1593 console.warn('Offline redirect suppressed (grace window)', sinceLaunch); 1594 return; 1595 } 1596 1597 // If browser reports online, skip redirect 1598 if (navigator.onLine) { 1599 console.warn('Offline redirect skipped because navigator.onLine'); 1600 return; 1601 } 1602 1603 const currentUrl = window.location.href; 1604 1605 // Save URL using Capacitor Preferences 1606 try { 1607 if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Preferences) { 1608 await window.Capacitor.Plugins.Preferences.set({ 1609 key: 'returnUrl', 1610 value: currentUrl 1611 }); 1612 } else { 1613 localStorage.setItem('capacitor_returnUrl', currentUrl); 1614 } 1615 } catch (e) { 1616 try { 1617 localStorage.setItem('capacitor_returnUrl', currentUrl); 1618 } catch (e2) {} 1619 } 1620 1621 // Navigate to offline page 1622 window.location.href = 'offline.html'; 1623 } 1624 1625 // Livewire requests use fetch instead of HTMX's request events. 1626 let livewireNetworkMonitorSet = false; 1627 function monitorLivewireRequests() { 1628 if (livewireNetworkMonitorSet || !window.Livewire?.interceptRequest) return; 1629 livewireNetworkMonitorSet = true; 1630 window.Livewire.interceptRequest(({ onFailure }) => { 1631 onFailure(() => saveUrlAndGoOffline()); 1632 }); 1633 } 1634 document.addEventListener('livewire:initialized', monitorLivewireRequests); 1635 document.addEventListener('livewire:navigated', monitorLivewireRequests); 1636 monitorLivewireRequests(); 1637 1638 // HTMX: Request failed to send (offline) 1639 document.body.addEventListener('htmx:sendError', function() { 1640 console.log('? HTMX sendError - going offline'); 1641 saveUrlAndGoOffline(); 1642 }); 1643 1644 // HTMX: Request sent but got network error (status 0) 1645 document.body.addEventListener('htmx:responseError', function(event) { 1646 if (event.detail.xhr.status === 0) { 1647 console.log('? HTMX responseError status 0 - going offline'); 1648 saveUrlAndGoOffline(); 1649 } 1650 }); 1651 1652 console.log('? Network monitoring ready'); 1653 } 1654 } 1655 document.addEventListener("DOMContentLoaded", initCapacitorCore); 1656 document.addEventListener('livewire:navigated', initCapacitorCore); 1657 document.body.addEventListener("htmx:afterSwap", initCapacitorCore); 1658 </script>
1658 1659 1660 1661 <!-- Add this to your initOneSignal() function --> 1662 1663
1663<script data-navigate-once> 1664 // Avoid double init on htmx reloads 1665 let oneSignalInitStarted = false; 1666 let oneSignalReady = false; 1667 1668 // Store pending deep links to apply after app is ready 1669 const DEEPLINK_KEY = 'pending_deeplink'; 1670 1671 function handleOneSignalOpen(notification) { 1672 if (!notification) { 1673 console.log("OneSignal: click with no payload"); 1674 return; 1675 } 1676 1677 const additionalData = notification.additionalData || {}; 1678 const target = 1679 additionalData.targetUrl || 1680 additionalData.url || 1681 notification.launchURL; 1682 1683 console.log("OneSignal payload", { 1684 additionalData, 1685 launchURL: notification.launchURL, 1686 resolvedTarget: target 1687 }); 1688 1689 if (!target) { 1690 console.log("OneSignal: no target URL found"); 1691 return; 1692 } 1693 1694 // Stash target to apply after app is fully ready 1695 try { 1696 localStorage.setItem(DEEPLINK_KEY, target); 1697 console.log("Stored pending deep link", target); 1698 } catch (e) { 1699 console.warn("Could not store deep link", e); 1700 } 1701 1702 // Try to hide splash immediately to avoid freeze 1703 if (window.Capacitor?.Plugins?.SplashScreen) { 1704 window.Capacitor.Plugins.SplashScreen.hide().catch(() => {}); 1705 } 1706 } 1707 1708 async function initOneSignal() { 1709 if (!window.plugins?.OneSignal) return; 1710 if (oneSignalInitStarted) { 1711 // Navigation can change the signed-in user or follow a failed save. 1712 if (oneSignalReady) await syncOneSignalSubscription(); 1713 return; 1714 } 1715 oneSignalInitStarted = true; 1716 1717 try { 1718 // Enable debug logs (optional for dev, turn down in production) 1719 window.plugins.OneSignal.Debug.setLogLevel(6); 1720 1721 // Initialize with your OneSignal App ID 1722 window.plugins.OneSignal.initialize("38da2164-7e3c-4830-a4bc-8a975f1cee53"); 1723 1724 // Request notification permission if needed 1725 const hasPermission = await window.plugins.OneSignal.Notifications.hasPermission(); 1726 if (!hasPermission) { 1727 const accepted = await window.plugins.OneSignal.Notifications.requestPermission(true); 1728 if (!accepted) { 1729 console.log('ð Notification permission denied'); 1730 alert( 1731 'ð Notifications disabled\n\nTo receive updates and messages, enable notifications:\n\nSettings > Apps > Lave > Notifications' 1732 ); 1733 oneSignalInitStarted = false; 1734 return; 1735 } 1736 } 1737 1738 // Ensure subscription is opted in 1739 await window.plugins.OneSignal.User.pushSubscription.optIn(); 1740 1741 // ð¥ Handle notification opened events 1742 window.plugins.OneSignal.Notifications.addEventListener("click", (event) => { 1743 console.log("Notification clicked:", event); 1744 handleOneSignalOpen(event.notification); 1745 }); 1746 1747 // Handle cold-start opens (notification tapped before JS attached) 1748 try { 1749 const lastOpen = await window.plugins.OneSignal.Notifications.getLastNotification(); 1750 if (lastOpen?.notification) { 1751 console.log("Handling last opened notification on init"); 1752 handleOneSignalOpen(lastOpen.notification); 1753 } 1754 } catch (e) { 1755 console.warn("Unable to read last opened notification", e); 1756 } 1757 1758 // Listen for subscription changes 1759 window.plugins.OneSignal.User.pushSubscription.addEventListener("change", async (event) => { 1760 if (event.current?.id) { 1761 console.log("Subscription ID changed:", event.current.id); 1762 await saveSubscriptionId(event.current.id); 1763 } 1764 }); 1765 1766 oneSignalReady = true; 1767 await syncOneSignalSubscription(); 1768 1769 } catch (err) { 1770 console.error("OneSignal Error:", err); 1771 oneSignalInitStarted = false; // allow retry 1772 } 1773 } 1774 1775 async function syncOneSignalSubscription() { 1776 try { 1777 const subscriptionId = await window.plugins.OneSignal.User.pushSubscription.getIdAsync(); 1778 if (subscriptionId) await saveSubscriptionId(subscriptionId); 1779 } catch (error) { 1780 console.error('Could not read OneSignal subscription:', error); 1781 } 1782 } 1783 1784 /** 1785 * Save subscription ID - Simple & Effective! 1786 */ 1787 async function saveSubscriptionId(subscriptionId) { 1788 // Let the server check the current session; data-navigate-once scripts 1789 // must not freeze the authentication state of the initial page. 1790 try { 1791 const response = await fetch("/save/onesignal", { 1792 method: "POST", 1793 headers: { 1794 "Content-Type": "application/json", 1795 "Accept": "application/json" 1796 }, 1797 body: JSON.stringify({ 1798 subscription_id: subscriptionId 1799 }) 1800 }); 1801 1802 if (response.status === 401) return; 1803 1804 if (!response.ok) { 1805 const text = await response.text(); 1806 console.error("? Server error:", response.status, text.substring(0, 200)); 1807 return; 1808 } 1809 1810 const result = await response.json(); 1811 if (result.status === 'success') { 1812 console.log("? Subscription saved"); 1813 } else { 1814 console.error("? Failed:", result.message); 1815 } 1816 } catch (error) { 1817 console.error("? Error:", error); 1818 } 1819 } 1820 1821 // Initialize OneSignal on page load 1822 document.addEventListener("DOMContentLoaded", initOneSignal); 1823 document.addEventListener('deviceready', initOneSignal); 1824 document.addEventListener('livewire:navigated', initOneSignal); 1825 document.body.addEventListener("htmx:load", initOneSignal); 1826 </script>
1826 1827 1828 1829 1830 1831 1832
1832<script data-navigate-once> 1833 let specialSchemeClickHandler = null; 1834 1835 function initSpecialSchemeHandlers() { 1836 if (!window.Capacitor) return; 1837 1838 // Remove existing handler first to prevent duplicates 1839 if (specialSchemeClickHandler) { 1840 document.removeEventListener("click", specialSchemeClickHandler); 1841 } 1842 1843 // Handle tel:, mailto:, sms: links 1844 specialSchemeClickHandler = async (e) => { 1845 const link = e.target.closest('a[href^="tel:"], a[href^="mailto:"], a[href^="sms:"]'); 1846 if (!link) return; 1847 1848 e.preventDefault(); 1849 const url = link.href; 1850 1851 try { 1852 // Open in external system app (phone dialer, email client, sms app) 1853 await Capacitor.Plugins.InAppBrowser.openInExternalBrowser({ 1854 url 1855 }); 1856 console.log('? Opened special scheme URL:', url); 1857 } catch (err) { 1858 console.error('? Error opening special scheme URL:', err); 1859 // Fallback: try window.open 1860 window.open(url, '_system'); 1861 } 1862 }; 1863 document.addEventListener("click", specialSchemeClickHandler); 1864 console.log('? Special scheme handlers initialized (tel:, mailto:, sms:)'); 1865 } 1866 1867 document.addEventListener("DOMContentLoaded", initSpecialSchemeHandlers); 1868 document.addEventListener('livewire:navigated', initSpecialSchemeHandlers); 1869 document.body.addEventListener("htmx:afterSwap", initSpecialSchemeHandlers); 1870 </script>
1870 1871 1872 1873
1873<script data-navigate-once> 1874 (function initDeepLinks() { 1875 if (!window.Capacitor?.Plugins?.App) return; 1876 1877 const { 1878 App 1879 } = window.Capacitor.Plugins; 1880 1881 function isAllowedHost(hostname) { 1882 return hostname === 'lave.live' || hostname.endsWith('.lave.live'); 1883 } 1884 1885 function toWebUrl(incomingUrl) { 1886 if (!incomingUrl) return null; 1887 1888 // Handle lave://https://lave.live/... format (full URL embedded after scheme) 1889 if (incomingUrl.startsWith('lave://https://') || incomingUrl.startsWith('lave://http://')) { 1890 const realUrl = incomingUrl.replace(/^lave:\/\//, ''); 1891 try { 1892 const parsed = new URL(realUrl); 1893 if (isAllowedHost(parsed.hostname)) return parsed.toString(); 1894 } catch (_) { 1895 // fall through 1896 } 1897 } 1898 1899 let u; 1900 try { 1901 u = new URL(incomingUrl); 1902 } catch (_) { 1903 return null; 1904 } 1905 1906 // Direct https links (if someone passes full URL) 1907 if ((u.protocol === 'https:' || u.protocol === 'http:') && isAllowedHost(u.hostname)) { 1908 return u.toString(); 1909 } 1910 1911 // Custom scheme: lave://home/app OR lave://open?url=https%3A%2F%2Flave.live%2F... 1912 if (u.protocol === 'lave:') { 1913 // Check for embedded URL param first 1914 const embedded = u.searchParams.get('url'); 1915 if (embedded) { 1916 try { 1917 const embeddedUrl = new URL(embedded); 1918 if (isAllowedHost(embeddedUrl.hostname)) return embeddedUrl.toString(); 1919 } catch (_) { 1920 // ignore invalid embedded URL 1921 } 1922 } 1923 1924 // Convert lave://some/path?x=1#hash => https://lave.live/some/path?x=1#hash 1925 // u.host contains the first path segment (e.g. "home" from lave://home/app) 1926 const path = `/${u.host}${u.pathname}`.replace(/\/{2,}/g, '/'); 1927 return `https://lave.live${path}${u.search}${u.hash}`; 1928 } 1929 1930 return null; 1931 } 1932 1933 function navigateFromDeepLink(url) { 1934 const target = toWebUrl(url); 1935 if (!target) return; 1936 console.log('ð Deep link received, navigating to:', target); 1937 window.location.href = target; 1938 } 1939 1940 // Cold start - app was closed, opened via deep link 1941 App.getLaunchUrl() 1942 .then(result => { 1943 if (result?.url) navigateFromDeepLink(result.url); 1944 }) 1945 .catch(() => {}); 1946 1947 // Warm start - app was in background, opened via deep link 1948 App.addListener('appUrlOpen', (event) => { 1949 if (event?.url) navigateFromDeepLink(event.url); 1950 }); 1951 1952 console.log('? Deep link handler initialized'); 1953 })(); 1954 </script>
1954 1955 1956 1957
1957<script data-navigate-once> 1958 let targetBlankClickHandler = null; 1959 let targetBlankSubmitHandler = null; 1960 1961 function initTargetBlankHandlers() { 1962 if (!window.Capacitor) return; 1963 1964 // Remove existing handlers first to prevent duplicates 1965 if (targetBlankClickHandler) { 1966 document.removeEventListener("click", targetBlankClickHandler, true); 1967 } 1968 if (targetBlankSubmitHandler) { 1969 document.removeEventListener("submit", targetBlankSubmitHandler, true); 1970 } 1971 1972 // Handle _blank target for <a> elements 1973 // Use capture phase to catch events early and prevent default browser behavior 1974 targetBlankClickHandler = async (e) => { 1975 const link = e.target.closest('a[target="_blank"]'); 1976 if (!link) return; 1977 1978 // Skip if it's a special scheme (handled by specialSchemeClickHandler) 1979 if (link.href && (link.href.startsWith('tel:') || link.href.startsWith('mailto:') || link.href 1980 .startsWith('sms:'))) { 1981 return; 1982 } 1983 1984 // Prevent default immediately to stop browser from opening 1985 e.preventDefault(); 1986 e.stopPropagation(); 1987 1988 const url = link.href || link.getAttribute('href'); 1989 if (!url) return; 1990 1991 try { 1992 // Check if native="external_browser" attribute is set to use external browser 1993 if (link.getAttribute("native") === "external_browser") { 1994 await Capacitor.Plugins.InAppBrowser.openInExternalBrowser({ 1995 url: url 1996 }); 1997 console.log('â Opened in external browser:', url); 1998 } else { 1999 // Default: Use InAppBrowser System Browser (Custom Tabs for Android, SFSafariViewController for iOS) 2000 // This opens in-app but uses system browser components, not external browser 2001 await Capacitor.Plugins.Browser.open({ 2002 url, 2003 options: { 2004 showToolbar: true, 2005 toolbarPosition: "TOP" 2006 } 2007 }); 2008 } 2009 } catch (err) { 2010 alert("Browser error:", err); 2011 } 2012 }; 2013 // Use capture phase (true) to catch events before they bubble 2014 document.addEventListener("click", targetBlankClickHandler, true); 2015 2016 } 2017 2018 document.addEventListener("DOMContentLoaded", initTargetBlankHandlers); 2019 document.addEventListener('livewire:navigated', initTargetBlankHandlers); 2020 document.body.addEventListener("htmx:afterSwap", initTargetBlankHandlers); 2021 </script>
2021 2022 2023 2024
2024<script data-navigate-once> 2025 let capacitorFileClickHandler = null; 2026 2027 function initFileInput() { 2028 if (!window.Capacitor) return; 2029 2030 const fileModal = document.getElementById("fileModal"); 2031 let currentInput = null; 2032 let tempInputs = []; // Keep references to prevent garbage collection 2033 2034 // Create modal if it doesn't exist 2035 if (!fileModal) { 2036 const modal = document.createElement("div"); 2037 modal.id = "fileModal"; 2038 modal.className = "fixed inset-0 hidden items-center justify-center bg-black bg-opacity-60 px-4"; 2039 modal.style.zIndex = "2147483647"; 2040 modal.innerHTML = ` 2041 <div class="relative top-40 mx-auto w-full max-w-md overflow-hidden rounded-2xl bg-white shadow-xl"> 2042 <div class="flex items-center justify-between border-b border-gray-200 px-5 py-3"> 2043 <div class="text-base font-semibold text-gray-800">Select file</div> 2044 <button onclick="closeModal('fileModal')" class="text-2xl font-light text-gray-400 hover:text-gray-600" type="button">Ã</button> 2045 </div> 2046 <div class="divide-y divide-gray-100">
2047 <button onclick="choosePhoto()" class="flex w-full items-center gap-3 px-5 py-3 text-left text-gray-700 hover:bg-gray-50 active:bg-gray-100"> 2048 <span class="text-xl">ð¸</span> 2049 <span class="text-sm">Take photo with camera</span> 2050 </button> 2051 <button onclick="chooseFromLibrary()" class="flex w-full items-center gap-3 px-5 py-3 text-left text-gray-700 hover:bg-gray-50 active:bg-gray-100"> 2052 <span class="text-xl">ð¼ï¸</span> 2053 <span class="text-sm">Select photo from gallery</span> 2054 </button> 2055 <button onclick="recordVideo()" class="flex w-full items-center gap-3 px-5 py-3 text-left text-gray-700 hover:bg-gray-50 active:bg-gray-100"> 2056 <span class="text-xl">ð¹</span> 2057 <span class="text-sm">Record video</span> 2058 </button>
2059 <button onclick="chooseFile()" class="flex w-full items-center gap-3 px-5 py-3 text-left text-gray-700 hover:bg-gray-50 active:bg-gray-100"> 2060 <span class="text-xl">ð</span> 2061 <span class="text-sm">Choose file</span> 2062 </button> 2063 </div> 2064 </div> 2065 `; 2066 document.body.appendChild(modal); 2067 } 2068 2069 const modal = document.getElementById("fileModal"); 2070 2071 // Delegation also covers inputs inserted by Livewire modal renders. 2072 if (capacitorFileClickHandler) { 2073 document.removeEventListener('click', capacitorFileClickHandler); 2074 } 2075 capacitorFileClickHandler = (e) => { 2076 const inp = e.target.closest('input[type="file"]'); 2077 if (!inp || inp._capacitorNativePicker) return; 2078 e.preventDefault(); 2079 currentInput = inp; 2080 if (modal) modal.classList.remove('hidden'); 2081 }; 2082 document.addEventListener('click', capacitorFileClickHandler); 2083 2084 // Click outside modal to close 2085 if (modal) { 2086 if (modal._modalClickHandler) { 2087 modal.removeEventListener("click", modal._modalClickHandler); 2088 } 2089 2090 modal._modalClickHandler = (e) => { 2091 if (e.target === modal) modal.classList.add("hidden"); 2092 }; 2093 modal.addEventListener("click", modal._modalClickHandler); 2094 } 2095 2096 // Helper function to inject file into input 2097 function injectFile(file, targetInput) { 2098 if (!targetInput?.isConnected) { 2099 console.error('No file input available to inject file'); 2100 return; 2101 } 2102 try { 2103 const dt = new DataTransfer(); 2104 dt.items.add(file); 2105 targetInput.files = dt.files; 2106 targetInput.dispatchEvent(new Event("change", { 2107 bubbles: true 2108 })); 2109 console.log(`Selected: ${file.name}`); 2110 } catch (error) { 2111 console.error('Error injecting file:', error); 2112 } 2113 } 2114 2115 // Helper to create temp input and inject file 2116 function createAndInjectFile(inputEl) { 2117 const targetInput = currentInput; 2118 inputEl._capacitorNativePicker = true; 2119 // Add input to DOM (hidden) to prevent garbage collection 2120 inputEl.style.position = 'absolute'; 2121 inputEl.style.left = '-9999px'; 2122 inputEl.style.opacity = '0'; 2123 inputEl.style.pointerEvents = 'none'; 2124 document.body.appendChild(inputEl); 2125 2126 // Keep reference to prevent garbage collection 2127 tempInputs.push(inputEl); 2128 2129 // Use addEventListener for better reliability 2130 const changeHandler = (e) => { 2131 if (inputEl.files && inputEl.files.length > 0) { 2132 injectFile(inputEl.files[0], targetInput); 2133 } 2134 // Clean up after use 2135 inputEl.removeEventListener('change', changeHandler); 2136 setTimeout(() => { 2137 if (inputEl.parentNode) { 2138 inputEl.parentNode.removeChild(inputEl); 2139 } 2140 const index = tempInputs.indexOf(inputEl); 2141 if (index > -1) { 2142 tempInputs.splice(index, 1); 2143 } 2144 }, 100); 2145 }; 2146 2147 inputEl.addEventListener('change', changeHandler); 2148 2149 // Small delay to ensure modal is closed and file picker can open 2150 setTimeout(() => { 2151 inputEl.click(); 2152 }, 100); 2153 } 2154 2155 // Global functions for modal buttons 2156 window.choosePhoto = function() { 2157 if (!currentInput) { 2158 console.error('No file input selected'); 2159 return; 2160 } 2161 if (modal) modal.classList.add("hidden"); 2162 // Use native file input - doesn't interfere with status bar 2163 const input = document.createElement("input"); 2164 input.type = "file"; 2165 input.accept = "image/*"; 2166 input.capture = "environment"; // Opens camera directly 2167 createAndInjectFile(input); 2168 }; 2169 2170 window.chooseFromLibrary = function() { 2171 if (!currentInput) { 2172 console.error('No file input selected'); 2173 return; 2174 } 2175 if (modal) modal.classList.add("hidden"); 2176 // Use native file input - doesn't interfere with status bar 2177 const input = document.createElement("input"); 2178 input.type = "file"; 2179 input.accept = "image/*"; 2180 createAndInjectFile(input); 2181 }; 2182 2183 window.recordVideo = function() { 2184 if (!currentInput) { 2185 console.error('No file input selected'); 2186 return; 2187 } 2188 if (modal) modal.classList.add("hidden"); 2189 // Use native file input - doesn't interfere with status bar 2190 const input = document.createElement("input"); 2191 input.type = "file"; 2192 input.accept = "video/*"; 2193 input.capture = "environment"; // Opens camera for video 2194 createAndInjectFile(input); 2195 }; 2196 2197 window.chooseFile = function() { 2198 if (!currentInput) { 2199 console.error('No file input selected'); 2200 return; 2201 } 2202 if (modal) modal.classList.add("hidden"); 2203 // Use native file input - simple and reliable 2204 const input = document.createElement("input"); 2205 input.type = "file"; 2206 createAndInjectFile(input); 2207 }; 2208 2209 window.closeModal = function(modalId) { 2210 const m = document.getElementById(modalId); 2211 if (m) m.classList.add("hidden"); 2212 }; 2213 } 2214 2215 document.addEventListener("DOMContentLoaded", initFileInput); 2216 document.addEventListener('livewire:navigated', initFileInput); 2217 document.body.addEventListener("htmx:afterSwap", initFileInput); 2218 </script>
2218 2219 2220 2221
2221<script data-navigate-once> 2222 function updateCapacitorCountryCode(select) { 2223 const form = select.closest('form') || select.parentElement; 2224 const hiddenField = form?.querySelector('input[name="tel_country_code"]'); 2225 const selectedOption = select.options[select.selectedIndex]; 2226 if (!hiddenField || !selectedOption) return; 2227 2228 hiddenField.value = selectedOption.getAttribute('data-phonecode') || ''; 2229 hiddenField.dispatchEvent(new Event('input', { bubbles: true })); 2230 } 2231 2232 // The select may be rendered after page initialization by Livewire. 2233 document.addEventListener('change', (event) => { 2234 const select = event.target.closest('select[name="country"]'); 2235 if (select) updateCapacitorCountryCode(select); 2236 }); 2237 2238 function initCountrySelect() { 2239 document.querySelectorAll('select[name="country"]').forEach(updateCapacitorCountryCode); 2240 } 2241 2242 // Initialize on page load and after HTMX swaps 2243 document.addEventListener('DOMContentLoaded', initCountrySelect); 2244 document.addEventListener('livewire:navigated', initCountrySelect); 2245 document.body.addEventListener('htmx:afterSwap', initCountrySelect); 2246 </script>
2246 2247 2248 2249
2249<script data-navigate-once> 2250 let contactPickerClickHandler = null; 2251 2252 function initContactPicker() { 2253 if (!window.Capacitor) return; 2254 2255 const contactPickerModal = document.getElementById('contactPickerModal'); 2256 const contactList = document.getElementById('contactList'); 2257 const contactNameHeader = document.getElementById('contactNameHeader'); 2258 const closeModalPhoneBtn = document.getElementById('closeModalPhone'); 2259 2260 function opencontactPickerModal(contactName, phones, onSelect) { 2261 if (!contactList) return; 2262 contactList.innerHTML = ''; 2263 if (contactNameHeader) { 2264 contactNameHeader.textContent = contactName || 'Select Phone Number'; 2265 contactNameHeader.title = contactName; 2266 } 2267 2268 phones.forEach((number, index) => { 2269 const btn = document.createElement('button'); 2270 btn.className = 2271 "w-full text-left px-5 py-4 text-base text-gray-800 hover:bg-blue-50 active:bg-blue-100 transition-colors flex items-center gap-3"; 2272 btn.type = "button"; 2273 2274 // Add phone icon 2275 const icon = document.createElement('span'); 2276 icon.innerHTML = 'ð±'; 2277 icon.className = "text-xl"; 2278 2279 const text = document.createElement('span'); 2280 text.textContent = number; 2281 text.className = "flex-1"; 2282 2283 btn.appendChild(icon); 2284 btn.appendChild(text); 2285 2286 btn.addEventListener('click', () => { 2287 closecontactPickerModal(); 2288 onSelect(number); 2289 }); 2290 contactList.appendChild(btn); 2291 }); 2292 2293 if (contactPickerModal) { 2294 contactPickerModal.classList.remove('hidden'); 2295 contactPickerModal.classList.add('flex'); 2296 } 2297 } 2298 2299 function closecontactPickerModal() { 2300 if (contactPickerModal) { 2301 contactPickerModal.classList.add('hidden'); 2302 contactPickerModal.classList.remove('flex'); 2303 } 2304 } 2305 2306 // Close button handler 2307 if (closeModalPhoneBtn) { 2308 // Remove existing listener first 2309 if (closeModalPhoneBtn._clickHandler) { 2310 closeModalPhoneBtn.removeEventListener('click', closeModalPhoneBtn._clickHandler); 2311 } 2312 closeModalPhoneBtn._clickHandler = closecontactPickerModal; 2313 closeModalPhoneBtn.addEventListener('click', closeModalPhoneBtn._clickHandler); 2314 } 2315 2316 // Click outside to close 2317 if (contactPickerModal) { 2318 if (contactPickerModal._outsideClickHandler) { 2319 contactPickerModal.removeEventListener('click', contactPickerModal._outsideClickHandler); 2320 } 2321 contactPickerModal._outsideClickHandler = (e) => { 2322 if (e.target === contactPickerModal) { 2323 closecontactPickerModal(); 2324 } 2325 }; 2326 contactPickerModal.addEventListener('click', contactPickerModal._outsideClickHandler); 2327 } 2328 2329 // Remove existing contact picker handler 2330 if (contactPickerClickHandler) { 2331 document.removeEventListener('click', contactPickerClickHandler); 2332 } 2333 2334 contactPickerClickHandler = async (e) => { 2335 const el = e.target.closest('[native="contact"]'); 2336 if (!el) return; 2337 2338 e.preventDefault(); 2339 2340 const contactForm = el.closest('form'); 2341 if (!contactForm) return; 2342 2343 const { 2344 Contacts 2345 } = window.Capacitor.Plugins || {}; 2346 if (!Contacts) { 2347 console.log('Contacts plugin not available'); 2348 return; 2349 } 2350 2351 // Function to pick contact 2352 async function tryPickContact() { 2353 return await Contacts.pickContact({ 2354 projection: { 2355 name: true, 2356 phones: true, 2357 emails: true 2358 } 2359 }); 2360 } 2361 2362 try { 2363 // â Try pickContact() first - it should handle permissions automatically 2364 const result = await tryPickContact(); 2365 await processContactResult(result); 2366 } catch (err) { 2367 console.error("Contact picker error:", err); 2368 2369 // ð Fallback: If permission error, request permission and retry 2370 if (err.message && err.message.toLowerCase().includes('permission')) { 2371 console.log('Permission error detected, requesting permissions...'); 2372 2373 try { 2374 const permission = await Contacts.checkPermissions(); 2375 console.log('Current permission:', permission); 2376 2377 if (permission.contacts !== 'granted') { 2378 const requested = await Contacts.requestPermissions(); 2379 console.log('Permission requested:', requested); 2380 2381 if (requested.contacts !== 'granted') { 2382 alert( 2383 'ð± To select contacts, please allow access in your device settings.\n\nSettings > Apps > Lave > Permissions > Contacts > Full Access' 2384 ); 2385 return; 2386 } 2387 } 2388 2389 // Retry with permission granted 2390 const result = await tryPickContact(); 2391 await processContactResult(result); 2392 2393 } catch (permErr) { 2394 console.error("Permission request failed:", permErr); 2395 alert( 2396 'ð± To select contacts, please allow access in your device settings.\n\nSettings > Apps > Lave > Permissions > Contacts > Full Access' 2397 ); 2398 } 2399 } else { 2400 // Different error, show it 2401 alert('â ERROR picking contact:\n\n' + err.message); 2402 } 2403 } 2404 2405 // Process contact result 2406 async function processContactResult(result) { 2407 2408 if (result?.contact) { 2409 2410 const rawName = result.contact.name?.display || 'No name'; 2411 const phones = result.contact.phones?.map(p => p.number) || []; 2412 const emails = result.contact.emails?.map(e => e.address) || []; 2413 2414 2415 // Function to keep only alphanumeric characters and spaces (removes emojis & special chars) 2416 function stripEmojis(text) { 2417 return text 2418 .replace(/[^a-zA-Z0-9\s.,'-]/g, 2419 '') // Keep only letters, numbers, space
2419s, and basic punctuation 2420 .trim() 2421 .replace(/\s+/g, ' '); // Clean up extra spaces 2422 } 2423 2424 const name = stripEmojis(rawName); 2425 2426 // Function to extract country code by comparing with select options 2427 function extractCountryCodeFromSelect(phoneNumber, countrySelect) { 2428 // Remove all spaces, dashes, parentheses, and + sign 2429 let cleaned = phoneNumber.replace(/[\s\-\(\)\+]/g, ''); 2430 2431 // If no digits, return empty 2432 if (!cleaned || !/^\d+$/.test(cleaned)) { 2433 return { 2434 countryCode: null, 2435 phone: cleaned, 2436 matchFound: false 2437 }; 2438 } 2439 2440 // Get all country codes from the select options 2441 const options = countrySelect ? countrySelect.options : []; 2442 const countryCodes = []; 2443 2444 // Preferred countries for shared codes (main countries over territories) 2445 const preferredCountries = [ 2446 'United Kingdom', 'United States', 'Canada', 'Australia', 2447 'France', 'Denmark', 'Netherlands', 'New Zealand' 2448 ]; 2449 2450 for (let i = 0; i < options.length; i++) { 2451 const phonecode = options[i].getAttribute('data-phonecode'); 2452 const countryName = options[i].value || options[i].textContent.trim(); 2453 if (phonecode) { 2454 const isPreferred = preferredCountries.some(pref => 2455 countryName.toLowerCase().includes(pref.toLowerCase()) 2456 ); 2457 countryCodes.push({ 2458 code: phonecode, 2459 index: i, 2460 name: countryName, 2461 isPreferred: isPreferred 2462 }); 2463 } 2464 } 2465 2466 // Sort by: 2467 // 1. Code length (longest first) - e.g., 1684 before 1 2468 // 2. Preferred countries first (for same length codes) 2469 // 3. Index (original order) 2470 countryCodes.sort((a, b) => { 2471 if (b.code.length !== a.code.length) { 2472 return b.code.length - a.code.length; 2473 } 2474 if (a.isPreferred !== b.isPreferred) { 2475 return b.isPreferred ? 1 : -1; 2476 } 2477 return a.index - b.index; 2478 }); 2479 2480 // Try to find a matching country code 2481 for (const country of countryCodes) { 2482 if (cleaned.startsWith(country.code)) { 2483 // Found a match! 2484 const phone = cleaned.substring(country.code.length); 2485 return { 2486 countryCode: country.code, 2487 phone: phone, 2488 matchFound: true, 2489 countryIndex: country.index, 2490 countryName: country.name 2491 }; 2492 } 2493 } 2494 2495 // No match found, return the whole number as-is 2496 return { 2497 countryCode: null, 2498 phone: cleaned, 2499 matchFound: false 2500 }; 2501 } 2502 2503 // Function to populate form fields 2504 function populateContactForm(selectedPhone) { 2505 // The picker may return after navigation or after its form has closed. 2506 if (!contactForm.isConnected) return; 2507 2508 const nameInput = contactForm.querySelector('input[name="name"]'); 2509 const emailInput = contactForm.querySelector('input[name="email"]'); 2510 const telInput = contactForm.querySelector('input[name="tel"]'); 2511 const countrySelect = contactForm.querySelector('select[name="country"]'); 2512 const countryCodeInput = contactForm.querySelector('input[name="tel_country_code"]'); 2513 2514 function updateInput(input, value) { 2515 if (!input) return; 2516 input.value = value; 2517 input.dispatchEvent(new Event('input', { bubbles: true })); 2518 } 2519 2520 // Populate name 2521 updateInput(nameInput, name); 2522 2523 // Populate email 2524 if (emails.length > 0) updateInput(emailInput, emails[0]); 2525 2526 // Process phone number and country code 2527 if (selectedPhone) { 2528 const result = extractCountryCodeFromSelect(selectedPhone, countrySelect); 2529 2530 // Populate phone number (without country code) 2531 updateInput(telInput, result.phone); 2532 2533 // Set country based on matched country code 2534 if (result.matchFound && countrySelect) { 2535 // Set the dropdown 2536 countrySelect.selectedIndex = result.countryIndex; 2537 2538 // Directly update the hidden field with the country code 2539 if (countryCodeInput && result.countryCode) { 2540 updateInput(countryCodeInput, result.countryCode); 2541 } 2542 2543 // Also trigger the change event as backup 2544 countrySelect.dispatchEvent(new Event('change', { bubbles: true })); 2545 2546 console.log('â Country set to:', result.countryName, '(+' + result 2547 .countryCode + ')'); 2548 } 2549 } 2550 } 2551 2552 if (phones.length === 0) { 2553 // No phone number, just populate name and email 2554 populateContactForm(''); 2555 } else if (phones.length === 1) { 2556 // Single phone number, populate immediately 2557 populateContactForm(phones[0]); 2558 } else { 2559 // Multiple phone numbers, show modal 2560 opencontactPickerModal(name, phones, selected => { 2561 populateContactForm(selected); 2562 }); 2563 } 2564 } else { 2565 // No contact in result 2566 alert('â ï¸ No contact found'); 2567 } 2568 } 2569 }; 2570 document.addEventListener('click', contactPickerClickHandler); 2571 } 2572 2573 document.addEventListener("DOMContentLoaded", initContactPicker); 2574 document.addEventListener('livewire:navigated', initContactPicker); 2575 document.body.addEventListener("htmx:afterSwap", initContactPicker); 2576 </script>
2576 2577 2578 2579 2580
2580<script data-navigate-once> 2581 function initCapacitorButtons() { 2582 if (!window.Capacitor) return; 2583 2584 const btnDevice = document.getElementById('btnDevice'); 2585 if (btnDevice && !btnDevice._capacitorHandlerAttached) { 2586 btnDevice._capacitorHandlerAttached = true; 2587 btnDevice.addEventListener('click', async () => { 2588 try { 2589 if (window.Capacitor.Plugins.Device) { 2590 const info = await window.Capacitor.Plugins.Device.getInfo(); 2591 alert(JSON.stringify(info, null, 2)); 2592 } else alert(JSON.stringify({ 2593 platform: 'web', 2594 userAgent: navigator.userAgent 2595 }, null, 2)); 2596 } catch (err) { 2597 alert("Error: " + err); 2598 } 2599 }); 2600 } 2601 2602 } 2603 2604 document.addEventListener("DOMContentLoaded", initCapacitorButtons); 2605 document.addEventListener('livewire:navigated', initCapacitorButtons); 2606 document.body.addEventListener("htmx:afterSwap", initCapacitorButtons); 2607 </script>
2607 2608 2609 2610
2610<script data-navigate-once> 2611 /** 2612 * Reveal the parsed page after a paint opportunity. Images and other 2613 * non-critical resources can finish loading with the page visible. 2614 */ 2615 let splashScreenHidden = false; 2616 let splashScreenHiding = false; 2617 2618 async function hideSplashScreen() { 2619 // Prevent multiple calls 2620 if (splashScreenHidden || splashScreenHiding) return; 2621 2622 // Wait for Capacitor to be available 2623 if (!window.Capacitor) { 2624 setTimeout(hideSplashScreen, 100); 2625 return; 2626 } 2627 2628 // Wait for SplashScreen plugin to be available 2629 if (!window.Capacitor.Plugins || !window.Capacitor.Plugins.SplashScreen) { 2630 setTimeout(hideSplashScreen, 100); 2631 return; 2632 } 2633 2634 splashScreenHiding = true; 2635 try { 2636 // Wait for DOM to be ready 2637 if (document.readyState === 'loading') { 2638 await new Promise(resolve => { 2639 if (document.readyState === 'loading') { 2640 document.addEventListener('DOMContentLoaded', resolve); 2641 } else { 2642 resolve(); 2643 } 2644 }); 2645 } 2646 2647 // Give the browser a chance to paint before revealing the page. 2648 await new Promise(resolve => requestAnimationFrame(resolve)); 2649 await new Promise(resolve => requestAnimationFrame(resolve)); 2650 2651 // Hide the splash screen 2652 await window.Capacitor.Plugins.SplashScreen.hide(); 2653 splashScreenHidden = true; 2654 console.log('â Splash screen hidden - initial page ready'); 2655 } catch (error) { 2656 console.warn('Could not hide splash screen:', error); 2657 // Try one more time after a delay 2658 setTimeout(async () => { 2659 try { 2660 if (!splashScreenHidden && window.Capacitor?.Plugins?.SplashScreen) { 2661 await window.Capacitor.Plugins.SplashScreen.hide(); 2662 splashScreenHidden = true; 2663 console.log('â Splash screen hidden (retry)'); 2664 } 2665 } catch (retryError) { 2666 console.error('Failed to hide splash screen after retry:', retryError); 2667 } 2668 }, 1000); 2669 } finally { 2670 splashScreenHiding = false; 2671 } 2672 } 2673 2674 // Start checking when script loads 2675 if (document.readyState === 'loading') { 2676 document.addEventListener('DOMContentLoaded', hideSplashScreen, { once: true }); 2677 } else { 2678 // DOM is already ready, start the hide process 2679 hideSplashScreen(); 2680 } 2681 2682 // Fallback: ensure splash screen is hidden after window fully loads 2683 window.addEventListener('load', hideSplashScreen, { once: true }); 2684 2685 // Retry initialization if an earlier readiness hook was missed. 2686 setTimeout(() => { 2687 if (splashScreenHidden) return; 2688 console.warn('Fallback splash hide after timeout'); 2689 hideSplashScreen(); 2690 }, 4000); 2691 </script>
2691 2692 2693 2694
2694<script data-navigate-once> 2695 // Apply any pending deep link without waiting for HTMX 2696 (function applyPendingDeepLink() { 2697 const DEEPLINK_KEY = 'pending_deeplink'; 2698 let deepLinkApplied = false; 2699 2700 async function apply(delayMs = 0) { 2701 if (deepLinkApplied) return; 2702 2703 // Small optional delay to let bootstrapping settle 2704 if (delayMs > 0) { 2705 await new Promise(resolve => setTimeout(resolve, delayMs)); 2706 } 2707 2708 const target = localStorage.getItem(DEEPLINK_KEY); 2709 if (!target) return; 2710 2711 try { 2712 localStorage.removeItem(DEEPLINK_KEY); 2713 } catch (_) {} 2714 2715 deepLinkApplied = true; 2716 console.log('Applying pending deep link', target); 2717 window.location.href = target; 2718 } 2719 2720 const applySoon = () => apply(50); 2721 const applyNow = () => apply(0); 2722 2723 if (document.readyState === 'loading') { 2724 document.addEventListener('DOMContentLoaded', applySoon); 2725 } else { 2726 applySoon(); 2727 } 2728 2729 window.addEventListener('load', applyNow); 2730 window.addEventListener('focus', applyNow); 2731 document.addEventListener('visibilitychange', applyNow); 2732 2733 // When app returns to foreground (Capacitor) 2734 if (window.Capacitor?.Plugins?.App) { 2735 try { 2736 window.Capacitor.Plugins.App.addListener('appStateChange', ({ 2737 isActive 2738 }) => { 2739 if (isActive) applyNow(); 2740 }); 2741 } catch (e) { 2742 console.warn('App state listener failed', e); 2743 } 2744 } 2745 2746 // Periodic safety check in case other hooks miss 2747 setTimeout(applyNow, 500); 2748 setTimeout(applyNow, 1500); 2749 })(); 2750 </script>
2750 2751 2752</div> 2753 2754
2754<script> 2755 (function() { 2756 const isPWA = 2757 window.matchMedia('(display-mode: standalone)').matches || 2758 window.navigator.standalone === true; 2759 2760 if (isPWA) { 2761 document.cookie = "app_mode=pwa; path=/; max-age=31536000"; 2762 } 2763 })(); 2764 </script>
2764 2765 2766 2767 2768 2769 2770 <!-- Search settings modal container --> 2771
2771<script> 2772 /** 2773 * Global Shadow DOM Content Renderer 2774 * Processes elements with .shadow-content and .raw-content 2775 */ 2776 function renderShadowContent(container = document) { 2777 // Ensure we have a valid element to search in 2778 const searchRoot = (container && container.querySelectorAll) ? container : document; 2779 2780 const targets = (searchRoot.classList && searchRoot.classList.contains('shadow-content')) ? [searchRoot] : 2781 searchRoot.querySelectorAll('.shadow-content'); 2782 2783 // If we're processing a specific container and find nothing, fallback once to the whole document. 2784 if (targets.length === 0 && searchRoot !== document) { 2785 renderShadowContent(document); 2786 return; 2787 } 2788 2789 targets.forEach(host => { 2790 if (host.shadowRoot) return; // Skip if already processed 2791 2792 const dataElement = host.parentElement.querySelector('.raw-content'); 2793 if (!dataElement) return; 2794 2795 try { 2796 const shadow = host.attachShadow({ 2797 mode: 'open' 2798 }); 2799 const content = JSON.parse(dataElement.textContent); 2800 2801 const style = document.createElement('style'); 2802 style.textContent = ` 2803 2804 img { 2805 max-width: 100% !important; 2806 height: auto !important; 2807 display: block !important; 2808 object-fit: contain !important; 2809 margin: auto !important; 2810 } 2811 iframe, video { 2812 display: table !important; 2813 margin-left: auto !important; 2814 margin-right: auto !important; 2815 max-width: 100% !important; 2816 object-fit: contain !important; 2817 } 2818 `; 2819 2820 const wrapper = document.createElement('div'); 2821 wrapper.innerHTML = content; 2822 2823 shadow.appendChild(style); 2824 shadow.appendChild(wrapper); 2825 } catch (e) { 2826 console.error('Shadow DOM Error:', e); 2827 } 2828 }); 2829 } 2830 2831 document.addEventListener('livewire:init', () => { 2832 renderShadowContent(); 2833 Livewire.hook('morphed', ({ 2834 el 2835 }) => renderShadowContent(el)); 2836 }); 2837 2838 document.addEventListener('livewire:navigated', () => renderShadowContent()); 2839 </script>
2839 2840 2841 2842
2842<script> 2843 document.addEventListener('livewire:init', () => { 2844 const refreshUIkit = (root) => { 2845 window.requestAnimationFrame(() => { 2846 if (!window.UIkit || !root?.isConnected) { 2847 return; 2848 } 2849 2850 const targets = []; 2851 2852 if (isUIkitComponent(root)) { 2853 targets.push(root); 2854 } 2855 2856 root.querySelectorAll('*').forEach((element) => { 2857 if (isUIkitComponent(element)) { 2858 targets.push(element); 2859 } 2860 }); 2861 2862 targets.forEach((element) => window.UIkit.update(element)); 2863 }); 2864 }; 2865 2866 const isUIkitManagedGraphic = (element) => { 2867 if (element.matches( 2868 '[uk-img], [data-uk-img], [uk-svg], [data-uk-svg], [uk-spinner], [data-uk-spinner]' 2869 )) { 2870 return true; 2871 } 2872 2873 return Array.from(element.attributes).some(({ 2874 name 2875 }) => { 2876 return /^(data-)?uk-(.+-)?icon$/.test(name); 2877 }); 2878 }; 2879 2880 const isUIkitComponent = (element) => { 2881 return Array.from(element.attributes).some(({ 2882 name 2883 }) => { 2884 return /^(data-)?uk-/.test(name); 2885 }); 2886 }; 2887 2888 Livewire.hook('morph.updating', ({ 2889 el, 2890 skip, 2891 childrenOnly 2892 }) => { 2893 if (isUIkitManagedGraphic(el)) { 2894 skip(); 2895 2896 return; 2897 } 2898 2899 if (isUIkitComponent(el)) { 2900 childrenOnly(); 2901 } 2902 }); 2903 2904 Livewire.hook('morphed', ({ 2905 el 2906 }) => { 2907 refreshUIkit(el); 2908 }); 2909 2910 Livewire.hook('partial.morphed', ({ 2911 startNode 2912 }) => { 2913 refreshUIkit(startNode.parentElement); 2914 }); 2915 2916 Livewire.hook('morph.added', ({ 2917 el 2918 }) => { 2919 refreshUIkit(el); 2920 }); 2921 }); 2922 </script>
2922 2923 2924
2924<script> 2925 document.addEventListener('livewire:init', () => { 2926 const pendingSubmitComponents = new Set(); 2927 2928 document.addEventListener('submit', (event) => { 2929 const form = event.target; 2930 2931 if (!(form instanceof HTMLFormElement)) { 2932 return; 2933 } 2934 2935 const isLivewireForm = Array.from(form.attributes) 2936 .some((attribute) => attribute.name === 'wire:submit' || attribute.name.startsWith( 2937 'wire:submit.')); 2938 2939 if (!isLivewireForm) { 2940 return; 2941 } 2942 2943 const component = form.closest('[wire\\:id]'); 2944 2945 if (component) { 2946 pendingSubmitComponents.add(component.getAttribute('wire:id')); 2947 } 2948 }, true); 2949 2950 Livewire.interceptMessage(({ 2951 message, 2952 onSuccess 2953 }) => { 2954 const component = message.component; 2955 2956 if (!pendingSubmitComponents.delete(component.id)) { 2957 return; 2958 } 2959 2960 onSuccess(({ 2961 onRender 2962 }) => { 2963 onRender(() => { 2964 const messages = component.$wire.$errors.all(); 2965 const firstError = Array.from(component.el.querySelectorAll( 2966 'p, span, small, div, strong')) 2967 .find((element) => messages.includes(element.textContent 2968 .trim())); 2969 2970 firstError?.scrollIntoView({ 2971 behavior: 'smooth', 2972 block: 'center', 2973 }); 2974 }); 2975 }); 2976 }); 2977 }); 2978 </script>
2978 2979 2980
2980<script src="https://lave.live/vendor/livewire/livewire.min.js?id=0ae8dee3" data-csrf="79B8yq4jw64o4Bw286nioXvdhTpv0lXhgw6jxEaX" data-module-url="https://lave.live/livewire-af6929b1" data-update-uri="https://lave.live/livewire-af6929b1/update" data-navigate-once="true"></script>
2980 2981 2982
2982<script> 2983 window.$zoho = window.$zoho || {}; 2984 window.__zohoChatPending = false; 2985 window.__zohoChatSyncTimer = null; 2986 2987 window.__showZohoChat = function() { 2988 if (window.$zoho?.salesiq?.floatwindow?.visible) { 2989 window.$zoho.salesiq.floatwindow.visible('show'); 2990 return true; 2991 } 2992 2993 return false; 2994 }; 2995 2996 window.openZohoChat = function(event) { 2997 if (event) { 2998 event.preventDefault(); 2999 } 3000 3001 if (window.__showZohoChat()) { 3002 return false; 3003 } 3004 3005 window.__zohoChatPending = true; 3006 return false; 3007 }; 3008 3009 $zoho.salesiq = $zoho.salesiq || {}; 3010 $zoho.salesiq.ready = function() { 3011 if (window.$zoho?.salesiq?.floatbutton?.visible) { 3012 window.$zoho.salesiq.floatbutton.visible('hide'); 3013 } 3014 3015 if (window.__zohoChatPending) { 3016 window.__zohoChatPending = false; 3017 window.__showZohoChat(); 3018 } 3019 }; 3020</script>
vendor: 1 bytes, line 3020
3020
3021<script id="zsiqscript" src="https://salesiq.zohopublic.com/widget?wc=siq1cec881323d29b6db65f0661720c7d07" defer> 3022</script>
3022 3023 3024
3024<script> 3025if (!window.laveUploadSizeGuardInstalled) { 3026 window.laveUploadSizeGuardInstalled = true; 3027 document.addEventListener('change', function (event) { 3028 const input = event.target; 3029 if (!(input instanceof HTMLInputElement) || input.type !== 'file' || !input.dataset.uploadMaxKb) return; 3030 const limit = Number(input.dataset.uploadMaxKb); 3031 if (Array.from(input.files || []).some(file => file.size > limit * 1024)) { 3032 event.preventDefault(); 3033 event.stopImmediatePropagation(); 3034 input.value = ''; 3035 alert('This file is too large. Maximum file size is :size MB.'.replace(':size', String(Math.round(limit / 1024 * 100) / 100))); 3036 } 3037 }, true); 3038} 3039</script>
vendor: 1 bytes, line 3039
3039
3040<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a402afad8c6ee804',t:'MTc5MDI2MjMxNQ=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
3040</body> 3041 3042</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.