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