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