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