1<!DOCTYPE html> 2<html lang="id"> 3<head> 4 <meta charset="UTF-8"> 5 <title>LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL </title> 6 <meta name="description" content="IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!."> 7 <meta name="robots" content="index,follow"> 8 <link rel="canonical" href="https://loklokpro.com/"> 9 <meta name="google-site-verification" content="dS8NukqmhZE0Xlt2hv7aaHu5Z0skABDq-RAMgf49UEE" /> 10 <meta name="google-site-verification" content="PffTTNBgs-gxR6cDaxhyzXsSVv307G8bSFLMuSWIpp0" /> 11 <meta name="google-site-verification" content="v6fbSXU8gHq89C6OKR-4e8x6MtPIjhh35GErg9hvevM" /> 12 <meta name="google-site-verification" content="lIQOZHaCPZr0Idg1pXY9Wx-CCh6BAfCa1zBpHMZ9GOc" /> 13 <meta name="keywords" content="iwantogel, dracin terbaru,donghua terbaru" /> 14 <link rel="amphtml" href="https://slot-gacor.loklokpro.com/"> 15 <link rel="alternate" href="https://slot-gacor.loklokpro.com/" hreflang="id"> 16 <link rel="alternate" href="https://slot-gacor.loklokpro.com/" hreflang="en"> 17 <link rel="alternate" href="https://slot-gacor.loklokpro.com/" hreflang="x-default"> 18 <link rel="alternate" hreflang="id-ID" href="https://slot-gacor.loklokpro.com/" /> 19 <link rel="alternate" hreflang="en-US" href="https://slot-gacor.loklokpro.com/" /> 20 <link rel="alternate" hreflang="en-SG" href="https://slot-gacor.loklokpro.com/" /> 21 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 66 bytes, lines 21-22
21 22 <script async src="https://www.googletagmanager.com/gtag/js?id=
22G-0LSM1YJSPZ
vendor: 47 bytes, lines 22-23
22" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 23
23<script nonce="se3iyzEHMc9FQEB4wSyd0w==">window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"granted",wait_for_update:500});gtag("js",new Date);gtag("config","G-0LSM1YJSPZ",{send_page_view:!0,linker:{domains:["plazait.co.id","www.plazait.co.id"]}});window.ga4Ecomm={viewItem:function(e){if(!window.gtag)return;try{gtag("event","view_item",{currency:e.currency||"IDR",value:e.value||0,items:[{item_id:e.item_id||"",item_name:e.item_name||"",item_brand:e.item_brand||"",item_category:e.item_category||"",price:e.price||0,quantity:1}]})}catch(t){}},addToCart:function(e){if(!window.gtag)return;try{gtag("event","add_to_cart",{currency:e.currency||"IDR",value:(e.price||0)*(e.quantity||1),items:[{item_id:e.item_id||"",item_name:e.item_name||"",item_brand:e.item_brand||"",item_category:e.item_category||"",price:e.price||0,quantity:e.quantity||1}]})}catch(t){}}};</script>
23 24 <meta property="og:locale" content="id_ID"> 25 <meta property="og:site_name" content="TOTO TOGEL"> 26 <meta property="og:type" content="product"> 27 <meta property="og:title" content="LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL "> 28 <meta property="og:description" content="IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!."> 29 <meta property="og:url" content="https://loklokpro.com/"> 30 <meta property="og:image" content="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png"> 31 <meta property="og:image:alt" content="LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL "> 32<meta property="product:price:amount" content="5000"> 33<meta property="product:price:currency" content="IDR"> 34<meta property="product:availability" content="out of stock"> 35<meta name="google-site-verification" content="xZhbOCnLFUisoQt6G1ewazv5WqmwQ-bdG6l_-u84nqw" /> 36<meta name="twitter:card" content="summary_large_image"> 37<meta name="twitter:title" content="LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL "> 38<meta name="twitter:description" content="IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!."> 39<meta name="twitter:image" content="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png"> 40
41<script type="application/ld+json"> 42 { 43 "@context": "http:\/\/schema.org", 44 "@type": "BreadcrumbList", 45 "itemListElement": [ 46 { 47 "@type": "ListItem", 48 "item": { 49 "@id": "https://loklokpro.com/", 50 "name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL " 51 }, 52 "position": 1 53 } 54 ] 55 } 56 </script>
56 57
57<script type="application/ld+json"> 58 { 59 "@context": "http:\/\/schema.org", 60 "@type": "Product", 61 "name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL ", 62 "description": "IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!.", 63 "image": "https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png", 64 "offers": { 65 "@type": "http:\/\/schema.org\/Offer", 66 "price": 9999, 67 "url": "https://loklokpro.com/", 68 "priceCurrency": "IDR", 69 "availability": "http:\/\/schema.org\/InStock" 70 } 71 } 72 </script>
72 73
73<script type="application/ld+json"> 74 { 75 "@context": "http:\/\/schema.org\/", 76 "@type": "WebPage", 77 "speakable": { 78 "@type": "SpeakableSpecification", 79 "cssSelector": [".description"], 80 "xpath": ["\/html\/head\/title"] 81 } 82 } 83 </script>
83 84 <!-- ZS --> 85 <!-- Schema JSON untuk SBO -->
86<script type="application/ld+json"> 87{ 88"@context": "https://schema.org/", 89"@type": "Product", 90"name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL ", 91"url": "https://loklokpro.com/", 92"sku": "IWANTOGEL", 93"image": [ 94 "https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" 95], 96"description": "IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!.", 97"brand": { 98 "@type": "Organization", 99 "name": "IWAN TOGEL" 100}, 101"offers": { 102 "@type": "Offer", 103 "name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL ", 104 "sku": "LOGIN IWANTOGEL", 105 "price": "9999", 106 "priceCurrency": "IDR", 107 "availability": "https://schema.org/InStock" 108} 109} 110</script>
110 111
112<script type="application/ld+json"> 113{ 114"@context": "https://schema.org/", 115"@type": "Product", 116"name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL ", 117"image": "https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png", 118"description": "IWANTOGEL Merupakan Tempat Kumpulnya Vidio Game Online Terlaris di seluruh Dunia Dengan Updatenya permainan Terbaru dan Vidio live Score Setiap harinya Serta Dukungan Akses Stabil Dan navigasi Sederhana Login Dan nikmati Bersama IWANTOGEL Sekarang juga!!.", 119"sku": "TOGEL ONLINE", 120"offers": { 121 "@type": "Offer", 122 "priceCurrency": "IDR", 123 "url": "https://loklokpro.com/", 124 "availability": "https://schema.org/InStock", 125 "price": "9999.00" 126} 127} 128</script>
128 129
130<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 131{ 132"@context": "https://schema.org/", 133"@id": "https://loklokpro.com/", 134"aggregateRating": { 135 "@type": "AggregateRating", 136 "itemReviewed": { 137 "@type": "IndividualProduct", 138 "name": "IWANTOGEL" 139 }, 140 "ratingValue": "5.0", 141 "reviewCount": 954638, 142 "bestRating": 5 143} 144} 145</script>
145 146
147<script type="application/ld+json"> 148{ 149"@context": "https://schema.org", 150"@type": "BreadcrumbList", 151"itemListElement": [ 152 { 153 "@type": "ListItem", 154 "position": 1, 155 "item": { 156 "@id": "https://loklokpro.com/", 157 "name": "IWANTOGEL" 158 } 159 }, 160 { 161 "@type": "ListItem", 162 "position": 2, 163 "item": { 164 "@id": "https://loklokpro.com/", 165 "name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL " 166 } 167 }, 168 { 169 "@type": "ListItem", 170 "position": 3, 171 "item": { 172 "@id": "https://loklokpro.com/", 173 "name": "IWAN TOGEL" 174 } 175 }, 176 { 177 "@type": "ListItem", 178 "position": 4, 179 "item": { 180 "@id": "https://loklokpro.com/", 181 "name": "LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL " 182 } 183 } 184] 185} 186</script>
186 187
188<script type="application/ld+json" id="bv-jsonld-reviews-data"> 189{ 190 "@context": "https://schema.org", 191 "@type": "Organization", 192 "@id": "https://loklokpro.com/", 193 "name": "LOGIN IWANTOGEL", 194 "alternateName": "BANDAR TOGEL ONLINE", 195 "url": "https://loklokpro.com/", 196 "review": [ 197 { 198 "@type": "Review", 199 "dateCreated": "2026-03-31T12:28:35+00:00", 200 "datePublished": "2026-03-31T12:28:35+00:00", 201 "headline": "DAFTAR IWANTOGEL", 202 "reviewBody": "IWANTOGEL benar-benar memudahkan akses login bandar togel online terpercaya 24 jam nonstop antarmuka yang elegan dan proses masuk yang cepat membuat saya bisa bertaruh kapan saja tanpa gangguan teknis sehingga pengalaman bermain terasa lebih mewah dan efisien.", 203 "reviewRating": { 204 "@type": "Rating", 205 "ratingValue": 5, 206 "bestRating": 5, 207 "worstRating": 1 208 }, 209 "author": { 210 "@type": "Person", 211 "name": "Taufan â Surabaya" 212 } 213 }, 214 { 215 "@type": "Review", 216 "dateCreated": "2026-04--02T18:28:21+00:00", 217 "datePublished": "2026-04-03T18:28:21+00:00", 218 "headline": "LINK IWANTOGEL", 219 "reviewBody": "Saya mengapresiasi bagaimana IWANTOGEL menyediakan proses deposit cepat yang jelas dan transparan; konfirmasi transaksi berjalan instan sehingga waktu menunggu berkurang dan pemain bisa langsung fokus pada strategi permainan.", 220 "reviewRating": { 221 "@type": "Rating", 222 "ratingValue": 5, 223 "bestRating": 5, 224 "worstRating": 1 225 }, 226 "author": { 227 "@type": "Person", 228 "name": "Thomas Tampi â Semarang" 229 } 230 }, 231 { 232 "@type": "Review", 233 "dateCreated": "2026-04-05T19:28:18+00:00", 234 "datePublished": "2026-04-05T19:28:18+00:00", 235 "headline": "SLOT GACOR", 236 "reviewBody": "Keamanan data di IWANTOGEL memberi rasa tenang yang luar biasa; fitur enkripsi dan proteksi akun bekerja baik sehingga informasi pribadi dan riwayat transaksi tetap aman, membuat platform ini tampak sangat profesional dan dapat dipercaya.", 237 "reviewRating": { 238 "@type": "Rating", 239 "ratingValue": 5, 240 "bestRating": 5, 241 "worstRating": 1 242 }, 243 "author": { 244 "@type": "Person", 245 "name": "Soesilowati â Makassar" 246 } 247 } 248 ] 249} 250</script>
vendor: 1 bytes, line 250
250
251<script type="application/ld+json"> 252{ 253 "@context": "https://schema.org", 254 "@type": "FAQPage", 255 "mainEntity": [ 256 { 257 "@type": "Question", 258 "name": "Apa itu IWANTOGEL dan mengapa disebut bandar togel online terpercaya 24 jam nonstop?", 259 "acceptedAnswer": { 260 "@type": "Answer", 261 "text": "IWANTOGEL adalah platform bandar togel online yang menyediakan akses login 24 jam nonstop, didesain untuk kemudahan pengguna dengan antarmuka elegan dan sistem yang handal. Disebut terpercaya karena menerapkan protokol keamanan, verifikasi transaksi, dan layanan pelanggan penuh waktu yang memastikan pengalaman taruhan yang aman, konsisten, dan minim gangguan." 262 } 263}, 264{ 265 "@type": "Question", 266 "name": "Bagaimana proses deposit cepat bekerja di IWANTOGEL dan apa manfaatnya bagi pemain?", 267 "acceptedAnswer": { 268 "@type": "Answer", 269 "text": "Proses deposit cepat di IWANTOGEL mengandalkan integrasi dengan metode pembayaran modern dan sistem konfirmasi otomatis sehingga dana tercatat hampir seketika. Manfaatnya termasuk pengurangan waktu tunggu sebelum taruhan bisa ditempatkan, minimnya risiko kehilangan peluang taruhan, dan alur bermain yang lebih efisien serta menyenangkan." 270 } 271}, 272{ 273 "@type": "Question", 274 "name": "Seberapa kuat perlindungan keamanan data di IWANTOGEL dan fitur apa yang digunakan?", 275 "acceptedAnswer": { 276 "@type": "Answer", 277 "text": "IWANTOGEL menerapkan enkripsi tingkat tinggi, proteksi berlapis untuk autentikasi pengguna, dan kebijakan privasi ketat yang melindungi informasi pribadi serta transaksi. Fitur seperti enkripsi SSL/TLS, pemantauan aktivitas mencurigakan, dan kontrol akses membantu meminimalkan risiko kebocoran data dan penipuan, sehingga pemain dapat bermain dengan tenang." 278 } 279}, 280{ 281 "@type": "Question", 282 "name": "Apakah layanan pelanggan IWANTOGEL benar-benar tersedia 24 jam nonstop dan saluran apa saja yang bisa digunakan?", 283 "acceptedAnswer": { 284 "@type": "Answer", 285 "text": "Ya, IWANTOGEL menawarkan layanan pelanggan 24 jam nonstop melalui beberapa saluran seperti live chat, email, dan pusat bantuan di situs. Tim dukungan dilatih untuk menangani pertanyaan teknis, panduan deposit, dan masalah akun secara responsif sehingga pemain mendapatkan bantuan cepat untuk menjaga kelancaran taruhan." 286 } 287}, 288{ 289 "@type": "Question", 290 "name": "Bagaimana IWANTOGEL memastikan pengalaman login yang mudah namun tetap aman?", 291 "acceptedAnswer": { 292 "@type": "Answer", 293 "text": "IWANTOGEL menggabungkan desain antarmuka yang intuitif dengan langkah keamanan seperti verifikasi dua faktor bila diperlukan, enkripsi data, dan pemeriksaan aktivitas tidak biasa. Pendekatan ini memastikan pengguna dapat masuk dengan cepat tanpa mengorbankan integritas akun, sehingga akses login tetap nyaman dan terlindungi." 294 } 295} 296 ] 297} 298</script>
298 299 <!-- User Session Data --> 300 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 301 <meta name="user-logged-in" content="false"> 302 <meta name="user-id" content="0"> 303 304 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 305 306 <link rel="shortcut icon" href="https://area-terpercaya.b-cdn.net/newicon-iwantogel.png" type="image/x-icon"> 307 <style data-inlined="style.css"> 308@font-face { 309 font-family: 'Segoe UI'; 310 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 311 url('/assets/fonts/segoeui.woff') format('woff'); 312 font-weight: 400; 313 font-style: normal; 314} 315 316:root { 317 --brand-50:#FFF5E6; 318 --brand-100:#FFE7CC; 319 --brand-500:#F28C1A; 320 --brand-600:#D97809; 321 --gray-900:#1F1F23; 322 --gray-700:#3D3F45; 323 --gray-500:#6F737B; 324 --gray-300:#C5C9CF; 325 --gray-100:#F3F5F7; 326 --border:#E4E7EB; 327 --radius:8px; 328 --transition: .18s ease; 329 /* font-family: "Inter", system-ui, sans-serif; */ 330 font-family: 'Segoe UI', sans-serif; 331} 332 333* { box-sizing: border-box; } 334body { 335 margin:0; 336 padding-bottom: 0; /* Reset for desktop */ 337 color:var(--gray-900); 338 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 339} 340img { max-width:100%; display:block; } 341 342.container { 343 width:100%; 344 max-width:1280px; 345 margin:0 auto; 346 padding:10px 0; 347} 348 349.topbar { 350 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 351 color:#fff; 352 font-size:13px; 353} 354.topbar-inner { 355 display:flex; 356 justify-content:space-between; 357 align-items:center; 358 min-height:36px; 359} 360.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 361 362/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 363.navbar { 364 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 365 backdrop-filter:blur(12px); 366 -webkit-backdrop-filter:blur(12px); 367 border-bottom:1px solid rgba(228,231,235,0.6); 368 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 369 position:sticky; 370 top:0; 371 z-index:500; 372 transition:all .3s ease; 373} 374 375/* Accent line gradient orange */ 376.navbar::after { 377 content:''; 378 position:absolute; 379 bottom:0; 380 left:0; 381 right:0; 382 height:2px; 383 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 384 opacity:0.5; 385} 386 387.nav-inner { 388 display:flex; 389 align-items:center; 390 gap:24px; 391 min-height:70px; 392} 393.nav-inner .search .m-search-ico { display: none; } 394 395/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 396.login-btn .login-ico { display:none; } 397.navbar-logo { 398 display: none; 399 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 400 backdrop-filter:blur(12px); 401 -webkit-backdrop-filter:blur(12px); 402 border-bottom:1px solid rgba(228,231,235,0.6); 403 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 404 position:sticky; 405 top:0; 406 z-index:500; 407} 408 409/* Hide bottom navigation on desktop - only show on mobile */ 410.bottom-nav { 411 display: none; 412} 413 414/* ============================================================== 415 MOBILE (<=640px) 416 ============================================================== */ 417@media (max-width:640px){ 418 .container { padding:10px 16px; } 419 420 .topbar { display:none !important; } 421 422 /* Add padding bottom for sticky bottom nav */ 423 body { 424 padding-bottom: 68px; /* Height of bottom nav + safe area */ 425 } 426 427 /* Mobile Top Navbar - Clean & Minimal */ 428 .navbar { 429 background:linear-gradient(135deg, rgba(0,0,0,0.97) 0%, rgba(130, 22, 3,0.9) 100%); 430 backdrop-filter:blur(16px); 431 -webkit-backdrop-filter:blur(16px); 432 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 433 border: 0; 434 } 435 436 /* Simple grid layout - Logo, Search, Category */ 437 .nav-inner { 438 display:grid; 439 grid-template-columns:1fr; 440 gap:10px; 441 padding:12px 16px 0 12px; 442 min-height:auto; 443 } 444 445 /* Logo centered at top */ 446 .nav-inner .logo { 447 justify-self:center; 448 margin:0; 449 display:flex; 450 } 451 .nav-inner .logo img { height:36px; } 452 453 /* Search bar - full width */ 454 .nav-inner .search { 455 position:relative; 456 display:flex; 457 align-items:center; 458 gap:0; 459 width:100%; 460 height:44px; 461 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 462 border:1.5px solid #e5e7eb; 463 border-radius:12px; 464 padding:0 12px; 465 box-shadow:0 1px 3px rgba(0,0,0,0.04); 466 transition:all .25s; 467 } 468 .nav-inner .search:focus-within { 469 border-color:#fb923c; 470 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 471 } 472 473 .nav-inner .search-icon-btn { 474 color:#9ca3af; 475 padding:4px; 476 margin-right:4px; 477 } 478 479 .nav-inner .search-icon-btn svg { 480 width:18px; 481 height:18px; 482 } 483 484 .nav-inner .search input { 485 flex:1; 486 min-width:0; 487 border:none; 488 outline:none; 489 background:transparent; 490 font-size:14px; 491 padding:0 8px; 492 height:100%; 493 } 494 495 .nav-inner .ai-bot-toggle { 496 padding:6px; 497 margin-left:0; 498 } 499 500 .nav-inner .ai-bot-toggle .bot-icon { 501 width:20px; 502 height:20px; 503 } 504 505 .nav-inner .search-suggest { 506 left:0; 507 right:0; 508 width:calc(100vw - 32px); 509 margin-top:6px; 510 border-radius:12px; 511 max-height:70vh; 512 } 513 514 /* Hide desktop nav actions but keep structure for userMenu */ 515 .nav-inner .nav-actions { 516 display:flex !important; /* Keep flex to allow userMenu positioning */ 517 visibility:hidden; /* Hide visually but keep in DOM */ 518 pointer-events:none; /* Disable interactions with hidden elements */ 519 position:absolute; /* Remove from flow */ 520 right:0; 521 top:0; 522 } 523 524 /* Re-enable userMenu for logged-in users */ 525 .nav-inner .nav-actions #userMenu { 526 visibility:visible !important; 527 pointer-events:auto !important; 528 } 529 530 .nav-inner .logo { display: none; } 531 .navbar-logo { 532 display: block; 533 /* background:linear-gradient(135deg, rgba(0,0,0,0.97) 0%, rgba(130, 22, 3,0.9) 100%); */ 534 backdrop-filter:blur(16px); 535 -webkit-backdrop-filter:blur(16px); 536 position: static; /* Remove sticky on mobile */ 537 box-shadow: none; 538 border-bottom: 0; 539 padding-top: .6rem; 540 } 541 .nav-inner .logo-mobile { display: block; } 542 543 /* Category menu wrapper will handle visibility */ 544 545 /* ============================================================== 546 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 547 ============================================================== */ 548 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 549 550 /* Hide desktop elements that now moved to bottom nav */ 551 #wishlistBtn, 552 #notifBtn, 553 #loginBtn { display:none !important; } 554 555 /* Hide desktop user menu on mobile - use bottom sheet instead */ 556 .navbar #userMenu { 557 display:none !important; 558 } 559 560 .nav-actions .cart-indicator, 561 .nav-actions .compare-indicator { 562 display:none !important; 563 } 564 565 /* Hide category button from bottom nav */ 566 #bottomCategoryBtn { 567 display: none !important; 568 } 569 570 /* User menu button styling for mobile - scoped to navbar only */ 571 .navbar #userMenu .login-btn.logged-in { 572 width:40px; 573 height:40px; 574 padding:0; 575 border-radius:50%; 576 display:flex; 577 align-items:center; 578 justify-content:center; 579 background:rgba(0,0,0,0.95); 580 backdrop-filter:blur(12px); 581 -webkit-backdrop-filter:blur(12px); 582 border:1px solid rgba(228,231,235,0.6); 583 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 584 } 585 586 .navbar #userMenu .login-btn.logged-in .login-text { 587 display:none; 588 } 589 590 .navbar #userMenu .login-btn.logged-in .login-ico { 591 display:inline; 592 font-size:20px; 593 line-height:1; 594 } 595 596 /* User dropdown positioning for mobile */ 597 .navbar #userMenu .user-dropdown { 598 position:fixed !important; 599 top:60px !important; 600 right:16px !important; 601 left:auto !important;
602 min-width:200px; 603 background:rgba(0,0,0,0.98); 604 backdrop-filter:blur(20px); 605 -webkit-backdrop-filter:blur(20px); 606 border:1px solid rgba(228,231,235,0.6); 607 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 608 z-index:1002 !important; 609 border-radius:12px; 610 padding:8px 0; 611 } 612 613 /* Force visibility when not hidden attribute */ 614 .navbar #userMenu .user-dropdown:not([hidden]) { 615 display:block !important; 616 visibility:visible !important; 617 opacity:1 !important; 618 } 619 620 /* Container untuk kategori dan tombol kategori sejajar */ 621 .navbar .container:last-child { 622 display:flex !important; 623 align-items:center !important; 624 padding:4px 16px 6px !important; 625 gap:5px; 626 overflow:visible; 627 } 628 629 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 630 .mega-cats-wrapper { 631 position:relative; 632 flex:1; 633 overflow:hidden; 634 min-height:34px; /* Ensure content is visible */ 635 display:flex; /* Ensure wrapper is flex container */ 636 } 637 638 /* Fade gradient indicator di ujung kanan (mobile only) */ 639 .mega-cats-fade { 640 position:absolute; 641 top:0; 642 right:0; 643 bottom:0; 644 width:80px; /* Increased from 60px for better visibility */ 645 background:linear-gradient(to left, 646 rgba(130, 22, 3,1) 0%, 647 rgba(130, 22, 3,0.98) 15%, 648 rgba(130, 22, 3,0.85) 35%, 649 transparent 100%); 650 pointer-events:none; 651 z-index:5; /* Increased z-index */ 652 transition:opacity 0.3s ease; 653 } 654 655 /* Kategori menu di mobile */ 656 .mega-cats.container { 657 display:flex !important; 658 gap:12px; 659 overflow-x:auto; 660 overflow-y:visible; 661 -webkit-overflow-scrolling:touch; 662 scrollbar-width:none; 663 flex:1; 664 padding:0 !important; 665 margin:0 !important; 666 position:relative; /* For z-index stacking */ 667 z-index:1; /* Below fade gradient */ 668 } 669 .mega-cats.container::-webkit-scrollbar { display:none; } 670 671 /* Styling untuk menu items di mobile */ 672 .mega-cats a { 673 font-size:13px; 674 padding:6px 10px; 675 white-space:nowrap; 676 flex-shrink:0; 677 position:relative; /* Ensure proper stacking */ 678 } 679 .mega-cats a.active { 680 background:var(--brand-600); 681 color:#fff; 682 font-weight:600; 683 border-radius:6px; 684 } 685 686 /* Tombol kategori di mobile */ 687 .mm-wp { 688 flex-shrink:0; 689 } 690 .mm2-trigger { 691 display:flex; 692 align-items:center; 693 gap:6px; 694 padding:6px 12px; 695 background:var(--gray-100); 696 color:#FFFFFF; 697 text-decoration:none; 698 border-radius:6px; 699 font-size:13px; 700 white-space:nowrap; 701 } 702 703 .mm2-trigger .label-cat {display: none; } 704 /* Tombol more kategori */ 705 .mega-more-btn { 706 border:1px solid var(--border); 707 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 708 border-radius:10px; 709 padding:6px 14px; 710 font-size:18px; 711 cursor:pointer; 712 } 713 .mega-cats-more.active > .mega-more-btn { 714 background:var(--brand-600); 715 color:#fff; 716 border-color:var(--brand-600); 717 } 718 .mega-more-dropdown { 719 top:calc(100% + 10px); 720 right:0; 721 min-width:85vw; 722 max-width:90vw; 723 border-radius:16px; 724 } 725 .mega-more-grid { 726 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 727 } 728 729 /* ============================================================== 730 MOBILE USER BOTTOM SHEET 731 ============================================================== */ 732 .mobile-user-sheet { 733 position:fixed; 734 top:0; 735 left:0; 736 right:0; 737 bottom:0; 738 z-index:10000; 739 pointer-events:none; 740 } 741 742 .mobile-user-sheet.active { 743 pointer-events:auto; 744 } 745 746 .mobile-user-sheet-backdrop { 747 position:absolute; 748 top:0; 749 left:0; 750 right:0; 751 bottom:0; 752 background:rgba(0,0,0,0.5); 753 opacity:0; 754 transition:opacity .3s ease; 755 } 756 757 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 758 opacity:1; 759 } 760 761 .mobile-user-sheet-content { 762 position:absolute; 763 bottom:0; 764 left:0; 765 right:0; 766 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 767 border-radius:20px 20px 0 0; 768 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 769 transform:translateY(100%); 770 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 771 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 772 } 773 774 .mobile-user-sheet.active .mobile-user-sheet-content { 775 transform:translateY(0); 776 } 777 778 .mobile-user-sheet-header { 779 padding:16px 20px 12px; 780 border-bottom:1px solid #e5e7eb; 781 } 782 783 .mobile-user-sheet-handle { 784 width:40px; 785 height:4px; 786 background:#d1d5db; 787 border-radius:2px; 788 margin:0 auto 16px; 789 } 790 791 .mobile-user-info { 792 display:flex; 793 align-items:center; 794 gap:12px; 795 } 796 797 .mobile-user-avatar { 798 width:48px; 799 height:48px; 800 border-radius:50%; 801 background:linear-gradient(135deg, #FFE7CC, #FFD9A8); 802 display:flex; 803 align-items:center; 804 justify-content:center; 805 font-size:24px; 806 flex-shrink:0; 807 } 808 809 .mobile-user-details { 810 flex:1; 811 min-width:0; 812 } 813 814 .mobile-user-name { 815 font-size:16px; 816 font-weight:600; 817 color:#111827; 818 margin-bottom:2px; 819 white-space:nowrap; 820 overflow:hidden; 821 text-overflow:ellipsis; 822 } 823 824 .mobile-user-email { 825 font-size:13px; 826 color:#6b7280; 827 white-space:nowrap; 828 overflow:hidden; 829 text-overflow:ellipsis; 830 } 831 832 .mobile-user-sheet-body { 833 padding:8px 0; 834 } 835 836 .mobile-user-sheet-item { 837 display:flex; 838 align-items:center; 839 gap:16px; 840 padding:14px 20px; 841 text-decoration:none; 842 color:#374151; 843 font-size:15px; 844 transition:background .2s; 845 border:none; 846 background:none; 847 width:100%; 848 text-align:left; 849 cursor:pointer; 850 } 851 852 .mobile-user-sheet-item:active { 853 background:#f3f4f6; 854 } 855 856 .mobile-user-sheet-icon { 857 font-size:20px; 858 width:24px; 859 text-align:center; 860 flex-shrink:0; 861 } 862 863 .mobile-user-sheet-label { 864 flex:1; 865 font-weight:500; 866 } 867 868 .mobile-user-sheet-divider { 869 height:1px; 870 background:#e5e7eb; 871 margin:8px 0; 872 } 873} 874 875/* END NAVBAR */ 876 877.logo img { display:block; } 878.search { 879 flex:1; 880 position:relative; 881 display:flex; 882 align-items:center; 883 gap:0; 884 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 885 border:1.5px solid #e5e7eb; 886 border-radius:12px; 887 padding:0 16px; 888 height:38px; 889 transition:all 0.3s ease; 890} 891.search:focus-within { 892 border-color:#fb923c; 893 box-shadow:0 0 0 3px rgba(251, 146, 60, 0.1); 894} 895.search input { 896 flex:1; 897 border:none; 898 outline:none; 899 background:transparent; 900 font-size:15px; 901 padding:0 12px; 902 height:100%; 903 color:#1f2937; 904} 905.search input::placeholder { 906 color:#9ca3af; 907} 908.search-icon-btn { 909 background:transparent; 910 border:none; 911 padding:0; 912 display:flex; 913 align-items:center; 914 justify-content:center; 915 cursor:pointer;
916 color:#6b7280; 917 transition:color 0.2s ease; 918 flex-shrink:0; 919} 920.search-icon-btn:hover { 921 color:#fb923c; 922} 923.search-suggest { 924 position:absolute; 925 left:0; right:0; top:100%; 926 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 927 border:1px solid var(--border); 928 border-radius:var(--radius); 929 padding:8px 0; 930 box-shadow:0 8px 20px rgba(0,0,0,.08); 931 margin-top:4px; 932 font-size:14px; 933 max-height:260px; 934 overflow:auto; 935} 936.search-suggest div { 937 padding:6px 12px; 938 cursor:pointer; 939} 940.search-suggest div:hover { background:var(--gray-100); } 941 942.nav-actions { display:flex; gap:10px; align-items:center; } 943.icon-btn, .login-btn { 944 background:linear-gradient(135deg, rgba(0,0,0,0.95), rgba(249,250,251,0.9)); 945 border:1px solid rgba(228,231,235,0.6); 946 padding:8px 12px; 947 border-radius:var(--radius); 948 cursor:pointer; 949 font-size:14px; 950 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 951 height: 38px; 952 position:relative; 953 box-shadow: 954 0 1px 3px rgba(0,0,0,0.06), 955 0 0 0 1px rgba(0,0,0,0.5) inset; 956 backdrop-filter:blur(8px); 957 -webkit-backdrop-filter:blur(8px); 958} 959.icon-btn:hover, .login-btn:hover { 960 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 961 border-color:rgba(209,213,219,0.8); 962 box-shadow: 963 0 2px 8px rgba(0,0,0,0.08), 964 0 0 0 1px rgba(0,0,0,0.6) inset; 965 transform:translateY(-1px); 966} 967.cart-indicator, .compare-indicator { position:relative; } 968.badge { 969 background:linear-gradient(135deg, #EF4444, #DC2626); 970 color:#fff; 971 font-size:10.5px; 972 font-weight:600; 973 padding:2px 6px; 974 border-radius:999px; 975 position:absolute; 976 top:-6px; 977 right:-6px; 978 min-width:18px; 979 height:18px; 980 display:flex; 981 align-items:center; 982 justify-content:center; 983 box-shadow: 984 0 2px 8px rgba(239,68,68,0.4), 985 0 0 0 2px #fff, 986 0 0 12px -2px rgba(239,68,68,0.6); 987 transition:all 0.3s ease; 988} 989.icon-btn:hover .badge { 990 transform:scale(1.08); 991 box-shadow: 992 0 4px 12px rgba(239,68,68,0.5), 993 0 0 0 2px #fff, 994 0 0 16px -2px rgba(239,68,68,0.7); 995} 996.badge.is-empty { display:none; } 997 998/* User Menu Dropdown */ 999.user-menu { 1000 position:relative; 1001} 1002.user-dropdown { 1003 position:absolute; 1004 top:calc(100% + 8px); 1005 right:0; 1006 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 1007 border:1px solid var(--border); 1008 border-radius:12px; 1009 box-shadow:0 10px 40px rgba(0,0,0,0.12);
1010 min-width:200px; 1011 padding:8px 0; 1012 z-index:9999; 1013} 1014 1015/* Ensure dropdown is visible when not hidden */ 1016.user-dropdown:not([hidden]) { 1017 display:block !important; 1018 visibility:visible !important; 1019 opacity:1 !important; 1020} 1021 1022/* Mobile: Force user dropdown to be visible above everything */ 1023@media (max-width:640px) { 1024 .user-dropdown:not([hidden]) { 1025 position:fixed !important; 1026 z-index:9999 !important; 1027 top:60px !important; 1028 right:16px !important; 1029 } 1030} 1031 1032.dropdown-item { 1033 display:flex; 1034 align-items:center; 1035 gap:12px; 1036 padding:10px 16px; 1037 text-decoration:none; 1038 color:#FFFFFF; 1039 font-size:14px; 1040 transition:var(--transition); 1041 border:none; 1042 background:none; 1043 width:100%; 1044 text-align:left; 1045 cursor:pointer; 1046} 1047.dropdown-item:hover { 1048 background:var(--gray-100); 1049 color:var(--gray-900); 1050} 1051.dropdown-icon { 1052 font-size:16px; 1053 opacity:0.7; 1054} 1055.dropdown-separator { 1056 height:1px; 1057 background:var(--border); 1058 margin:8px 0; 1059} 1060.login-btn.logged-in { 1061 background:linear-gradient(135deg, rgba(130, 22, 3,0.8), rgba(255,231,204,0.6)); 1062 border:1px solid rgba(242,140,26,0.2); 1063 color:var(--brand-700); 1064 max-width:200px; /* Prevent button from getting too wide */ 1065 display:inline-flex; 1066 align-items:center; 1067 gap:10px; 1068 padding:4px 12px 4px 4px; 1069 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1070 position:relative; 1071 overflow:hidden; 1072 box-shadow: 1073 0 1px 3px rgba(242,140,26,0.08), 1074 0 0 0 1px rgba(0,0,0,0.5) inset, 1075 0 0 12px -6px rgba(242,140,26,0.1); 1076 backdrop-filter:blur(8px); 1077 -webkit-backdrop-filter:blur(8px); 1078} 1079/* Subtle background glow effect */ 1080.login-btn.logged-in::before { 1081 content:''; 1082 position:absolute; 1083 top:50%; 1084 left:0; 1085 width:32px; 1086 height:32px; 1087 background:radial-gradient(circle, rgba(242,140,26,0.15), transparent 70%); 1088 transform:translateY(-50%); 1089 opacity:0; 1090 transition:opacity 0.3s ease; 1091} 1092.login-btn.logged-in:hover::before { 1093 opacity:1; 1094} 1095.login-btn.logged-in .user-avatar { 1096 width:32px; 1097 height:32px; 1098 border-radius:50%; 1099 background:linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 1100 color:#D97809; 1101 font-size:11.5px; 1102 font-weight:700; 1103 display:flex; 1104 align-items:center; 1105 justify-content:center; 1106 flex-shrink:0; 1107 letter-spacing:0.3px; 1108 position:relative; 1109 box-shadow: 1110 0 2px 8px rgba(242,140,26,0.25), 1111 0 1px 3px rgba(0,0,0,0.12), 1112 inset 0 1px 0 rgba(0,0,0,0.5); 1113 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1114} 1115/* Ring gradient border */ 1116.login-btn.logged-in .user-avatar::before { 1117 content:''; 1118 position:absolute; 1119 inset:-2px; 1120 border-radius:50%; 1121 padding:2px; 1122 background:linear-gradient(135deg, #F28C1A, #FFD9A8, #F28C1A); 1123 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1124 -webkit-mask-composite:xor; 1125 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1126 mask-composite:exclude; 1127 opacity:0; 1128 transition:opacity 0.3s ease; 1129} 1130.login-btn.logged-in:hover .user-avatar::before { 1131 opacity:1; 1132} 1133.login-btn.logged-in:hover .user-avatar { 1134 transform:scale(1.08) rotate(-3deg); 1135 box-shadow: 1136 0 4px 12px rgba(242,140,26,0.35), 1137 0 2px 6px rgba(0,0,0,0.15), 1138 inset 0 1px 0 rgba(0,0,0,0.6); 1139} 1140.login-btn.logged-in .login-text { 1141 overflow:hidden; 1142 text-overflow:ellipsis; 1143 white-space:nowrap; 1144 flex:1; 1145 min-width:0; 1146 font-weight:500; 1147 font-size:14px; 1148 transition:color 0.2s ease; 1149} 1150.login-btn.logged-in:hover .login-text { 1151 color:var(--brand-600); 1152} 1153.login-btn.logged-in:hover { 1154 background:linear-gradient(135deg, rgba(130, 22, 3,0.95), rgba(255,231,204,0.85)); 1155 border-color:rgba(242,140,26,0.3); 1156 box-shadow: 1157 0 4px 16px -4px rgba(242,140,26,0.25), 1158 0 0 0 1px rgba(242,140,26,0.1) inset, 1159 0 1px 2px rgba(0,0,0,0.05); 1160 transform:translateY(-1px); 1161} 1162 1163/* Desktop: hide wrapper elements, show menu normally */ 1164@media (min-width: 761px) { 1165 .mega-cats-wrapper { 1166 display:contents; /* Remove wrapper from layout */ 1167 } 1168 .mega-cats-fade { 1169 display:none !important; /* Hide fade on desktop */ 1170 } 1171} 1172 1173.mega-cats { 1174 display:flex; 1175 gap:5px; 1176 /* padding:6px 16px 10px; */ 1177 scrollbar-width:none; 1178 position: relative; 1179 z-index: 510; 1180 overflow-x:auto; 1181 -webkit-overflow-scrolling:touch; 1182 flex:1; 1183} 1184.mega-cats::-webkit-scrollbar { 1185 display:none; 1186} 1187.mega-cats a { 1188 text-decoration:none; 1189 font-size:14px; 1190 color:#FFFFFF; 1191 padding:6px 10px; 1192 border-radius:6px; 1193 white-space:nowrap; 1194 transition:var(--transition); 1195} 1196.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1197/*.mega-cats::after {
1198 content:''; 1199 position:absolute; 1200 right:0; 1201 top:0; 1202 width:40px; 1203 height:100%; 1204 background:linear-gradient(to right,rgba(0,0,0,0),#fff); 1205 pointer-events:none; 1206}*/ 1207 1208/* Tambahan untuk tombol More & dropdown. 1209 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1210 1211.mega-cats-more { 1212 position:relative; 1213 display:flex; 1214 align-items:center; 1215} 1216 1217.mega-cats-more.active > .mega-more-btn { 1218 background:var(--brand-600); 1219 color:#fff; 1220} 1221 1222.mega-more-btn { 1223 cursor:pointer; 1224 border:none; 1225 background:var(--gray-100,#f5f5f5); 1226 color:var(--gray-700,#333); 1227 padding:6px 12px; 1228 font-size:16px; 1229 line-height:1; 1230 border-radius:6px; 1231 display:flex; 1232 align-items:center; 1233 justify-content:center; 1234 transition:var(--transition); 1235} 1236.mega-more-btn:hover { 1237 background:var(--brand-100); 1238 color:var(--brand-600); 1239} 1240 1241.mega-more-dropdown { 1242 position:absolute; 1243 top:100%; 1244 right:0; 1245 margin-top:8px; 1246 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 1247 border:1px solid var(--gray-200,#e3e6e9); 1248 border-radius:10px; 1249 padding:12px 14px 14px; 1250 min-width:400px; 1251 max-width:520px; 1252 max-height:420px; 1253 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1254 z-index: 3000; 1255} 1256 1257.mega-more-grid { 1258 display:grid; 1259 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1260 gap:8px 10px; 1261 overflow-x: hidden; 1262 max-height:380px; 1263 overscroll-behavior:contain; 1264 scrollbar-width:thin; 1265} 1266.mega-more-grid::-webkit-scrollbar {
1267 width:6px; 1268} 1269.mega-more-grid::-webkit-scrollbar-thumb { 1270 background:var(--gray-300,#c7ccd1); 1271 border-radius:3px; 1272} 1273 1274/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1275 Kita tambahkan state active agar konsisten. */ 1276.mega-more-grid a.active { 1277 background:var(--brand-600); 1278 color:#fff; 1279 font-weight:600; 1280} 1281.hero { 1282 background:linear-gradient(135deg,var(--brand-50), #fff); 1283} 1284.hero-inner { 1285 display:grid; 1286 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1287 gap:48px; 1288 align-items:center; 1289} 1290.hero-text h1 { 1291 font-size:48px; 1292 line-height:1.1; 1293 margin:0; 1294 font-weight:600; 1295 font-family:"Poppins",sans-serif; 1296} 1297.hero-text .text-brand { color:var(--brand-500); } 1298.lead { font-size:18px; color:#FFFFFF; margin-top:16px; } 1299.hero-cta { margin-top:28px; display:flex; gap:16px; } 1300.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1301.hero-box { 1302 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 1303 padding:10px; 1304 border-radius:16px; 1305 box-shadow:0 4px 18px rgba(0,0,0,.08); 1306 /* min-height:320px; */ 1307 display:flex; 1308 align-items:center; 1309 justify-content:center; 1310} 1311.hero-box img {border-radius: 10px; } 1312 1313@media (max-width:680px) { 1314 .hero-inner { gap: 25px; } 1315} 1316 1317.usp-section { 1318 position:relative; 1319 background: 1320 linear-gradient(180deg,#FFFDFB 0%, #FFFFFF 60%) ; 1321 border-top:1px solid var(--border); 1322 border-bottom:1px solid var(--border); 1323 padding:10px 0 10px; 1324} 1325 1326.usp-list { 1327 list-style:none; 1328 margin:0; 1329 padding:0; 1330 display:grid; 1331 gap:26px; 1332} 1333 1334.usp-list.variant-soft { 1335 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1336} 1337 1338.usp-item { 1339 display:flex; 1340 gap:16px; 1341 align-items:flex-start; 1342 background:#fff; 1343 padding:18px 18px 18px 16px; 1344 border:1px solid rgba(226,226,226,0.7); 1345 border-radius:18px; 1346 position:relative; 1347 overflow:hidden; 1348 isolation:isolate; 1349 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1350 transition: .35s cubic-bezier(.16,.8,.32,1); 1351} 1352 1353.usp-item::before { 1354 content:""; 1355 position:absolute; 1356 inset:0; 1357 background:radial-gradient(circle at 18% 22%, rgba(242,140,26,.18), transparent 60%); 1358 opacity:.65; 1359 transition:.5s; 1360 z-index:-1; 1361} 1362 1363.usp-item:hover { 1364 transform:translateY(-6px); 1365 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1366 border-color:rgba(242,140,26,.35); 1367} 1368 1369.usp-item:hover::before { 1370 background:radial-gradient(circle at 70% 40%, rgba(242,140,26,.22), transparent 65%); 1371} 1372 1373.usp-icon { 1374 flex:0 0 56px; 1375 width:56px; 1376 height:56px; 1377 border-radius:16px; 1378 display:flex; 1379 align-items:center; 1380 justify-content:center; 1381 background: 1382 linear-gradient(135deg,#FFE7CC,#FFD9A8); 1383 position:relative; 1384 box-shadow:inset 0 0 0 1px rgba(0,0,0,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1385 transition:.45s; 1386} 1387 1388.usp-item:hover .usp-icon { 1389 transform:rotate(-4deg) scale(1.06); 1390 box-shadow:inset 0 0 0 1px rgba(0,0,0,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1391} 1392 1393.usp-icon svg { 1394 width:28px; 1395 height:28px; 1396 display:block; 1397 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18)); 1398} 1399 1400.usp-text h3 { 1401 font-size:15px; 1402 font-weight:600; 1403 margin:2px 0 6px; 1404 letter-spacing:.2px; 1405} 1406 1407.usp-text p { 1408 font-size:13px; 1409 line-height:1.3; 1410 margin:0; 1411 color:var(--gray-600); 1412} 1413 1414@media (max-width:680px) { 1415 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1416 .usp-item { padding:16px 14px; } 1417 .usp-icon { width:50px;height:50px;border-radius:14px; } 1418 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1419 .usp-text p { font-size:12.5px; } 1420 .usp-list { gap: 10px; } 1421} 1422 1423.flash { padding:50px 0 30px; background:#fff; } 1424.flash-head { 1425 display:flex; align-items:center; 1426 justify-content:space-between; 1427 margin-bottom:20px; 1428} 1429.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1430.flash .countdown { 1431 font-family:monospace; 1432 background:#111; 1433 color:#fff; 1434 padding:8px 14px; 1435 border-radius:8px; 1436 font-size:16px; 1437 letter-spacing:1px; 1438} 1439.flash-scroll { 1440 display:flex; 1441 gap:16px; 1442 overflow-x:auto; 1443 padding-bottom:6px; 1444 scrollbar-width:none; 1445} 1446.flash-scroll::-webkit-scrollbar { display:none; } 1447 1448.products { padding:60px 0; background:#fff; } 1449.products-head { 1450 display:flex; justify-content:space-between; align-items:center; 1451 margin-bottom:24px; 1452 flex-wrap:wrap; 1453 gap:16px; 1454} 1455.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1456.product-filters input { 1457 padding:8px 12px; 1458 border:1px solid var(--border); 1459 border-radius:8px; 1460 font-size:14px; 1461 width:220px; 1462} 1463 1464.product-grid { 1465 display:grid; 1466 gap:24px; 1467 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1468} 1469 1470.prod-card { 1471 background:#fff; 1472 border:1px solid var(--border); 1473 padding:12px; 1474 border-radius:12px; 1475 position:relative; 1476 display:flex; 1477 flex-direction:column; 1478 transition:var(--transition); 1479} 1480.prod-card.small { min-width:160px; } 1481.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1482.prod-card .img-box { 1483 background:var(--gray-100); 1484 border-radius:10px; 1485 aspect-ratio:1/1; 1486 display:flex; 1487 align-items:center; 1488 justify-content:center; 1489 overflow:hidden; 1490} 1491.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1492.prod-card:hover .img-box img { transform:scale(1.04); } 1493.prod-card .name { 1494 font-size:13.5px; 1495 font-weight:500; 1496 margin:10px 0 6px; 1497 min-height:36px; 1498 line-height:1.3; 1499} 1500.price { 1501 display:flex; 1502 gap:6px; 1503 align-items:center; 1504 flex-wrap:wrap; 1505} 1506.price strong { color:var(--brand-600); font-size:14px; } 1507.price .old { 1508 font-size:11px; 1509 color:var(--gray-500); 1510 text-decoration:line-through; 1511} 1512.tag.discount { 1513 position:absolute; 1514 top:8px; left:8px; 1515 background:#E54848; 1516 color:#fff; 1517 font-size:11px; 1518 padding:4px 6px; 1519 border-radius:6px; 1520 font-weight:600; 1521} 1522.btn { 1523 border:none; 1524 cursor:pointer; 1525 font-weight:500; 1526 font-size:14px; 1527 padding:12px 22px; 1528 border-radius:10px; 1529 background:var(--gray-900); 1530 color:#fff; 1531 transition:var(--transition); 1532 text-decoration:none; 1533 text-align:center; 1534 display:inline-flex; 1535 align-items:center; 1536 justify-content:center; 1537 gap:6px; 1538} 1539.btn:hover { background:#000; } 1540.btn-primary { background:var(--brand-500); } 1541.btn-primary:hover { background:var(--brand-600); } 1542.btn-outline { 1543 background:#fff;
1544 color:var(--gray-900); 1545 border:1px solid var(--border); 1546} 1547.btn-outline:hover { background:var(--gray-100); } 1548.btn-sm { padding:8px 14px; font-size:13px; } 1549.btn-mini { padding:6px 10px; font-size:12px; } 1550.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1551.add-cart:hover { background:var(--brand-600); } 1552 1553/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1554 1555/* 1. Pastikan grid aktif */ 1556 1557.site-footer.footer-compact .ft-grid { 1558 display:grid; 1559 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1560 gap:30px 40px; 1561 align-items:start; 1562} 1563 1564/* 2. Spacing atas bawah dipadatkan */ 1565.site-footer.footer-compact .ft-top { 1566 padding:42px 0 26px; 1567} 1568 1569/* 3. Judul & teks */ 1570.site-footer.footer-compact .ft-title { 1571 margin:0 0 14px; 1572 font-size:14px; 1573 letter-spacing:.8px; 1574 text-transform:uppercase; 1575 font-weight:600; 1576 color:#82b1c9; 1577} 1578 1579.site-footer.footer-compact .ft-desc, 1580.site-footer.footer-compact .ft-text { 1581 margin:0 0 16px; 1582 font-size:13.5px; 1583 line-height:1.45; 1584 color:#c6d6e0; 1585 max-width:330px; 1586} 1587.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1588 1589/* 4. Reset ikon SVG agar tidak membesar */ 1590.site-footer.footer-compact svg { 1591 width:20px; 1592 height:20px; 1593 flex:0 0 20px; 1594 display:inline-block; 1595} 1596.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1597.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1598 1599/* Jika ada aturan global svg { width:100%; } â paksa override */ 1600svg { max-width:100%; } 1601.site-footer.footer-compact svg { 1602 width:20px !important; 1603 height:20px !important; 1604} 1605 1606/* 5. Link list */ 1607.site-footer.footer-compact .ft-links { 1608 list-style:none; 1609 margin:0 0 12px; 1610 padding:0; 1611 display:flex; 1612 flex-direction:column; 1613 gap:4px; 1614} 1615.site-footer.footer-compact .ft-links li { margin:0; } 1616.site-footer.footer-compact .ft-links a { 1617 display:flex; 1618 align-items:center; 1619 gap:8px; 1620 font-size:13.5px; 1621 padding:5px 2px; 1622 text-decoration:none; 1623 color:#c6d6e0; 1624 border-radius:8px; 1625 transition:.25s; 1626} 1627.site-footer.footer-compact .ft-links a:hover { 1628 background:rgba(0,0,0,.06); 1629 padding-left:6px; 1630 color:#fff; 1631} 1632 1633.site-footer.footer-compact .ico-s { 1634 width:24px; 1635 height:24px; 1636 background:#103346; 1637 color:#89b8cf; 1638 border-radius:6px; 1639 display:flex; 1640 align-items:center; 1641 justify-content:center; 1642 flex:0 0 24px; 1643 font-size:12px; 1644 transition:.3s; 1645} 1646.site-footer.footer-compact .ft-links a:hover .ico-s { 1647 background:#F28C1A; 1648 color:#fff; 1649 transform:rotate(-4deg); 1650} 1651 1652/* 6. WhatsApp button */ 1653.site-footer.footer-compact .ft-wa-btn { 1654 display:inline-flex; 1655 align-items:center; 1656 gap:8px; 1657 background:#fff; 1658 color:#062437; 1659 padding:8px 18px; 1660 font-size:13.5px; 1661 border-radius:22px; 1662 font-weight:500; 1663 text-decoration:none; 1664 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1665 transition:.25s; 1666} 1667.site-footer.footer-compact .ft-wa-btn:hover { 1668 transform:translateY(-2px); 1669 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1670} 1671 1672/* 7. Sosial */ 1673.site-footer.footer-compact .ft-social { 1674 display:flex; 1675 gap:12px; 1676 margin-top:4px; 1677} 1678.site-footer.footer-compact .ft-social a { 1679 width:34px; 1680 height:34px; 1681 background:rgba(0,0,0,.08); 1682 display:flex; 1683 align-items:center; 1684 justify-content:center; 1685 border-radius:10px; 1686 color:#c6d6e0; 1687 transition:.25s; 1688} 1689.site-footer.footer-compact .ft-social a:hover { 1690 background:#F28C1A; 1691 color:#fff; 1692} 1693 1694/* 8. Alamat */ 1695.site-footer.footer-compact .ft-address { 1696 font-size:12.5px; 1697 line-height:1.44; 1698 color:#8fa4b1; 1699 max-width:330px; 1700 margin-top:12px; 1701} 1702.site-footer.footer-compact .ft-address .company { 1703 margin:0 0 4px; 1704 font-weight:600; 1705 color:#e5f2f7; 1706 font-size:12.8px; 1707} 1708 1709/* 9. Payments kecil & rapi */ 1710.site-footer.footer-compact .ft-payments { 1711 margin-top:14px; 1712 display:flex; 1713 flex-direction:column; 1714 gap:8px; 1715 max-width:350px; 1716} 1717.site-footer.footer-compact .ft-payments .pay-row { 1718 display:flex; 1719 gap:8px; 1720 flex-wrap:wrap; 1721} 1722.site-footer.footer-compact .ft-payments img { 1723 width:46px; 1724 height:32px; 1725 padding:3px 5px; 1726 background:#fff; 1727 border-radius:7px; 1728 object-fit:contain; 1729 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1730 transition:.25s; 1731} 1732.site-footer.footer-compact .ft-payments img:hover { 1733 transform:translateY(-3px); 1734 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1735} 1736 1737/* 10. Bottom bar */ 1738.site-footer.footer-compact .ft-bottom { 1739 border-top:1px solid rgba(0,0,0,.08); 1740 background:#041f31; 1741 padding:12px 0 16px; 1742 margin-top:4px; 1743} 1744.site-footer.footer-compact .ft-copy { 1745 font-size:11.5px; 1746 color:#8fa7b5; 1747 display:flex; 1748 flex-wrap:wrap; 1749 gap:12px; 1750 justify-content:center; 1751 line-height:1.3; 1752 margin:0; 1753} 1754.site-footer.footer-compact .ft-copy span { 1755 position:relative; 1756 padding:0 10px; 1757} 1758.site-footer.footer-compact .ft-copy span+span:before { 1759 content:""; 1760 position:absolute; 1761 left:0; 1762 top:50%; 1763 width:1px; 1764 height:12px; 1765 background:rgba(0,0,0,.14); 1766 transform:translateY(-50%); 1767} 1768@media (max-width:580px){ 1769 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1770 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1771 .site-footer.footer-compact .ft-copy span { padding:0; } 1772 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1773} 1774.copy { 1775 margin-top:40px; 1776 border-top:1px solid #222; 1777 text-align:center; 1778 padding:18px 0; 1779 font-size:12px; 1780 color:#666; 1781} 1782 1783@media (max-width:760px) { 1784 .hero-text h1 { font-size:38px; } 1785 .hero { padding:0px 0 30px; } 1786 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1787 .nav-inner { flex-wrap:wrap; } 1788 .mega-cats { gap:10px; } 1789 .product-grid { gap:16px; } 1790 .prod-card { padding:10px; } 1791} 1792 1793/* === PLAZA IT DARK ORANGE FOOTER THEME === */ 1794.site-footer.footer-compact { 1795 --ft-bg-0:#000000; 1796 --ft-bg-1:#000000; 1797 --ft-bg-2:#821603; 1798 --ft-bg-3:#821603; 1799 --ft-orange:#F28C1A; 1800 --ft-orange-dark:#D97809; 1801 --ft-text:#E9F2F6; 1802 --ft-text-sec:#B8CAD3; 1803 --ft-text-muted:#8FA4B1; 1804 --ft-divider:rgba(0,0,0,0.08); 1805 --ft-glow:rgba(242,140,26,0.18); 1806 --ft-glow-fade:rgba(242,140,26,0); 1807 color:var(--ft-text); 1808} 1809 1810/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1811.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1812.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1813.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1814.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1815.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1816 1817/* ICON BACKGROUND (kubu gelap) */
1818.site-footer.footer-compact .ico-s { 1819 background:#0F3647; 1820 color:#9FC7DA; 1821} 1822.site-footer.footer-compact .ft-links a:hover .ico-s { 1823 background:var(--ft-orange); 1824 color:#fff; 1825} 1826 1827/* WhatsApp button biarkan putih agar kontras */ 1828.site-footer.footer-compact .ft-wa-btn { 1829 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(0,0,0,0.05); 1830} 1831.site-footer.footer-compact .ft-wa-btn:hover { 1832 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(242,140,26,0.25); 1833} 1834 1835/* Payment cards border halus */ 1836.site-footer.footer-compact .ft-payments img { 1837 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(0,0,0,0.05); 1838} 1839.site-footer.footer-compact .ft-payments img:hover { 1840 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(242,140,26,0.4); 1841} 1842 1843/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1844.site-footer.footer-compact.variant-ember { 1845 background: 1846 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1847 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1848} 1849.site-footer.footer-compact.variant-ember .ft-bottom { 1850 background:rgba(0,0,0,0.25); 1851 backdrop-filter:blur(2px); 1852} 1853 1854/* ========== VARIANT B: TOP BAR ORANGE ========== */ 1855.site-footer.footer-compact.variant-bar { 1856 background:var(--ft-bg-1); 1857 position:relative; 1858} 1859.site-footer.footer-compact.variant-bar::before { 1860 content:""; 1861 position:absolute; 1862 left:0;top:0; 1863 width:100%;height:3px; 1864 background:linear-gradient(90deg,var(--ft-orange-dark),var(--ft-orange)); 1865 box-shadow:0 0 12px -2px var(--ft-orange); 1866} 1867.site-footer.footer-compact.variant-bar .ft-bottom { 1868 background:#041c28; 1869} 1870 1871/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1872.site-footer.footer-compact.variant-radial { 1873 background: 1874 radial-gradient(circle at 70% 90%, rgba(242,140,26,0.22) 0%, rgba(242,140,26,0.04) 38%, rgba(242,140,26,0) 65%), 1875 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1876} 1877.site-footer.footer-compact.variant-radial .ft-bottom { 1878 background:#041c29; 1879} 1880 1881/* ========== VARIANT D: LAYERED STRIPES ========== */ 1882.site-footer.footer-compact.variant-stripes { 1883 background: 1884 repeating-linear-gradient(180deg, rgba(0,0,0,0.015) 0 38px, rgba(0,0,0,0) 38px 76px), 1885 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1886 position:relative; 1887} 1888.site-footer.footer-compact.variant-stripes::after { 1889 content:""; 1890 position:absolute; 1891 left:0;bottom:0; 1892 width:100%;height:110px; 1893 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1894 pointer-events:none; 1895} 1896.site-footer.footer-compact.variant-stripes .ft-bottom { 1897 background:#031821; 1898} 1899 1900/* Garis aksen oranye tipis di atas bottom */ 1901.site-footer.footer-compact.variant-stripes .ft-bottom { 1902 position:relative; 1903} 1904.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1905 content:""; 1906 position:absolute; 1907 left:0;top:0; 1908 width:100%;height:2px; 1909 background:linear-gradient(90deg,rgba(242,140,26,0),rgba(242,140,26,.65),rgba(242,140,26,0)); 1910} 1911 1912/* ========== VARIANT E: GLASS PANEL ========== */ 1913.site-footer.footer-compact.variant-glass { 1914 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 100%); 1915} 1916.site-footer.footer-compact.variant-glass .ft-grid { 1917 position:relative; 1918 padding:32px clamp(10px,2vw,24px); 1919 border-radius:22px; 1920 background:linear-gradient(140deg,rgba(0,0,0,0.06),rgba(0,0,0,0.02)); 1921 border:1px solid rgba(0,0,0,0.08); 1922 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(0,0,0,0.04) inset; 1923 backdrop-filter:blur(8px); 1924 -webkit-backdrop-filter:blur(8px); 1925} 1926.site-footer.footer-compact.variant-glass .ft-bottom { 1927 background:#02141d; 1928} 1929 1930/* HOVER ACCENTS */ 1931.site-footer.footer-compact a { 1932 transition:color .25s, background .25s, box-shadow .25s; 1933} 1934.site-footer.footer-compact .ft-links a:hover { 1935 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1936 0 0 0 1px rgba(242,140,26,0.55); 1937} 1938.site-footer.footer-compact .ft-social a { 1939 background:#0E3445; 1940 color:#bcd4dd; 1941} 1942.site-footer.footer-compact .ft-social a:hover { 1943 background:var(--ft-orange); 1944 color:#fff; 1945 box-shadow:0 0 0 3px rgba(242,140,26,0.28); 1946} 1947 1948/* Divider di atas footer (opsional) */ 1949.footer-divider-top { 1950 height:1px; 1951 background:linear-gradient(90deg,transparent,rgba(0,0,0,0.15),transparent); 1952 width:100%; 1953 margin:0 0 4px; 1954} 1955
1956/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1957 1958/* Desktop: wrapper tidak mengganggu grid layout */ 1959.ft-collapsible-content { 1960 display: contents; 1961} 1962 1963@media (max-width: 768px) { 1964 /* Wrapper untuk konten yang bisa collapse */ 1965 .ft-top.ft-collapsible .ft-collapsible-content { 1966 display: block; 1967 max-height: 80px; 1968 overflow: hidden; 1969 position: relative; 1970 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1971 } 1972 1973 /* Fade gradient overlay */ 1974 .ft-top.ft-collapsible .ft-collapsible-content::after { 1975 content: ''; 1976 position: absolute; 1977 bottom: 0; 1978 left: 0; 1979 right: 0; 1980 height: 70px; 1981 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1982 pointer-events: none; 1983 opacity: 1; 1984 transition: opacity 0.3s ease; 1985 z-index: 1; 1986 } 1987 1988 /* State: Expanded */ 1989 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1990 max-height: 1500px; 1991 } 1992 1993 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1994 opacity: 0; 1995 } 1996 1997 /* Expand Button - Di luar collapsible, selalu visible */ 1998 .ft-expand-btn { 1999 display: flex; 2000 align-items: center; 2001 justify-content: center; 2002 gap: 8px; 2003 width: 100%; 2004 margin: -8px 0 0; 2005 padding: 12px 20px; 2006 background: rgba(0,0,0,0.12); 2007 border: 1px solid rgba(0,0,0,0.25); 2008 border-radius: 10px; 2009 color: #e5f2f7; 2010 font-size: 13.5px; 2011 font-weight: 500; 2012 cursor: pointer; 2013 transition: all 0.3s ease; 2014 outline: none; 2015 position: relative; 2016 z-index: 10; 2017 } 2018 2019 .ft-expand-btn:hover { 2020 background: rgba(0,0,0,0.18); 2021 border-color: rgba(0,0,0,0.35); 2022 color: #fff; 2023 transform: translateY(-2px); 2024 } 2025 2026 .ft-expand-btn:active { 2027 transform: scale(0.98); 2028 } 2029 2030 .ft-expand-btn svg { 2031 width: 18px; 2032 height: 18px; 2033 transition: transform 0.3s ease; 2034 fill: currentColor; 2035 } 2036 2037 .ft-top.ft-collapsible.expanded .ft-expand-btn { 2038 margin-top: 15px; 2039 } 2040 2041 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 2042 transform: rotate(180deg); 2043 } 2044} 2045 2046/* Desktop: hide expand button */ 2047@media (min-width: 769px) { 2048 .ft-expand-btn { 2049 display: none !important; 2050 } 2051} 2052 2053/* RESPONSIVE (jika background panel butuh penyesuaian) */ 2054@media (max-width:640px){ 2055 .site-footer.footer-compact.variant-glass .ft-grid { 2056 padding:26px 18px; 2057 } 2058} 2059 2060</style> 2061 <style data-inlined="bottom-nav.css"> 2062/* Mobile Bottom Navigation */ 2063.bottom-nav { 2064 position: fixed; 2065 bottom: 0; 2066 left: 0; 2067 right: 0; 2068 background: #ffffff; 2069 border-top: 1px solid #e5e7eb; 2070 z-index: 9998; /* Below chat widget */ 2071 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 2072 display: none; /* Hidden on desktop */ 2073} 2074 2075.bottom-nav-inner { 2076 display: flex; 2077 justify-content: space-around; 2078 align-items: center; 2079 padding: 2px 0; 2080 max-width: 640px; 2081 margin: 0 auto; 2082} 2083 2084.bottom-nav-item { 2085 flex: 1; 2086 display: flex; 2087 flex-direction: column; 2088 align-items: center; 2089 justify-content: center; 2090 gap: 4px; 2091 padding: 8px 8px; 2092 background: none; 2093 border: none; 2094 cursor: pointer; 2095 text-decoration: none; 2096 color: #6b7280; 2097 transition: all 0.3s ease; 2098 position: relative; 2099} 2100 2101.bottom-nav-item:hover {
2102 color: #F28C1A; 2103} 2104 2105.bottom-nav-item.active { 2106 flex-direction: row; 2107 gap: 8px; 2108 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2109 color: white; 2110 border-radius: 20px; 2111 padding: 8px 16px; 2112 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 2113} 2114 2115.bottom-nav-item-icon { 2116 width: 24px; 2117 height: 24px; 2118 position: relative; 2119} 2120 2121.bottom-nav-item-icon svg { 2122 width: 100%; 2123 height: 100%; 2124} 2125 2126/* Icon variant switching */ 2127.bottom-nav-item .icon-outlined { 2128 display: block; 2129} 2130 2131.bottom-nav-item .icon-filled { 2132 display: none; 2133} 2134 2135.bottom-nav-item.active .icon-outlined { 2136 display: none; 2137} 2138 2139.bottom-nav-item.active .icon-filled { 2140 display: block; 2141} 2142 2143.bottom-nav-item-label { 2144 font-size: 12px; 2145 font-weight: 600; 2146 line-height: 1; 2147 display: none; 2148 white-space: nowrap; 2149} 2150 2151.bottom-nav-item.active .bottom-nav-item-label { 2152 display: block; 2153} 2154 2155/* Avatar styles for bottom-nav */ 2156.bottom-nav-avatar-img, 2157.bottom-nav-avatar-initials { 2158 width: 24px; 2159 height: 24px; 2160 border-radius: 50%; 2161 display: block; 2162} 2163 2164.bottom-nav-avatar-img { 2165 object-fit: cover; 2166} 2167 2168.bottom-nav-avatar-initials { 2169 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 2170 color: white; 2171 font-size: 10px; 2172 font-weight: 700; 2173 display: flex; 2174 align-items: center; 2175 justify-content: center; 2176 line-height: 1; 2177 letter-spacing: 0.5px; 2178} 2179 2180/* Hide SVG when avatar is shown */ 2181.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2182.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2183 display: none; 2184} 2185 2186/* Active state adjustments for avatar */ 2187.bottom-nav-item.active .bottom-nav-avatar-initials { 2188 background: white; 2189 color: #F28C1A; 2190 border: 2px solid rgba(255, 255, 255, 0.8); 2191 box-sizing: border-box; 2192} 2193 2194.bottom-nav-item.active .bottom-nav-avatar-img { 2195 border: 2px solid rgba(255, 255, 255, 0.9); 2196 box-sizing: border-box; 2197} 2198 2199.bottom-nav-item-badge { 2200 position: absolute; 2201 top: -4px; 2202 right: -4px; 2203 background: #ef4444; 2204 color: white; 2205 border-radius: 10px; 2206 padding: 2px 5px; 2207 font-size: 10px; 2208 font-weight: 600; 2209 min-width: 18px; 2210 height: 18px; 2211 display: flex; 2212 align-items: center; 2213 justify-content: center; 2214 line-height: 1; 2215} 2216 2217/* Show on mobile only */ 2218@media (max-width: 640px) { 2219 .bottom-nav { 2220 display: block; 2221 } 2222 2223 /* Add bottom padding to body to prevent content being hidden */ 2224 body { 2225 padding-bottom: 60px; 2226 } 2227} 2228 2229/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2230.mobile-user-sheet { 2231 position: fixed; 2232 bottom: 0; 2233 left: 0; 2234 right: 0; 2235 z-index: 10000; 2236 pointer-events: none; 2237} 2238 2239.mobile-user-sheet:not([hidden]) { 2240 pointer-events: auto; 2241} 2242 2243.mobile-user-sheet-backdrop { 2244 position: fixed; 2245 inset: 0; 2246 background: rgba(0, 0, 0, 0.5); 2247 opacity: 0; 2248 transition: opacity 0.2s ease-out; 2249} 2250 2251.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2252 opacity: 1; 2253} 2254 2255.mobile-user-sheet-content { 2256 position: absolute; 2257 bottom: 0; 2258 left: 0; 2259 right: 0; 2260 background: white; 2261 border-radius: 16px 16px 0 0; 2262 max-height: 80vh; 2263 overflow-y: auto; 2264 transform: translateY(100%); 2265 transition: transform 0.3s ease-out; 2266} 2267 2268.mobile-user-sheet.active .mobile-user-sheet-content { 2269 transform: translateY(0); 2270} 2271 2272.mobile-user-sheet-handle { 2273 width: 40px; 2274 height: 4px; 2275 background: #d1d5db; 2276 border-radius: 2px; 2277 margin: 12px auto 16px; 2278} 2279 2280.mobile-user-sheet-header { 2281 padding: 0 20px 16px; 2282 border-bottom: 1px solid #e5e7eb; 2283} 2284 2285.mobile-user-info { 2286 display: flex; 2287 align-items: center; 2288 gap: 12px; 2289} 2290 2291.mobile-user-avatar { 2292 width: 48px; 2293 height: 48px; 2294 background: linear-gradient(135deg, #FFE7CC 0%, #FFD9A8 50%, #FFCC80 100%); 2295 border-radius: 50%; 2296 display: flex; 2297 align-items: center; 2298 justify-content: center; 2299 font-size: 24px; 2300 color: #D97809; 2301 font-weight: 600; 2302} 2303 2304.mobile-user-details { 2305 flex: 1; 2306} 2307 2308.mobile-user-name { 2309 font-weight: 600; 2310 font-size: 16px; 2311 color: #1f2937; 2312 margin-bottom: 4px; 2313} 2314 2315.mobile-user-email { 2316 font-size: 13px; 2317 color: #6b7280; 2318} 2319 2320.mobile-user-sheet-body { 2321 padding: 8px 0; 2322} 2323 2324.mobile-user-sheet-item { 2325 display: flex; 2326 align-items: center; 2327 gap: 12px; 2328 padding: 14px 20px; 2329 background: none; 2330 border: none; 2331 width: 100%; 2332 text-align: left; 2333 cursor: pointer; 2334 text-decoration: none; 2335 color: #374151; 2336 transition: background 0.2s; 2337} 2338 2339.mobile-user-sheet-item:hover { 2340 background: #f9fafb; 2341} 2342 2343.mobile-user-sheet-icon { 2344 font-size: 20px; 2345 width: 24px; 2346 text-align: center; 2347} 2348 2349.mobile-user-sheet-label { 2350 font-size: 15px; 2351 font-weight: 500; 2352} 2353 2354.mobile-user-sheet-divider { 2355 height: 1px; 2356 background: #e5e7eb; 2357 margin: 8px 0; 2358} 2359 2360@keyframes fadeIn { 2361 from { 2362 opacity: 0; 2363 } 2364 to { 2365 opacity: 1; 2366 } 2367} 2368 2369@keyframes slideUp { 2370 from { 2371 transform: translateY(100%); 2372 } 2373 to { 2374 transform: translateY(0); 2375 } 2376} 2377 2378/* Ensure chat widget works well with bottom nav */ 2379@media (max-width: 640px) { 2380 .chat-widget { 2381 bottom: 0 !important; /* Chat opens from bottom */ 2382 } 2383 2384 .chat-popup { 2385 /* Chat popup should be above bottom nav */ 2386 z-index: 9999; 2387 } 2388} 2389 2390</style> 2391 <style data-inlined="search-suggest.css"> 2392.search { 2393 position: relative; 2394} 2395 2396/* AI Bot Toggle Button */ 2397.ai-bot-toggle { 2398 display: flex; 2399 align-items: center; 2400 justify-content: center; 2401 background: transparent; 2402 border: none; 2403 padding: 8px; 2404 cursor: pointer; 2405 border-radius: 8px; 2406 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2407 margin-left: 4px; 2408 flex-shrink: 0; 2409 display: none; 2410} 2411 2412.ai-bot-toggle .bot-icon { 2413 color: #9ca3af; 2414 transition: all 0.3s ease; 2415} 2416 2417.ai-bot-toggle:hover .bot-icon { 2418 color: #6b7280; 2419 transform: scale(1.1); 2420} 2421 2422.ai-bot-toggle.active .bot-icon { 2423 color: #fb923c; 2424 animation: botPulse 2s ease-in-out infinite; 2425} 2426 2427.ai-bot-toggle.active { 2428 background: rgba(251, 146, 60, 0.1); 2429} 2430 2431.ai-bot-toggle.active:hover .bot-icon { 2432 color: #f97316; 2433} 2434 2435@keyframes botPulse { 2436 0%, 100% { transform: scale(1); } 2437 50% { transform: scale(1.12); } 2438} 2439 2440/* Search Suggest Box */ 2441.search-suggest { 2442 position: absolute; 2443 top: 100%; left: 0; right: 0; 2444 background: #fff; 2445 border: 1px solid #e1e4e8; 2446 border-top: none; 2447 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2448 max-height: 400px; 2449 overflow-y: auto; 2450 z-index: 100; 2451 padding: 0; 2452 font-size: 14px; 2453 border-radius: 0 0 8px 8px; 2454} 2455 2456.suggest-item { 2457 display: flex; 2458 align-items: center; 2459 gap: 8px; 2460 padding: 10px 16px; 2461 cursor: pointer; 2462 transition: all 0.2s ease; 2463 border-bottom: 1px solid #f1f5f9; 2464} 2465 2466.suggest-item:last-child { 2467 border-bottom: none; 2468} 2469 2470.suggest-item:hover, 2471.suggest-item.active { 2472 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2473 padding-left: 20px; 2474} 2475 2476.suggest-thumb { 2477 width: 42px; 2478 height: 42px; 2479 object-fit: cover; 2480 border: 1px solid #ddd; 2481 border-radius: 4px; 2482 background: #fafafa; 2483} 2484 2485.suggest-meta { flex: 1; min-width: 0; } 2486 2487.suggest-name { 2488 font-weight: 500; 2489 color: #222; 2490 line-height: 1.2; 2491 white-space: nowrap; 2492 overflow: hidden; 2493 text-overflow: ellipsis; 2494} 2495 2496.suggest-price { 2497 color: #d32f2f; 2498 font-size: 12px; 2499 margin-top: 2px; 2500} 2501 2502/* AI Insights */ 2503.suggest-ai-insight { 2504 display: flex; 2505 align-items: center; 2506 gap: 8px; 2507 padding: 8px 12px; 2508 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 2509 border-bottom: 1px solid #bae6fd; 2510 color: #0369a1; 2511 font-size: 12px; 2512 font-weight: 500; 2513 line-height: 1.4; 2514} 2515 2516.suggest-ai-insight svg { 2517 flex-shrink: 0; 2518 opacity: 0.8; 2519} 2520 2521.suggest-ai-insight span { 2522 flex: 1; 2523} 2524 2525/* AI Progress States */ 2526.ai-progress-container { 2527 padding: 20px 16px; 2528 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2529} 2530 2531.ai-progress-header { 2532 display: flex; 2533 align-items: center; 2534 gap: 10px; 2535 margin-bottom: 16px; 2536} 2537 2538.ai-progress-icon { 2539 width: 36px; 2540 height: 36px; 2541 display: flex; 2542 align-items: center; 2543 justify-content: center; 2544 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 2545 border-radius: 50%; 2546 color: #fff; 2547 font-size: 18px;
2548 animation: aiIconSpin 3s linear infinite; 2549} 2550 2551@keyframes aiIconSpin { 2552 0% { transform: rotate(0deg); } 2553 100% { transform: rotate(360deg); } 2554} 2555 2556.ai-progress-text { 2557 flex: 1; 2558} 2559 2560.ai-progress-title { 2561 font-weight: 600; 2562 color: #1e293b; 2563 font-size: 14px; 2564 margin-bottom: 2px; 2565} 2566 2567.ai-progress-subtitle { 2568 font-size: 12px; 2569 color: #64748b; 2570} 2571 2572/* Progress Steps */ 2573.ai-progress-steps { 2574 display: flex; 2575 flex-direction: column; 2576 gap: 10px; 2577 margin-top: 16px; 2578} 2579 2580.ai-progress-step { 2581 display: flex; 2582 align-items: center; 2583 gap: 10px; 2584 padding: 8px 12px; 2585 background: #fff; 2586 border-radius: 8px; 2587 border: 1px solid #e2e8f0; 2588 transition: all 0.3s ease; 2589} 2590 2591.ai-progress-step.active { 2592 border-color: #667eea; 2593 background: #f8f9ff; 2594 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2595} 2596 2597.ai-progress-step.completed { 2598 border-color: #10b981; 2599 background: #f0fdf4; 2600} 2601 2602.ai-step-icon { 2603 width: 20px; 2604 height: 20px; 2605 display: flex; 2606 align-items: center; 2607 justify-content: center; 2608 border-radius: 50%; 2609 font-size: 11px; 2610 flex-shrink: 0; 2611} 2612 2613.ai-progress-step.active .ai-step-icon { 2614 background: #667eea; 2615 color: #fff; 2616 animation: pulse 1.5s ease-in-out infinite; 2617} 2618 2619.ai-progress-step.completed .ai-step-icon { 2620 background: #10b981; 2621 color: #fff; 2622} 2623 2624.ai-progress-step.pending .ai-step-icon { 2625 background: #e2e8f0; 2626 color: #94a3b8; 2627} 2628 2629.ai-step-text { 2630 flex: 1; 2631 font-size: 13px; 2632 color: #475569; 2633 font-weight: 500; 2634} 2635 2636.ai-progress-step.completed .ai-step-text { 2637 color: #059669; 2638} 2639 2640/* AI Analysis Result Banner */ 2641.ai-result-banner { 2642 padding: 12px 16px; 2643 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2644 border-bottom: 1px solid #86efac; 2645 display: flex; 2646 align-items: center; 2647 gap: 10px; 2648} 2649 2650.ai-result-icon { 2651 font-size: 20px; 2652 animation: bounceIn 0.5s ease; 2653} 2654 2655@keyframes bounceIn { 2656 0% { transform: scale(0); opacity: 0; } 2657 50% { transform: scale(1.1); } 2658 100% { transform: scale(1); opacity: 1; } 2659} 2660 2661.ai-result-text { 2662 flex: 1; 2663} 2664 2665.ai-result-title { 2666 font-weight: 600; 2667 color: #065f46; 2668 font-size: 13px; 2669 margin-bottom: 2px; 2670} 2671 2672.ai-result-description { 2673 font-size: 12px; 2674 color: #047857; 2675 opacity: 0.9; 2676} 2677 2678/* Product Items Section Header */ 2679.suggest-section-header { 2680 padding: 10px 16px; 2681 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2682 border-bottom: 1px solid #e2e8f0; 2683} 2684 2685.suggest-header-main { 2686 display: flex; 2687 align-items: center; 2688 gap: 6px; 2689 margin-bottom: 4px; 2690} 2691 2692.suggest-header-icon { 2693 font-size: 14px; 2694} 2695 2696.suggest-header-text { 2697 font-size: 12px; 2698 font-weight: 600; 2699 color: #64748b; 2700 text-transform: uppercase; 2701 letter-spacing: 0.5px; 2702} 2703 2704.suggest-header-hint { 2705 font-size: 11px; 2706 color: #94a3b8; 2707 font-weight: 400; 2708 text-transform: none; 2709 letter-spacing: 0; 2710 display: flex; 2711 align-items: center; 2712 gap: 4px; 2713} 2714 2715.suggest-header-hint kbd { 2716 display: inline-block; 2717 padding: 2px 6px; 2718 font-size: 10px; 2719 font-weight: 600; 2720 line-height: 1; 2721 color: #475569; 2722 background: #fff; 2723 border: 1px solid #cbd5e1; 2724 border-radius: 4px; 2725 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2726 font-family: 'Segoe UI', monospace; 2727} 2728 2729/* Footer Hint */ 2730.suggest-footer-hint { 2731 display: flex; 2732 align-items: center; 2733 justify-content: center; 2734 padding: 12px 16px; 2735 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 2736 border-top: 1px solid #fbbf24; 2737 color: #92400e; 2738 font-size: 12px; 2739 font-weight: 500; 2740 text-align: center; 2741 cursor: default; 2742} 2743 2744.suggest-footer-hint svg { 2745 color: #f59e0b; 2746 flex-shrink: 0; 2747} 2748 2749.suggest-footer-hint kbd { 2750 display: inline-block; 2751 padding: 3px 7px; 2752 margin: 0 3px; 2753 font-size: 11px; 2754 font-weight: 600; 2755 line-height: 1; 2756 color: #78350f; 2757 background: #fff; 2758 border: 1px solid #f59e0b; 2759 border-radius: 4px; 2760 box-shadow: 0 1px 3px rgba(245, 158, 11, 0.2); 2761 font-family: 'Segoe UI', monospace; 2762} 2763 2764.suggest-footer-link { 2765 color: #ea580c; 2766 font-weight: 600; 2767 text-decoration: underline; 2768 text-underline-offset: 2px; 2769 text-decoration-thickness: 1px; 2770 cursor: pointer; 2771 transition: all 0.2s ease; 2772} 2773 2774.suggest-footer-link:hover { 2775 color: #c2410c; 2776 text-decoration-thickness: 2px; 2777 text-shadow: 0 0 8px rgba(234, 88, 12, 0.3); 2778} 2779 2780/* Loading state */ 2781.suggest-loading { 2782 display: flex; 2783 align-items: center; 2784 justify-content: center; 2785 gap: 8px; 2786 padding: 16px 12px; 2787 color: #64748b; 2788 font-size: 13px; 2789} 2790 2791@keyframes pulse { 2792 0%, 100% { opacity: 1; } 2793 50% { opacity: 0.5; } 2794} 2795 2796.suggest-loading { 2797 animation: pulse 1.5s ease-in-out infinite; 2798} 2799 2800/* Responsive Mobile Styles */ 2801@media (max-width: 768px) { 2802 .ai-bot-toggle { 2803 padding: 6px; 2804 margin-left: 0; 2805 } 2806 2807 .ai-bot-toggle .bot-icon {
2808 width: 20px; 2809 height: 20px; 2810 } 2811 2812 .search-icon-btn { 2813 margin-right: 4px; 2814 } 2815 2816 .search-icon-btn svg { 2817 width: 18px; 2818 height: 18px; 2819 } 2820 2821 .search-suggest { 2822 max-height: 350px; 2823 } 2824 2825 /* Mobile hint adjustments */ 2826 .suggest-header-hint { 2827 font-size: 10px; 2828 } 2829 2830 .suggest-header-hint kbd { 2831 padding: 1px 4px; 2832 font-size: 9px; 2833 } 2834 2835 .suggest-footer-hint { 2836 padding: 10px 12px; 2837 font-size: 11px; 2838 } 2839 2840 .suggest-footer-hint svg { 2841 width: 14px; 2842 height: 14px; 2843 } 2844 2845 .suggest-footer-hint kbd { 2846 padding: 2px 5px; 2847 font-size: 10px; 2848 } 2849 2850 .ai-progress-container { 2851 padding: 16px 12px; 2852 } 2853 2854 .ai-progress-icon { 2855 width: 32px; 2856 height: 32px; 2857 font-size: 16px; 2858 } 2859 2860 .ai-progress-title { 2861 font-size: 13px; 2862 } 2863 2864 .ai-progress-subtitle { 2865 font-size: 11px; 2866 } 2867 2868 .ai-progress-step { 2869 padding: 6px 10px; 2870 } 2871 2872 .ai-step-text { 2873 font-size: 12px; 2874 } 2875 2876 .suggest-item { 2877 padding: 8px 12px; 2878 } 2879 2880 .suggest-thumb { 2881 width: 36px; 2882 height: 36px; 2883 } 2884 2885 .suggest-name { 2886 font-size: 13px; 2887 } 2888 2889 .suggest-price { 2890 font-size: 11px; 2891 } 2892} 2893 2894@media (max-width: 480px) { 2895 .ai-bot-toggle { 2896 padding: 4px; 2897 } 2898 2899 .ai-bot-toggle .bot-icon { 2900 width: 18px; 2901 height: 18px; 2902 } 2903 2904 .search-icon-btn svg { 2905 width: 16px; 2906 height: 16px; 2907 } 2908 2909 /* Ultra-compact hints for small screens */ 2910 .suggest-header-hint { 2911 display: none; /* Hide header hint on very small screens */ 2912 } 2913 2914 .suggest-footer-hint { 2915 padding: 8px 10px; 2916 font-size: 10px; 2917 flex-direction: column; 2918 gap: 4px; 2919 } 2920 2921 .suggest-footer-hint svg { 2922 display: none; /* Hide icon to save space */ 2923 } 2924 2925 .suggest-footer-hint kbd { 2926 padding: 2px 4px; 2927 font-size: 9px; 2928 } 2929 2930 .ai-progress-steps { 2931 gap: 8px; 2932 } 2933 2934 .ai-step-text { 2935 font-size: 11px; 2936 } 2937} 2938 2939/* AI Search Prompt Box */ 2940.ai-search-prompt { 2941 padding: 24px; 2942 background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); 2943} 2944 2945.ai-prompt-header { 2946 display: flex; 2947 align-items: center; 2948 gap: 12px; 2949 margin-bottom: 16px; 2950} 2951 2952.ai-prompt-icon { 2953 width: 48px; 2954 height: 48px; 2955 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 2956 border-radius: 12px; 2957 display: flex; 2958 align-items: center; 2959 justify-content: center; 2960 font-size: 24px; 2961 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.3); 2962} 2963 2964.ai-prompt-text { 2965 flex: 1; 2966} 2967 2968.ai-prompt-title { 2969 font-size: 16px; 2970 font-weight: 700; 2971 color: #1f2937; 2972 margin-bottom: 4px; 2973} 2974 2975.ai-prompt-subtitle { 2976 font-size: 13px; 2977 color: #6b7280; 2978} 2979 2980.ai-prompt-query { 2981 background: white; 2982 border: 2px solid #fb923c; 2983 border-radius: 8px; 2984 padding: 12px 16px; 2985 margin-bottom: 16px; 2986} 2987 2988.ai-query-label { 2989 font-size: 11px; 2990 font-weight: 600; 2991 color: #fb923c; 2992 text-transform: uppercase; 2993 letter-spacing: 0.5px; 2994 margin-bottom: 4px; 2995} 2996 2997.ai-query-text { 2998 font-size: 15px; 2999 font-weight: 600; 3000 color: #1f2937; 3001} 3002 3003.ai-search-trigger-btn { 3004 width: 100%; 3005 padding: 14px 20px; 3006 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 3007 color: white; 3008 border: none; 3009 border-radius: 10px; 3010 font-size: 15px; 3011 font-weight: 700; 3012 cursor: pointer; 3013 display: flex; 3014 align-items: center; 3015 justify-content: center; 3016 gap: 8px; 3017 transition: all 0.3s ease; 3018 box-shadow: 0 4px 12px rgba(251, 146, 60, 0.4); 3019} 3020 3021.ai-search-trigger-btn:hover { 3022 transform: translateY(-2px); 3023 box-shadow: 0 6px 20px rgba(251, 146, 60, 0.5); 3024} 3025 3026.ai-search-trigger-btn:active { 3027 transform: translateY(0); 3028} 3029 3030.ai-btn-icon { 3031 font-size: 18px; 3032 animation: sparkle 2s ease-in-out infinite; 3033} 3034 3035@keyframes sparkle { 3036 0%, 100% { opacity: 1; transform: scale(1); } 3037 50% { opacity: 0.7; transform: scale(1.2); } 3038} 3039 3040.ai-btn-text { 3041 font-size: 15px; 3042} 3043 3044.ai-prompt-footer { 3045 margin-top: 12px; 3046 text-align: center; 3047 font-size: 12px; 3048 color: #9ca3af; 3049 font-style: italic; 3050} 3051 3052/* ==================== TRENDING SEARCH STYLES ==================== */ 3053 3054.search-suggest.trending-mode { 3055 border-top: 3px solid #fb923c; 3056 max-width: 900px; 3057 min-width: 600px; 3058} 3059 3060/* Two-column container */ 3061.trending-container { 3062 display: flex; 3063 gap: 0; 3064 padding: 0 !important; 3065} 3066 3067.trending-column { 3068 flex: 1; 3069 min-width: 0; 3070 padding: 0 !important; 3071} 3072 3073.trending-column.trending-searches { 3074 border-right: 2px solid #e2e8f0; padding: 0 !important; 3075} 3076 3077/* Mobile: stack columns vertically */ 3078@media (max-width: 768px) { 3079 .search-suggest.trending-mode { 3080 min-width: auto; 3081 max-width: 100%; 3082 } 3083 3084 .trending-container { 3085 flex-direction: column; 3086 } 3087 3088 .trending-column.trending-searches {
3089 border-right: none; 3090 border-bottom: 2px solid #e2e8f0; 3091 } 3092} 3093 3094.trending-header { 3095 display: flex; 3096 align-items: center; 3097 gap: 8px; 3098 padding: 14px 16px; 3099 border-bottom: 1px solid #e2e8f0; 3100 background: linear-gradient(135deg, #fb923c 0%, #f97316 100%); 3101 color: white; 3102 position: sticky; 3103 top: 0; 3104 z-index: 10; 3105} 3106 3107.trending-icon { 3108 font-size: 20px; 3109 animation: trendingPulse 2s infinite; 3110} 3111 3112@keyframes trendingPulse { 3113 0%, 100% { 3114 transform: scale(1); 3115 filter: drop-shadow(0 0 0 rgba(251, 146, 60, 0)); 3116 } 3117 50% { 3118 transform: scale(1.15); 3119 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3120 } 3121} 3122 3123.trending-title { 3124 font-weight: 700; 3125 font-size: 14px; 3126 letter-spacing: 0.3px; 3127} 3128 3129.trending-subtitle { 3130 font-size: 11px; 3131 opacity: 0.9; 3132 margin-left: auto; 3133 font-style: italic; 3134} 3135 3136/* Trending searches item */ 3137.trending-item { 3138 display: flex; 3139 align-items: center; 3140 gap: 12px; 3141 padding: 12px 16px; 3142 cursor: pointer; 3143 transition: all 0.2s ease; 3144 border-bottom: 1px solid #f1f5f9; 3145 position: relative; 3146} 3147 3148.trending-item:hover { 3149 background: linear-gradient(90deg, #fff7ed 0%, #ffedd5 100%); 3150 padding-left: 20px; 3151} 3152 3153.trending-item:hover::before { 3154 content: ''; 3155 position: absolute; 3156 left: 0; 3157 top: 0; 3158 bottom: 0; 3159 width: 4px; 3160 background: linear-gradient(180deg, #fb923c 0%, #f97316 100%); 3161} 3162 3163.trending-item:last-child { 3164 border-bottom: none; 3165} 3166 3167.trending-rank { 3168 font-size: 20px; 3169 flex-shrink: 0; 3170 width: 32px; 3171 text-align: center; 3172} 3173 3174.trending-query { 3175 flex: 1; 3176 font-size: 14px; 3177 color: #1f2937; 3178 font-weight: 500; 3179 line-height: 1.4; 3180} 3181 3182.trending-category { 3183 font-size: 11px; 3184 color: #f97316; 3185 background: #fff7ed; 3186 padding: 3px 10px; 3187 border-radius: 12px; 3188 white-space: nowrap; 3189 font-weight: 500; 3190 border: 1px solid #fed7aa; 3191} 3192 3193/* Trending products item */ 3194.trending-product-item { 3195 display: flex; 3196 align-items: center; 3197 gap: 12px; 3198 padding: 12px 16px; 3199 cursor: pointer; 3200 transition: all 0.2s ease; 3201 border-bottom: 1px solid #f1f5f9; 3202 position: relative; 3203} 3204 3205.trending-product-item:hover { 3206 background: linear-gradient(90deg, #fef3c7 0%, #fde68a 100%); 3207 padding-left: 20px; 3208} 3209 3210.trending-product-item:hover::before { 3211 content: ''; 3212 position: absolute; 3213 left: 0; 3214 top: 0; 3215 bottom: 0; 3216 width: 4px; 3217 background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%); 3218} 3219 3220.trending-product-item:last-child { 3221 border-bottom: none; 3222} 3223 3224.trending-product-thumb { 3225 width: 48px; 3226 height: 48px; 3227 object-fit: cover; 3228 border-radius: 8px; 3229 border: 1px solid #e5e7eb; 3230 flex-shrink: 0; 3231 background: white; 3232} 3233 3234.trending-product-info { 3235 flex: 1; 3236 min-width: 0; 3237} 3238 3239.trending-product-name { 3240 font-size: 13px; 3241 font-weight: 400; 3242 color: #1f2937; 3243 line-height: 1.4; 3244 margin-bottom: 0; 3245 padding: 0 !important; 3246 overflow: hidden; 3247 text-overflow: ellipsis; 3248 display: -webkit-box; 3249 -webkit-line-clamp: 2; 3250 -webkit-box-orient: vertical; 3251} 3252 3253.trending-product-price { 3254 font-size: 12px; 3255 font-weight: 700; 3256 color: #f59e0b; 3257 padding: 4px 0 !important; 3258} 3259.trending-product { padding: 0;} 3260/* ==================== END TRENDING STYLES ==================== */ 3261 3262</style> 3263 <style data-inlined="product-detail.css"> 3264/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3265 3266/* Root / Container */ 3267.product-single { 3268 --pd-gap: 2rem; 3269 --pd-border: #e2e8f0; 3270 --pd-bg: #ffffff; 3271 --pd-muted: #64748b; 3272 --pd-dark: #0f172a; 3273 --pd-accent: #0d6efd; /* tema utama situs */ 3274 --pd-radius: 12px; 3275 --pd-soft: #f8fafc; 3276 --pd-soft-border: #e5e7eb; 3277 --pd-danger: #dc2626; 3278 --pd-success-bg: #dcfce7; 3279 --pd-success-text: #166534; 3280 --pd-danger-bg: #fee2e2; 3281 --pd-danger-text: #b91c1c; 3282 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3283 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3284 max-width:1280px; 3285 margin-inline:auto; 3286} 3287 3288/* === Sticky Product Header Bar (muncul saat scroll) === */ 3289.pd-sticky-header { 3290 position: fixed; 3291 top: 0; 3292 left: 0; 3293 right: 0; 3294 z-index: 998; 3295 background: rgba(255, 255, 255, 0.98); 3296 backdrop-filter: blur(12px); 3297 -webkit-backdrop-filter: blur(12px); 3298 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3299 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3300 transform: translateY(-100%); 3301 opacity: 0; 3302 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3303 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3304 pointer-events: none; 3305} 3306 3307.pd-sticky-header.visible { 3308 transform: translateY(0); 3309 opacity: 1; 3310 pointer-events: auto; 3311} 3312 3313.pd-sticky-header-container { 3314 max-width: 1280px; 3315 margin: 0 auto; 3316 padding: 12px 20px; 3317 display: flex; 3318 align-items: center; 3319 gap: 16px; 3320} 3321 3322.pd-sticky-thumb { 3323 width: 48px; 3324 height: 48px; 3325 flex-shrink: 0; 3326 border-radius: 8px; 3327 border: 1px solid #e2e8f0; 3328 overflow: hidden; 3329 background: #f8fafc; 3330} 3331 3332.pd-sticky-thumb img { 3333 width: 100%; 3334 height: 100%; 3335 object-fit: cover; 3336 display: block; 3337} 3338 3339.pd-sticky-info { 3340 flex: 1; 3341 min-width: 0; 3342 display: flex; 3343 flex-direction: column; 3344 gap: 4px; 3345} 3346 3347.pd-sticky-title { 3348 font-size: 0.9rem; 3349 font-weight: 600; 3350 color: #0f172a; 3351 line-height: 1.3; 3352 white-space: nowrap; 3353 overflow: hidden; 3354 text-overflow: ellipsis; 3355 margin: 0; 3356} 3357 3358.pd-sticky-price-row { 3359 display: flex; 3360 align-items: center; 3361 gap: 8px; 3362 flex-wrap: wrap; 3363} 3364 3365.pd-sticky-price { 3366 font-size: 1.1rem; 3367 font-weight: 700; 3368 color: #0d6efd; 3369 line-height: 1; 3370} 3371 3372.pd-sticky-price-original { 3373 font-size: 0.75rem; 3374 color: #94a3b8; 3375 text-decoration: line-through; 3376 line-height: 1; 3377} 3378 3379.pd-sticky-discount { 3380 background: #dc2626; 3381 color: #fff; 3382 font-size: 0.65rem; 3383 font-weight: 600; 3384 padding: 2px 6px; 3385 border-radius: 4px; 3386 letter-spacing: 0.3px; 3387 line-height: 1; 3388} 3389 3390.pd-sticky-actions { 3391 display: flex; 3392 gap: 8px; 3393 flex-shrink: 0; 3394 align-items: center; 3395} 3396 3397.pd-sticky-share-btn { 3398 padding: 8px 16px; 3399 border-radius: 8px; 3400 border: 1px solid #e2e8f0; 3401 background: #fff; 3402 color: #64748b; 3403 cursor: pointer; 3404 transition: all 0.2s ease; 3405 display: inline-flex; 3406 align-items: center; 3407 justify-content: center; 3408 flex-shrink: 0; 3409} 3410 3411.pd-sticky-share-btn:hover { 3412 background: #f8fafc; 3413 border-color: #cbd5e1; 3414 color: #0f172a; 3415 transform: translateY(-1px); 3416} 3417 3418.pd-sticky-share-btn:active { 3419 transform: translateY(0); 3420} 3421 3422.pd-sticky-share-btn svg { 3423 display: block; 3424 width: 16px; 3425 height: 16px; 3426} 3427 3428.pd-sticky-btn-chat svg { 3429 width: 16px !important; 3430 height: 16px !important; 3431 max-width: none !important; 3432 margin-right: 4px; 3433 flex-shrink: 0; 3434} 3435 3436.pd-sticky-btn { 3437 padding: 8px 16px; 3438 border-radius: 8px; 3439 font-size: 0.85rem; 3440 font-weight: 600; 3441 cursor: pointer; 3442 transition: all 0.2s ease; 3443 border: 0; 3444 display: inline-flex; 3445 align-items: center; 3446 justify-content: center; 3447 white-space: nowrap; 3448} 3449 3450.pd-sticky-btn-cart { 3451 background: #fff; 3452 color: #0d6efd; 3453 border: 1px solid #0d6efd; 3454} 3455 3456.pd-sticky-btn-cart:hover { 3457 background: rgba(13, 110, 253, 0.06); 3458} 3459 3460.pd-sticky-btn-chat { 3461 background: #fff; 3462 color: #64748b; 3463 border: 1px solid #cbd5e1; 3464 flex: 0.8; 3465} 3466 3467.pd-sticky-btn-chat:hover { 3468 background: #f8fafc; 3469 border-color: #94a3b8; 3470 color: #475569; 3471} 3472 3473.pd-sticky-btn-buy { 3474 background: #0d6efd; 3475 color: #fff; 3476 border: 1px solid #0d6efd; 3477 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3478} 3479 3480.pd-sticky-btn-buy:hover { 3481 filter: brightness(0.95); 3482} 3483 3484.pd-sticky-btn:disabled { 3485 opacity: 0.5;
3486 cursor: not-allowed; 3487} 3488 3489/* Hide on mobile (use existing FAB instead) */ 3490@media (max-width: 880px) { 3491 .pd-sticky-header { 3492 display: none; 3493 } 3494} 3495 3496/* === PRINT STYLES (for PDF Export) === */ 3497@media print { 3498 /* Reset & Base */ 3499 * { 3500 -webkit-print-color-adjust: exact !important; 3501 print-color-adjust: exact !important; 3502 } 3503 3504 body { 3505 background: white !important; 3506 margin: 0; 3507 padding: 0; 3508 } 3509 3510 /* Hide all navigation, buttons, and interactive elements */ 3511 nav, header, footer, 3512 nav *, header *, footer *, 3513 .navbar, .navbar *, 3514 .nav-item, .nav-link, 3515 .pd-sticky-header, 3516 .pd-sticky-header.visible, 3517 .pd-breadcrumb, 3518 .pd-sidebar, 3519 .pd-mobile-fab, 3520 .pd-share-btn, 3521 .pd-sticky-share-btn, 3522 #pd-share-btn, 3523 .share-button, 3524 [data-share-btn], 3525 button, 3526 .btn-add-cart, 3527 .btn-buy, 3528 .pd-actions, 3529 .pd-compare-section, 3530 .pd-compare-btn, 3531 .pd-compare-info, 3532 #pd-compare-btn, 3533 .compare-note, 3534 .compare-container, 3535 .compare-dropdown, 3536 .compare-panel, 3537 .compare-wrapper, 3538 [data-compare], 3539 [id*="compare"], 3540 [class*="compare"], 3541 .pd-thumb-gallery, 3542 .pd-installment-card, 3543 .pd-installment-card2, 3544 .inst-alt-banner--compact, 3545 .qoala-banner-card, 3546 .tradein-compact-card, 3547 .pwp-deals-section, 3548 #pwp-deals-section, 3549 .pwp-trigger-badge, 3550 .pd-related-full, 3551 #pd-similar, 3552 .pd-similar-strip, 3553 .pd-scroll-top, 3554 .stock-info-btn, 3555 .stock-popover, 3556 .pd-countdown-modern, 3557 .pd-share-modal-overlay, 3558 [id*="promo-"], 3559 .bottom-nav, 3560 .pd-buy-form, 3561 .pd-buy-card, 3562 .pd-extra-meta, 3563 .pd-reviews-section, 3564 #reviews, 3565 hr, 3566 .divider, 3567 .separator { 3568 display: none !important; 3569 visibility: hidden !important; 3570 opacity: 0 !important; 3571 height: 0 !important; 3572 max-height: 0 !important; 3573 overflow: hidden !important; 3574 margin: 0 !important; 3575 padding: 0 !important; 3576 border: none !important; 3577 } 3578 3579 /* Extra aggressive reset for any remaining borders */ 3580 .pd-info-card *, 3581 .pd-meta-inline *, 3582 .pd-variants * { 3583 border: none !important; 3584 border-top: none !important; 3585 border-bottom: none !important; 3586 } 3587 3588 /* Layout restructure */ 3589 .product-single { 3590 max-width: 100% !important; 3591 margin: 0 !important; 3592 padding: 0 !important; 3593 box-shadow: none !important; 3594 } 3595 3596 /* Reset all shadows and borders for print */ 3597 * { 3598 box-shadow: none !important; 3599 } 3600 3601 .pd-top-layout { 3602 display: grid !important; 3603 grid-template-columns: 280px 1fr !important; 3604 gap: 24px !important; 3605 margin: 0 0 24px 0 !important; 3606 padding: 0 !important; 3607 page-break-inside: avoid; 3608 align-items: start !important; 3609 } 3610 3611 /* Print header with logo - centered and larger */ 3612 .product-single::before { 3613 content: "" !important; 3614 display: block !important; 3615 width: 150px !important; 3616 height: 50px !important; 3617 background: url('/assets/img/logo.png') no-repeat center !important; 3618 background-size: contain !important; 3619 margin: 0 auto 20px !important; 3620 visibility: visible !important; 3621 opacity: 1 !important; 3622 } 3623 3624 /* Media card - left column */ 3625 .pd-media-card { 3626 border: none !important; 3627 padding: 0 !important; 3628 margin: 0 !important; 3629 text-align: center; 3630 page-break-inside: avoid; 3631 } 3632 3633 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3634 .pd-main-image { 3635 display: none !important; 3636 } 3637 3638 /* Mobile slider - show first image only in print */ 3639 .pd-mobile-slider { 3640 display: block !important; 3641 overflow: visible !important; 3642 } 3643 3644 .pd-mslide { 3645 display: none !important; 3646 } 3647 3648 .pd-mslide:first-child { 3649 display: block !important; 3650 border: 1px solid #e2e8f0 !important; 3651 max-width: 280px !important; 3652 margin: 0 auto !important; 3653 border-radius: 8px !important; 3654 } 3655 3656 .pd-mslide:first-child img { 3657 max-height: 280px !important; 3658 width: 100% !important; 3659 height: auto !important; 3660 object-fit: contain !important; 3661 } 3662 3663 /* Info card - right column */ 3664 .pd-info-card { 3665 border: none !important; 3666 border-top: none !important; 3667 border-bottom: none !important; 3668 padding: 0 !important; 3669 margin: 0 !important; 3670 background: white !important; 3671 box-shadow: none !important; 3672 } 3673 3674 /* Title */ 3675 .pd-title-row { 3676 display: block !important; 3677 margin-bottom: 10px !important; 3678 border: none !important; 3679 border-top: none !important; 3680 border-bottom: none !important; 3681 padding: 0 !important; 3682 box-shadow: none !important; 3683 text-decoration: none !important; 3684 } 3685 3686 /* Hide share button in print */ 3687 .pd-title-row .pd-share-btn, 3688 .pd-title-row #pd-share-btn { 3689 display: none !important; 3690 } 3691 3692 .pd-title { 3693 font-size: 13pt !important; 3694 color: #000 !important; 3695 margin: 0 0 10px 0 !important; 3696 page-break-after: avoid; 3697 font-weight: 700 !important; 3698 line-height: 1.3 !important; 3699 border: none !important; 3700 border-top: none !important; 3701 border-bottom: none !important; 3702 padding-bottom: 0 !important; 3703 box-shadow: none !important; 3704 text-decoration: none !important; 3705 } 3706 3707 /* Price and QR container - side by side */ 3708 .pd-price-qr-container { 3709 display: flex !important; 3710 gap: 16px !important;
3711 align-items: flex-start !important; 3712 margin-bottom: 14px !important; 3713 box-shadow: none !important; 3714 border: none !important; 3715 text-decoration: none !important; 3716 } 3717 3718 /* Price block - left side */ 3719 .pd-price-block { 3720 flex: 1 !important; 3721 margin: 0 !important; 3722 padding-bottom: 0 !important; 3723 border-bottom: none !important; 3724 box-shadow: none !important; 3725 } 3726 3727 .pd-price-row { 3728 margin-bottom: 8px !important; 3729 border: none !important; 3730 border-top: none !important; 3731 border-bottom: none !important; 3732 padding: 0 !important; 3733 } 3734 3735 /* Hide all empty or unnecessary elements in price row */ 3736 .pd-price-row > span:empty, 3737 .pd-price-row > .sold-inline, 3738 #pd-sold-inline { 3739 display: none !important; 3740 } 3741 3742 .pd-price-current { 3743 font-size: 18pt !important; 3744 color: #0d6efd !important; 3745 } 3746 3747 /* Show price range for variant products in print */ 3748 .pd-price-current[data-price-range]::after { 3749 content: attr(data-price-range) !important; 3750 display: block !important; 3751 font-size: 11pt !important; 3752 color: #64748b !important; 3753 margin-top: 4px !important; 3754 font-weight: normal !important; 3755 } 3756 3757 /* Hide 'Range:' prefix text */ 3758 .pd-price-current[data-price-range]::after { 3759 content: attr(data-price-range) !important; 3760 } 3761 3762 .pd-price-original { 3763 font-size: 12pt !important; 3764 color: #64748b !important; 3765 } 3766 3767 .pd-discount-badge { 3768 font-size: 10pt !important; 3769 padding: 2px 6px !important; 3770 } 3771 3772 /* QR Code section - right side beside price */ 3773 .pd-qr-print { 3774 display: block !important; 3775 flex: 0 0 100px !important; 3776 margin: 0 !important; 3777 padding: 8px !important; 3778 background: #f8fafc !important; 3779 border-radius: 8px !important; 3780 text-align: center !important; 3781 page-break-inside: avoid !important; 3782 border: 1px solid #cbd5e1 !important; 3783 } 3784 3785 .pd-qr-label { 3786 font-size: 6.5pt !important; 3787 color: #64748b !important; 3788 margin-bottom: 5px !important; 3789 line-height: 1.2 !important; 3790 font-weight: 600 !important; 3791 } 3792 3793 .pd-qr-image { 3794 width: 80px !important; 3795 height: 80px !important; 3796 display: block !important; 3797 margin: 0 auto !important; 3798 } 3799 3800 /* Meta info */ 3801 .pd-meta-inline { 3802 display: flex !important; 3803 gap: 12px !important; 3804 margin-bottom: 0 !important; 3805 padding-bottom: 14px !important; 3806 font-size: 10pt !important; 3807 border: none !important; 3808 border-top: none !important; 3809 border-bottom: none !important; 3810 padding-top: 0 !important; 3811 box-shadow: none !important; 3812 text-decoration: none !important; 3813 background: transparent !important; 3814 } 3815 3816 /* Hide stock badge in print */ 3817 .stock-badge, 3818 .stock-info-btn { 3819 display: none !important; 3820 } 3821 3822 .sku { 3823 color: #64748b !important; 3824 } 3825 3826 /* Hide sold badge if empty/zero */ 3827 .sold-inline { 3828 display: none !important; 3829 } 3830 3831 /* Hide variant summary section in print */ 3832 .pd-variant-combined, 3833 #pd-variant-combined, 3834 .pd-variant-summary, 3835 [data-role="sold-chip"] { 3836 display: none !important; 3837 } 3838 3839 /* Hide compare section completely in print */ 3840 .pd-compare-section, 3841 .pd-compare-btn, 3842 #pd-compare-btn, 3843 .pd-compare-info, 3844 .compare-note, 3845 .compare-text, 3846 .compare-remove { 3847 display: none !important; 3848 visibility: hidden !important; 3849 height: 0 !important; 3850 margin: 0 !important; 3851 padding: 0 !important; 3852 overflow: hidden !important; 3853 } 3854 3855 /* Variants - show all options with selected highlighted */ 3856 .pd-variants { 3857 margin: 0 !important; 3858 padding: 0 !important; 3859 page-break-inside: avoid !important; 3860 border: none !important; 3861 border-top: none !important; 3862 border-bottom: none !important; 3863 box-shadow: none !important; 3864 background: transparent !important; 3865 } 3866 3867 /* Add 'Variant:' label before variant groups */ 3868 .pd-variants::before { 3869 content: "Variant:" !important; 3870 display: block !important; 3871 font-size: 11pt !important; 3872 font-weight: 700 !important; 3873 color: #0f172a !important; 3874 margin-bottom: 10px !important; 3875 border: none !important; 3876 padding: 0 !important; 3877 border-top: none !important; 3878 border-bottom: none !important; 3879 } 3880 3881 .v-group { 3882 display: flex !important;
3883 align-items: center !important; 3884 gap: 8px !important; 3885 margin-bottom: 8px !important; 3886 line-height: 1 !important; 3887 page-break-inside: avoid !important; 3888 border: none !important; 3889 padding: 0 !important; 3890 } 3891 3892 .v-group:last-child { 3893 margin-bottom: 0 !important; 3894 } 3895 3896 .v-label { 3897 font-weight: 700 !important; 3898 margin: 0 !important; 3899 font-size: 9pt !important; 3900 color: #475569 !important; 3901 min-width: 60px !important; 3902 flex-shrink: 0 !important; 3903 border: none !important; 3904 padding: 0 !important; 3905 } 3906 3907 .v-options { 3908 display: flex !important; 3909 gap: 6px !important; 3910 flex-wrap: wrap !important; 3911 border: none !important; 3912 padding: 0 !important; 3913 margin: 0 !important; 3914 } 3915 3916 .v-chip { 3917 border: 1px solid #cbd5e1 !important; 3918 padding: 3px 10px !important; 3919 font-size: 9pt !important; 3920 background: #f8fafc !important; 3921 border-radius: 6px !important; 3922 display: inline-flex !important; 3923 align-items: center !important; 3924 page-break-inside: avoid !important; 3925 } 3926 3927 /* Highlight selected variant with blue background */ 3928 .v-chip.selected { 3929 background: #eff6ff !important; 3930 border-color: #2563eb !important; 3931 font-weight: 600 !important; 3932 } 3933 3934 /* Hide radio inputs */ 3935 .v-chip input[type="radio"] { 3936 display: none !important; 3937 } 3938 3939 /* Hide "Habis" badge in print */ 3940 .v-chip .chip-badge { 3941 display: none !important; 3942 } 3943 3944 /* Short description - HIDDEN in print */ 3945 .pd-short-desc { 3946 display: none !important; 3947 } 3948 3949 /* QR Code section - right side beside price */ 3950 .pd-qr-print { 3951 display: block !important; 3952 flex: 0 0 100px !important; 3953 margin: 0 !important; 3954 padding: 8px !important; 3955 background: #f8fafc !important; 3956 border-radius: 8px !important; 3957 text-align: center !important; 3958 page-break-inside: avoid !important; 3959 border: 1px solid #cbd5e1 !important; 3960 } 3961 3962 .pd-qr-label { 3963 font-size: 6.5pt !important; 3964 color: #64748b !important; 3965 margin-bottom: 5px !important; 3966 line-height: 1.2 !important; 3967 font-weight: 600 !important; 3968 } 3969 3970 .pd-qr-image { 3971 width: 80px !important; 3972 height: 80px !important; 3973 display: block !important; 3974 margin: 0 auto !important; 3975 } 3976 3977 /* Full description - below 2 column layout */ 3978 .pd-full-description { 3979 border: none !important; 3980 padding: 0 !important; 3981 margin: 24px 0 0 0 !important; 3982 page-break-before: avoid; 3983 grid-column: 1 / -1 !important; 3984 } 3985 3986 .pd-full-desc-title { 3987 font-size: 13pt !important; 3988 font-weight: 700 !important; 3989 margin: 0 0 12px 0 !important; 3990 color: #000 !important; 3991 border-top: none !important; 3992 border-bottom: none !important; 3993 padding-top: 16px !important; 3994 } 3995 3996 .pd-full-desc-body { 3997 font-size: 10pt !important; 3998 line-height: 1.6 !important; 3999 color: #334155 !important; 4000 } 4001 4002 .pd-full-desc-body img { 4003 max-height: 200px !important; 4004 width: auto !important; 4005 page-break-inside: avoid; 4006 display: none !important; /* Hide images in print to save space */ 4007 } 4008 4009 /* Also target #deskripsi img if exists */ 4010 #deskripsi img { 4011 max-width: 350px !important; 4012 height: auto !important; 4013 } 4014 4015 .pd-full-desc-body table { 4016 border-collapse: collapse !important; 4017 width: 100% !important; 4018 margin: 12px 0 !important; 4019 font-size: 9pt !important; 4020 } 4021 4022 .pd-full-desc-body th, 4023 .pd-full-desc-body td { 4024 border: 1px solid #cbd5e1 !important; 4025 padding: 6px 8px !important; 4026 } 4027 4028 /* Print footer */ 4029 .product-single::after { 4030 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 4031 display: block; 4032 margin-top: 30px; 4033 padding-top: 16px; 4034 border-top: 1px solid #e2e8f0; 4035 font-size: 9pt; 4036 color: #64748b; 4037 text-align: center; 4038 } 4039 4040 /* Page breaks */ 4041 .pd-media-card, 4042 .pd-price-block, 4043 .pd-full-desc-title { 4044 page-break-after: avoid; 4045 } 4046 4047 .pd-full-description { 4048 page-break-before: auto; 4049 } 4050} 4051 4052/* === Breadcrumb (truncate last item) === */ 4053.pd-breadcrumb{ 4054 display:flex; 4055 flex-wrap:nowrap; 4056 align-items:center; 4057 gap:.5rem; 4058 font-size:.72rem; 4059 margin:.7rem 0 1.2rem .5rem; 4060 color:var(--pd-muted); 4061 overflow:hidden; 4062} 4063.pd-breadcrumb a{color:#ffffff;text-decoration:none} 4064.pd-breadcrumb a:hover{text-decoration:underline} 4065.pd-breadcrumb a, 4066.pd-breadcrumb span{flex:0 0 auto} 4067.pd-breadcrumb .current{ 4068 flex:1 1 auto; 4069 min-width:0; 4070 white-space:nowrap; 4071 overflow:hidden; 4072 text-overflow:ellipsis; 4073 font-weight:600; 4074 color:var(--pd-dark); 4075 padding-right:2px; 4076} 4077 4078/* === Top layout === */ 4079.pd-top-layout{ 4080 display:grid; 4081 gap:var(--pd-gap); 4082 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 4083 align-items:start; 4084 margin-bottom:2.4rem; 4085} 4086 4087/* === Media column === */ 4088.pd-media-card{ 4089 background:#fff; 4090 border:1px solid var(--pd-border); 4091 border-radius:var(--pd-radius); 4092 padding:1rem 1rem 1.2rem; 4093} 4094.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4095.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4096 4097/* Gallery Thumbnails */ 4098.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4099.pd-thumb-btn{ 4100 border:1px solid var(--pd-soft-border); 4101 background:#fff;
4102 cursor:pointer; 4103 width:64px;height:64px;padding:2px; 4104 display:flex;align-items:center;justify-content:center; 4105 overflow:hidden;border-radius:8px; 4106 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4107 position:relative; 4108} 4109.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4110.pd-thumb-btn:hover{border-color:#ffffff} 4111.pd-thumb-btn.active{border-color:#ffffff;box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4112.pd-thumb-btn:focus-visible{outline:2px solid #ffffff;outline-offset:2px} 4113.pd-thumb-btn:active{transform:scale(.96)} 4114 4115/* === Info column === */ 4116.pd-info-card{ 4117 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4118 padding:1.4rem 1.6rem 2rem; 4119} 4120.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4121.pd-price-block{margin:0 0 1rem} 4122.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4123.pd-price-current{font-size:2rem;font-weight:700;color:#ffffff} 4124.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4125.pd-discount-badge{background:#821603;color:#fff;font-size:.68rem;padding:4px 8px 3px;font-weight:600;border-radius:6px;letter-spacing:.5px} 4126.pd-meta-inline{display:flex;flex-wrap:wrap;gap:.9rem;align-items:center;font-size:.63rem;letter-spacing:.6px;text-transform:uppercase;font-weight:600} 4127.stock-badge{padding:4px 10px;border-radius:24px;background:var(--pd-soft);color:var(--pd-muted);display:inline-block;line-height:1;vertical-align:middle} 4128.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4129.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4130.digital-badge{padding:4px 10px;border-radius:24px;background:linear-gradient(135deg,#FF5722 0%,#764ba2 100%);color:#fff;font-size:.63rem;font-weight:600;letter-spacing:.6px;text-transform:uppercase;display:inline-block;line-height:1;vertical-align:middle;border:none} 4131.sku{color:var(--pd-muted)} 4132.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4133.pd-short-desc p{margin:0 0 .75rem} 4134.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4135 4136/* Meta row for sold and rating badges */ 4137.pd-meta-row { 4138 display: flex; 4139 align-items: center; 4140 flex-wrap: wrap; 4141 gap: 5px; 4142 margin-top: 8px; 4143 margin-bottom: 4px; 4144} 4145 4146/* Sales (Terjual) badge inline */ 4147.sold-inline{ 4148 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4149 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4150} 4151 4152/* Price row divider */ 4153.price-divider { 4154 display: inline-block; 4155 margin: 0 .4rem; 4156 color: #cbd5e1; 4157 font-weight: 400; 4158} 4159 4160/* Rating badge inline */ 4161.rating-inline { 4162 display: inline-flex; 4163 align-items: center; 4164 gap: 4px; 4165 padding: 3px 8px 4px; 4166 background: #fff7ed; 4167 border-radius: 6px; 4168 font-size: .65rem; 4169 font-weight: 600; 4170 color: #ea580c; 4171 text-decoration: none; 4172 transition: all .2s ease; 4173 cursor: pointer; 4174 margin-left: .5rem; 4175 white-space: nowrap; 4176} 4177 4178.rating-inline:hover { 4179 background: #ffedd5; 4180 transform: translateY(-1px); 4181 box-shadow: 0 2px 4px rgba(234, 88, 12, 0.1); 4182} 4183 4184.rating-inline .star-icon { 4185 font-size: .75rem; 4186 color: #f59e0b; 4187 line-height: 1; 4188} 4189 4190.rating-inline .rating-value { 4191 color: #ea580c; 4192} 4193 4194.rating-inline .rating-badge-count { 4195 color: #9a3412; 4196 opacity: 0.85; 4197} 4198 4199@media (max-width: 640px) { 4200 .rating-inline { 4201 font-size: .65rem; 4202 padding: 3px 8px 4px; 4203 } 4204 .rating-inline .star-icon { 4205 font-size: .7rem; 4206 } 4207} 4208 4209/* Highlight animation for review section */ 4210@keyframes highlightFlash { 4211 0%, 100% { background-color: transparent; } 4212 50% { background-color: rgba(251, 146, 60, 0.1); } 4213} 4214 4215.highlight-flash { 4216 animation: highlightFlash 1.5s ease; 4217} 4218 4219/* === Variant chips === */ 4220.pd-variants{margin-top:14px} 4221.v-group{margin-bottom:12px} 4222.v-label{font-weight:600;margin-bottom:6px} 4223.v-options{display:flex;flex-wrap:wrap;gap:8px} 4224.v-chip{ 4225 display:inline-flex;align-items:center;gap:8px; 4226 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4227 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4228} 4229.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4230.v-chip.selected{border-color:#2563eb;background:#eff6ff} 4231.v-chip.disabled{opacity:.45;cursor:not-allowed} 4232.v-chip.out{border-style:dashed} 4233.v-chip input{display:none} 4234.v-chip .dot{width:14px;
4234height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4235.v-chip .dot--neutral{background:#9aa3af} 4236.v-chip .txt{font-size:.95rem;white-space:nowrap} 4237.v-chip .chip-badge{margin-left:2px;font-size:.72rem;line-height:1;padding:3px 6px;border-radius:6px;background:#fee2e2;color:#991b1b;border:1px solid #fecaca} 4238.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4239.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4240 4241/* === Buy form (di sidebar kanan untuk desktop) === */ 4242.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4243.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4244.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4245.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4246 4247/* Tombol senada tema */ 4248.btn-add-cart,.btn-buy,.btn-chat-icon{ 4249 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4250 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4251 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4252} 4253.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4254.btn-add-cart:hover{filter:brightness(.95)} 4255.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4256.btn-buy:hover{background:rgba(13,110,253,.06)} 4257.btn-chat-icon{background:#fff;color:#fb923c;border:2px solid #fb923c;padding:0;width:44px;min-width:44px} 4258.btn-chat-icon:hover{background:rgba(251, 146, 60, 0.06);border-color:#f97316} 4259.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4260.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4261 4262/* Buy with Voucher Button - Cleaner, more compact design */ 4263.btn-buy-voucher{ 4264 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4265 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4266 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4267 background:linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); 4268 color:#fff; 4269 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4270 position:relative; 4271 overflow:hidden; 4272 flex:1 1 auto 4273} 4274.btn-buy-voucher:hover{ 4275 transform:translateY(-2px); 4276 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4277} 4278.btn-buy-voucher:active{ 4279 transform:translateY(0); 4280 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4281} 4282.btn-buy-voucher .bv-icon{ 4283 font-size:20px; 4284 line-height:1; 4285 flex-shrink:0 4286} 4287.btn-buy-voucher .bv-text{ 4288 display:flex; 4289 flex-direction:column; 4290 align-items:flex-start; 4291 gap:2px; 4292 flex:1; 4293 min-width:0; 4294 font-size:14px; 4295 line-height:1.2 4296} 4297.btn-buy-voucher .bv-text small{ 4298 font-size:10px; 4299 opacity:0.85; 4300 font-weight:500; 4301 letter-spacing:0.3px; 4302 font-family:monospace 4303} 4304.btn-buy-voucher .bv-save{ 4305 font-size:12px; 4306 font-weight:700; 4307 background:rgba(0,0,0,0.25); 4308 padding:4px 8px; 4309 border-radius:6px; 4310 white-space:nowrap; 4311 flex-shrink:0 4312} 4313 4314/* Extra meta (opsional) */ 4315.pd-extra-meta{background:var(--pd-soft);border:1px solid var(--pd-border);border-radius:10px;padding:.85rem 1rem;display:grid;gap:.45rem;font-size:.7rem;line-height:1.3} 4316.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4317.pd-extra-meta a{color:#ffffff;text-decoration:none} 4318.pd-extra-meta a:hover{text-decoration:underline} 4319 4320/* === Sidebar === */ 4321.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4322.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4323.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4324.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4325.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4326.pd-side-card li:before{content:'';width:6px;height:6px;background:#ffffff;border-radius:50%;position:absolute;
4326left:0;top:.45rem} 4327.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4328.pay-logos span{font-size:.55rem;background:var(--pd-soft);border:1px solid var(--pd-soft-border);padding:.35rem .55rem;border-radius:6px;font-weight:600;letter-spacing:.5px} 4329 4330/* Buy card hanya desktop (mobile pakai FAB) */ 4331.pd-buy-card{display:block} 4332@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4333 4334/* === Full Description === */ 4335.pd-full-description{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1.6rem 1.9rem .6rem;margin:0 0 .8rem} 4336.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4337.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4338.pd-full-desc-body p{margin:0 0 1rem} 4339.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4340.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4341.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4342.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4343.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4344 4345/* === Related (container) === */ 4346.pd-related-full{ 4347 background:#fff;border:1px solid var(--pd-border); 4348 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4349 margin:0 0 2rem; 4350} 4351.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4352 4353/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4354.pd-related-grid, 4355.pd-related-grid--full{ 4356 display:grid !important; 4357 gap:16px !important; 4358 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4359 align-items:stretch; 4360} 4361 4362/* Fallback jika grid tidak didukung */ 4363@supports not (display:grid){ 4364 .pd-related-grid, 4365 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4366 .pd-related-grid .pd-related-card, 4367 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4368 @media (max-width:1100px){ 4369 .pd-related-grid .pd-related-card, 4370 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4371 } 4372 @media (max-width:820px){ 4373 .pd-related-grid .pd-related-card, 4374 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4375 } 4376 @media (max-width:580px){ 4377 .pd-related-grid .pd-related-card, 4378 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4379 } 4380} 4381 4382/* Related Card */ 4383.pd-related-card{ 4384 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4385 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4386 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4387} 4388.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4389.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4390.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4391.mini-disc{position:absolute;top:6px;left:6px;background:#dc2626;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4392.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4393.pd-related-card h3{font-size:.68rem;line-height:1.25;margin:0;height:2.5em;overflow:hidden;font-weight:600;color:#334155;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} 4394.pd-related-card .price{font-size:.7rem;font-weight:600;color:#ffffff} 4395.pd-related-card .sold{display:block;font-size:.6rem;font-weight:600;color:#475569;margin-top:2px;letter-spacing:.3px;background:#f1f5f9;padding:2px 6px 3px;border-radius:5px; width: max-content;} 4396.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4397.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4398 4399/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4400.pd-mobile-slider{display:none} 4401.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4402 4403/* Gallery counter badge */ 4404.pd-gallery-counter{ 4405 position:absolute; 4406 bottom:12px; 4407 left:12px; 4408 background:rgba(0,0,0,0.25); 4409 color:#9E9E9E; 4410 padding:4px 10px; 4411 border-radius:10px; 4412 font-size:12px; 4413 font-weight:600; 4414 z-index:100; 4415 backdrop-filter:blur(6px); 4416 -webkit-backdrop-filter:blur(6px); 4417 font-variant-numeric:tabular-nums; 4418 pointer-events:none; 4419 display:none; 4420 box-shadow:0 1px 4px rgba(0,0,0,.15); 4421 line-height:1; 4422 border:1px solid rgba(0,0,0,0.08); 4423} 4424 4425@media (max-width:640px){ 4426 .pd-media-card{border: 0;padding: 0 1rem 0;} 4427 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4428 .pd-side-card {flex: 1 1 !important;} 4429 .pd-top-layout {gap: 0; margin-bottom: 0;} 4430 .pd-variants {margin-bottom: 1.5rem;} 4431 .pd-main-image{display:none} 4432 .pd-gallery-counter{display:block} 4433 .pd-mobile-slider-wrapper{ 4434 position:relative; 4435 margin-bottom:.6rem; 4436 } 4437 .pd-mobile-slider{ 4438 display:flex;gap:8px;overflow-x:auto; 4439 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch; 4440 } 4441 .pd-mslide{ 4442 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4443 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4444 } 4445 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4446} 4447 4448/* === Mobile Floating Action Bar === */ 4449.pd-mobile-fab{ 4450 position:fixed; 4451 left:0; 4452 right:0; 4453 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4454 display:none; 4455 align-items:center; 4456 gap:.75rem; 4457 padding:12px 16px; 4458 background:rgba(0,0,0,0.95); 4459 backdrop-filter:blur(20px); 4460 -webkit-backdrop-filter:blur(20px); 4461 border-top:1px solid rgba(228,231,235,0.6); 4462 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4463 z-index:999; /* Below bottom-nav (1000) */ 4464 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4465} 4466 4467/* When bottom-nav is hidden, move FAB to bottom */ 4468body:has(.bottom-nav.hidden) .pd-mobile-fab { 4469 bottom: 0; 4470} 4471.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4472.mf-btn{ 4473 border-radius:10px; 4474 padding:.6rem 1rem; 4475 font-weight:600; 4476 font-size:14px; 4477 cursor:pointer; 4478 transition:all .2s; 4479 -webkit-tap-highlight-color:transparent; 4480 display:inline-flex; 4481 align-items:center; 4482 justify-content:center; 4483 gap:4px; 4484} 4485.mf-buy{ 4486 background:#fb923c; 4487 color:#fff; 4488 border:0; 4489 box-shadow:0 2px 8px rgba(251, 146, 60, 0.3); 4490 flex:1; 4491 flex-direction:column; 4492 padding:.55rem 1rem; 4493 line-height:1; 4494 min-height:44px; 4495 font-family:inherit; 4496} 4497.mf-buy .mf-buy-text{ 4498 font-size:11px; 4499 font-weight:600; 4500 opacity:0.95; 4501 font-family:inherit; 4502} 4503.mf-buy .mf-buy-price{ 4504 font-size:13px; 4505 font-weight:800; 4506 letter-spacing:-0.3px; 4507 font-family:inherit; 4508 line-height:1; 4509} 4510.mf-buy:active{transform:scale(0.97)} 4511.mf-cart{ 4512 background:#fff;
4513 color:#fb923c; 4514 border:2px solid #fb923c; 4515 width:44px; 4516 height:44px; 4517 min-height:44px; 4518 padding:0; 4519 flex-shrink:0; 4520 font-family:inherit; 4521} 4522.mf-cart:active{transform:scale(0.95)} 4523.mf-chat{background:#fff;color:#fb923c;border:2px solid #fb923c;width:44px;height:44px;min-height:44px;padding:0;flex-shrink:0;font-family:inherit} 4524.mf-chat:active{transform:scale(0.95)} 4525.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4526.mf-share{ 4527 background:#fff; 4528 color:#fb923c; 4529 border:2px solid #fb923c; 4530 width:44px; 4531 height:44px; 4532 padding:0; 4533 flex-shrink:0; 4534} 4535.mf-share:active{transform:scale(0.95)} 4536.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4537@media (max-width:640px){ 4538 .pd-mobile-fab{display:flex} 4539 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4540} 4541 4542/* === Installment cards === */ 4543.pd-installment-card{border:1px solid var(--pd-border);background:#fff;border-radius:var(--pd-radius);padding:1rem 1.1rem 1.25rem;display:flex;flex-direction:column;gap:.85rem} 4544.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4545 4546.inst-tabs{display:flex;gap:.4rem} 4547.inst-tab{flex:1 1 0;background:var(--pd-soft);border:1px solid var(--pd-soft-border);padding:.45rem .5rem;font-size:.63rem;font-weight:600;border-radius:6px;cursor:pointer;letter-spacing:.4px;transition:var(--pd-transition)} 4548.inst-tab.active,.inst-tab:hover{background:#ffffff;color:#fff;border-color:#ffffff} 4549.inst-tab:focus{outline:2px solid #ffffff;outline-offset:2px} 4550 4551.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4552.tenor-btn{flex:1 0 auto;background:#fff;border:1px solid var(--pd-soft-border);padding:.42rem .55rem;font-size:.6rem;font-weight:600;border-radius:20px;cursor:pointer;transition:.18s;min-width:56px} 4553.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4554 4555.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4556.inst-line{display:flex;justify-content:space-between;align-items:center;padding:.35rem .55rem;background:var(--pd-soft);border:1px solid var(--pd-soft-border);border-radius:6px} 4557.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4558.inst-line .lbl{opacity:.8} 4559.inst-line .val{font-weight:600;letter-spacing:.3px} 4560.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4561@media (max-width: 860px){ 4562 .pd-installment-card{order:3} 4563 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4564 4565 /* Voucher button responsive - make it simpler */ 4566 .btn-buy-voucher{ 4567 padding:0 12px; 4568 gap:8px 4569 } 4570 .btn-buy-voucher .bv-icon{ 4571 font-size:18px 4572 } 4573 .btn-buy-voucher .bv-text{ 4574 font-size:13px 4575 } 4576 .btn-buy-voucher .bv-text small{ 4577 font-size:9px 4578 } 4579 .btn-buy-voucher .bv-save{ 4580 font-size:11px; 4581 padding:3px 6px 4582 } 4583} 4584 4585/* Installment List v2 (eye-catching) */ 4586.pd-installment-card2{ 4587 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4588 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4589 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4590 position:relative;overflow:hidden 4591} 4592.pd-installment-card2:before{ 4593 content:'';position:absolute;width:140px;height:140px; 4594 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4595 top:-40px;right:-40px;pointer-events:none 4596} 4597.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4598.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4599.inst-head-badge{background:#1d7df0;color:#fff;font-size:.55rem;font-weight:600;padding:.25rem .5rem;border-radius:20px;letter-spacing:.4px;box-shadow:0 2px 6px -2px rgba(29,125,240,.6)} 4600 4601.inst-bank-tabs{display:flex;gap:.45rem} 4602.bank-tab{flex:1 1 0;background:#fff;border:1px solid var(--pd-soft-border);padding:.5rem .55rem;font-size:.63rem;font-weight:600;border-radius:7px;cursor:pointer;transition:.18s;letter-spacing:.3px} 4603.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4604.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4605 4606.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4607.inst-month-item{display:flex;align-items:center;gap:.65rem;background:#fff;border:1px solid var(--pd-soft-border);border-radius:14px;padding:.55rem .7rem;position:relative;overflow:hidden;transition:.25s} 4608.inst-month-item:before{content:'';position:absolute;inset:0;opacity:0;background:linear-gradient(90deg,rgba(29,125,240,.08),rgba(29,125,240,0));transition:.25s} 4609.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4610.inst-month-item:hover:before{opacity:1} 4611.tenor-badge{background:#1d7df0;color:#fff;font-weight:700;min-width:42px;text-align:center;padding:.38rem .4rem;font-size:.8rem;border-radius:10px;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4612.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4613.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4614.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4615 4616.inst-note{font-size:.6rem;font-weight:600;color:#0f2f54;background:#e0f7ed;border:1px solid #b2ebd2;padding:.4rem .6rem;border-radius:8px;letter-spacing:.3px} 4617.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4618 4619@media (max-width:860px){ 4620 .pd-installment-card2{order:3} 4621 .tenor-badge{min-width:38px;font-size:.75rem} 4622 .amount{font-size:.78rem} 4623} 4624 4625/* Alt Fintech Banner (compact) */ 4626.inst-alt-banner--compact{ 4627 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4628 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4629 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4630} 4631.inst-alt-banner--compact:before, 4632.inst-alt-banner--compact:after{ 4633 content:'';position:absolute;border-radius:50%;background:rgba(0,0,0,.15);filter:blur(2px);pointer-events:none; 4634} 4635.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4636.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4637.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4638.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4639.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4640 4641.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4642.iab-logos-compact .logo-fin{width:30%} 4643.iab-logos-compact .logo{ 4644 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4645 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4646 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(0,0,0,.15); 4647 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4648} 4649.iab-logos-compact .logo:before{ 4650 content:'';position:absolute;inset:0;border-radius:inherit; 4651 background:linear-gradient(140deg,rgba(0,0,0,.35),rgba(0,0,0,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4652} 4653.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4654 4655.iab-cta--compact{ 4656 display:inline-flex;padding:.45rem .85rem;font-size:.58rem;font-weight:700;background:#fff;color:#0f3c6d;border-radius:30px;text-decoration:none;letter-spacing:.55px;box-shadow:0 4px 12px -5px rgba(0,0,0,.35);transition:.25s; 4657} 4658.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4659.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4660@media (max-width:860px){ 4661 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4662 .iab-logos-compact .logo{width:46px;height:46px} 4663} 4664 4665/* === Scroll To Top (baru) === */ 4666.pd-scroll-top{ 4667 position:fixed; 4668 right:20px; 4669 bottom:90px; 4670 width:48px; 4671 height:48px; 4672 border-radius:50%; 4673 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4674 color:#0f2f54; 4675 border:1px solid rgba(226, 232, 240, 0.8); 4676 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4677 display:flex; 4678 align-items:center; 4679 justify-content:center; 4680 cursor:pointer; 4681 z-index:1105; 4682 opacity:0; 4683 transform:translateY(12px) scale(.92); 4684 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4685 backdrop-filter:blur(8px); 4686 -webkit-backdrop-filter:blur(8px); 4687} 4688.pd-scroll-top:hover{ 4689 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4690 transform:translateY(0) scale(1); 4691 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4692 border-color:rgba(226, 232, 240, 1); 4693} 4694.pd-scroll-top:active{ 4695 transform:translateY(0) scale(.96); 4696} 4697.pd-scroll-top:focus-visible{outline:2px solid #ffffff;outline-offset:2px} 4698.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4699@media (max-width:640px){ 4700 /* Dynamic positioning via JavaScript */ 4701 .pd-scroll-top{ 4702 right:14px; 4703 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4704 } 4705} 4706 4707/* === Responsive layout tweaks === */ 4708@media (max-width:1250px){ 4709 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4710 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4711 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4712 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4713 .pd-buy-card{flex:0 0 100%} 4714} 4715@media (max-width:880px){ 4716 .pd-top-layout{grid-template-columns:1fr} 4717 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4718 .pd-title{font-size:1.2rem} 4719 .pd-price-current{font-size:1.5rem} 4720} 4721@media (max-width:640px){ 4722 /* Related: 2 kolom di mobile */ 4723 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4724 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4725} 4726@media (max-width:560px){ 4727 .pd-actions{flex-direction:column} 4728 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4729 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4730} 4731@media (max-width:380px){ 4732 .pd-related-full{padding:.9rem .7rem 1.1rem} 4733 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4734 .pd-related-card .info{padding:.4rem .48rem .55rem} 4735} 4736 4737/* === Modern Countdown (Product Detail) === */ 4738.pd-countdown-modern{ 4739 --cd-bg1:#0ea5e9; /* sky-500 */ 4740 --cd-bg2:#821603; /* indigo-500 */ 4741 --cd-text:#000000; 4742 --cd-contrast:#ffffff; 4743 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4744 --cd-border: rgba(0,0,0,.25); 4745 --cd-box: rgba(0,0,0,.65); 4746 --cd-box-text:#0f172a; 4747 --cd-danger:#ef4444; 4748 4749 display:flex; gap:.75rem; align-items:center; 4750 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4751 border:1px solid var(--cd-border); 4752 color:var(--cd-contrast); 4753 border-radius:14px; 4754 padding:.6rem .8rem; 4755 box-shadow:var(--cd-shadow); 4756 position:relative; 4757 overflow:hidden; 4758 margin-bottom: 1rem; 4759} 4760 4761.pd-countdown-modern .pd-cd-icon{ 4762 width:28px;height:28px;flex:0 0 28px; 4763 display:grid;place-items:center; 4764 background:rgba(0,0,0,.22); 4765 border:1px solid var(--cd-border); 4766 border-radius:10px; 4767 box-shadow: inset 0 1px 0 rgba(0,0,0,.35); 4768} 4769.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4770
4771.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4772 4773.pd-countdown-modern .pd-cd-label{ 4774 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4775 text-shadow:0 1px 0 rgba(0,0,0,.08); 4776} 4777 4778.pd-countdown-modern .pd-cd-time{ 4779 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4780 line-height:1; 4781} 4782 4783.pd-countdown-modern .unit{ 4784 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4785 min-width:58px; 4786} 4787.pd-countdown-modern .num{ 4788 display:inline-flex; align-items:center; justify-content:center; 4789 min-width:58px; height:40px; padding:0 .4rem; 4790 background:var(--cd-box); 4791 color:var(--cd-box-text); 4792 border:1px solid rgba(15,23,42,.08); 4793 border-radius:10px; 4794 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4795 letter-spacing:.5px; 4796 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(0,0,0,.55); 4797} 4798.pd-countdown-modern .unit-label{ 4799 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(0,0,0,.15); 4800} 4801 4802.pd-countdown-modern .colon{ 4803 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4804 animation: cdBlink 1.2s infinite steps(2, jump-none); 4805} 4806@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4807 4808.pd-countdown-modern .pd-cd-progress{ 4809 display:none; 4810 position:relative; 4811 height:6px; border-radius:999px; overflow:hidden; 4812 background:rgba(0,0,0,.25); 4813 border:1px solid rgba(0,0,0,.28); 4814} 4815.pd-countdown-modern .pd-cd-progress .bar{ 4816 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4817 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4818 transition:width .35s ease; 4819} 4820 4821/* State saat hampir habis */ 4822.pd-countdown-modern.danger{ 4823 --cd-bg1:#f97316; /* orange-500 */ 4824 --cd-bg2:#ef4444; /* red-500 */ 4825} 4826 4827/* Responsive */ 4828@media (max-width:640px){ 4829 .pd-countdown-modern{ 4830 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4831 } 4832 .pd-countdown-modern .unit{min-width:52px} 4833 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4834} 4835 4836/* === Modern Countdown polish (shine + compact) === */ 4837.pd-countdown-modern{ 4838 position:relative; 4839} 4840.pd-countdown-modern::after{ 4841 content:""; 4842 position:absolute; inset:-1px; 4843 background: linear-gradient(120deg, rgba(0,0,0,.12), rgba(0,0,0,0) 35%), 4844 radial-gradient(120% 120% at 0% 0%, rgba(0,0,0,.08), rgba(0,0,0,0) 60%); 4845 border-radius: inherit; 4846 pointer-events:none; 4847 mix-blend-mode: screen; 4848 animation: sheen 4.5s ease-in-out infinite; 4849} 4850@keyframes sheen { 4851 0% { opacity:.35; transform: translateX(-10%) } 4852 50% { opacity:.15; transform: translateX(4%) } 4853 100% { opacity:.35; transform: translateX(-10%) } 4854} 4855 4856/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4857.pd-countdown-modern[data-mode="compact"]{ 4858 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4859} 4860.pd-countdown-modern[data-mode="compact"] .unit{ 4861 min-width:46px; 4862} 4863.pd-countdown-modern[data-mode="compact"] .num{ 4864 min-width:46px; height:34px; font-size:.92rem; 4865} 4866.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4867 font-size:.66rem; 4868} 4869.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4870 width:24px; height:24px; border-radius:8px; 4871} 4872.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4873 height:5px; 4874} 4875 4876/* ============================================================ 4877 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4878 Buy card = form & paylater on the same row. 4879 Alt-fintech logos compact (no over-stretching). 4880 ============================================================ */ 4881@media (min-width:641px) and (max-width:1250px) { 4882 /* Buy card: put qty/buy buttons and paylater promo inline */ 4883 .pd-buy-card { 4884 display: flex !important; 4885 flex-direction: row; 4886 flex-wrap: wrap; 4887 align-items: center; 4888 gap: .6rem .75rem; 4889 } 4890 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4891 .pd-buy-card .paylater-promo { 4892 flex: 0 0 auto; 4893 align-self: center; 4894 margin: 0; 4895 max-width: 44%; 4896 } 4897 4898 /* Alt-fintech banner: tighter logos */ 4899 .iab-logos-compact { 4900 justify-content: center; 4901 gap: 1.25rem; 4902 } 4903 .iab-logos-compact .logo-fin { 4904 width: auto; 4905 max-height: 28px; 4906 object-fit: contain; 4907 } 4908} 4909 4910/* ============================================================ 4911 Tablet layout (641px â 880px) 4912 Restores 2-column grid + horizontal sidebar for mid-size screens. 4913 Overrides the generic â¤880px mobile-collapse rules above. 4914 ============================================================ */ 4915@media (min-width:641px) and (max-width:880px) { 4916 /* 2-column grid: image-card | info-card */ 4917 .pd-top-layout { 4918 grid-template-columns: minmax(260px, 40%) 1fr; 4919 gap: 1rem; 4920 } 4921 4922 /* Sidebar: full-width horizontal flex row below the two main columns */ 4923 .pd-sidebar { 4924 grid-column: 1 / -1; 4925 flex-direction: row; 4926 flex-wrap: wrap; 4927 gap: .75rem; 4928 padding: 0; 4929 } 4930 /* Banner cards fill 2-per-row */ 4931 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4932 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4933 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4934 4935 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4936 .pd-installment-card2, 4937 .inst-alt-banner--compact { display: none !important; } 4938
4939 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4940 .pd-sticky-header { display: flex !important; } 4941 4942 /* Typography â tighten slightly to fit the narrower info column */ 4943 .pd-price-current { font-size: 1.65rem; } 4944 .pd-title { font-size: 1.1rem; } 4945 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4946} 4947</style> 4948 <style data-inlined="product-grid.css"> 4949/* Product Grid (Firebird) */ 4950 4951.product-archive { 4952 max-width: 1320px; 4953 margin-inline: auto; 4954 padding: 0 0 3rem; 4955 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4956} 4957 4958.pa-toolbar { 4959 display: flex; 4960 flex-wrap: wrap; 4961 gap: 1rem; 4962 justify-content: space-between; 4963 align-items: flex-start; 4964} 4965 4966.pa-title { 4967 font-size: 1.4rem; 4968 margin: 0; 4969 font-weight: 600; 4970 line-height: 1.2; 4971 color: #0f172a; 4972} 4973 4974.pa-left { 4975 display: flex; 4976 flex-wrap: wrap; 4977 gap: .75rem; 4978 align-items: center; 4979} 4980 4981.pa-filter-badge { 4982 background: #f1f5f9; 4983 border: 1px solid #e2e8f0; 4984 padding: .45rem .6rem; 4985 border-radius: 8px; 4986 font-size: .7rem; 4987 display: flex; 4988 align-items: center; 4989 gap: .4rem; 4990 line-height: 1; 4991 color: #475569; 4992} 4993.pa-filter-badge strong { color: #0f172a; } 4994.pa-filter-badge .clear { 4995 display:inline-block; 4996 text-decoration:none; 4997 background:#e2e8f0; 4998 color:#475569; 4999 font-weight:600; 5000 padding:0 .35rem .05rem; 5001 border-radius:4px; 5002 transition:.2s; 5003 font-size:.75rem; 5004 line-height:1; 5005} 5006.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 5007 5008.pa-right { 5009 display:flex; 5010 gap:1rem; 5011 flex-wrap:wrap; 5012 align-items:flex-start; 5013 margin-bottom: 0.5rem; 5014} 5015 5016.pa-search { 5017 display:flex; 5018 gap:.5rem; 5019 align-items:center; 5020 background:#fff; 5021 border:1px solid #e2e8f0; 5022 padding:.4rem .5rem; 5023 border-radius:8px; 5024} 5025.pa-search input[type=text] { 5026 border:0; 5027 outline:none; 5028 font-size:.8rem; 5029 padding:.35rem .4rem;
5030 min-width:200px; 5031} 5032.pa-search button { 5033 background:#0d6efd; 5034 border:0; 5035 color:#fff; 5036 font-size:.7rem; 5037 padding:.45rem .75rem; 5038 border-radius:6px; 5039 cursor:pointer; 5040 font-weight:600; 5041 letter-spacing:.3px; 5042 transition:.25s; 5043} 5044.pa-search button:hover { background:#0b5dc4; } 5045 5046.pa-sort { 5047 display:flex; 5048 gap:.4rem; 5049 align-items:center; 5050 font-size:.7rem; 5051 background:#fff; 5052 border:1px solid #e2e8f0; 5053 padding:.4rem .55rem; 5054 border-radius:8px; 5055} 5056.pa-sort select { 5057 font-size:.75rem; 5058 padding:.35rem .5rem; 5059 border:1px solid #cbd5e1; 5060 border-radius:6px; 5061 background:#fff; 5062 outline:none; 5063} 5064 5065.pa-empty { 5066 border:1px dashed #cbd5e1; 5067 padding:3rem 1rem; 5068 text-align:center; 5069 border-radius:12px; 5070 background:#fff; 5071} 5072.pa-empty p { 5073 margin:0 0 1rem; 5074 font-size:.9rem; 5075 color:#475569; 5076} 5077.btn-reset { 5078 display:inline-block; 5079 background:#0d6efd; 5080 color:#fff; 5081 font-size:.7rem; 5082 padding:.55rem 1rem; 5083 border-radius:8px; 5084 text-decoration:none; 5085 font-weight:600; 5086 letter-spacing:.4px; 5087 transition:.25s; 5088} 5089.btn-reset:hover { background:#0a58c7; } 5090 5091/* Grid */ 5092.pa-grid { 5093 display:grid; 5094 gap:1rem; 5095 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5096 align-items:stretch; 5097} 5098 5099.pa-card { 5100 background:#fff; 5101 border:1px solid #e2e8f0; 5102 border-radius:12px; 5103 display:flex; 5104 flex-direction:column; 5105 text-decoration:none; 5106 color:inherit; 5107 position:relative; 5108 overflow:hidden; 5109 transition:.28s cubic-bezier(.4,0,.2,1); 5110 min-width:0; 5111 padding: 0; 5112} 5113.pa-card:hover { 5114 transform:translateY(-4px); 5115 box-shadow:0 8px 22px -8px rgba(0,0,0,.15); 5116 border-color:#dce2ea; 5117} 5118 5119.pa-card .thumb { 5120 aspect-ratio:1/1; 5121 background:#f1f5f9; 5122 display:flex; 5123 align-items:center; 5124 justify-content:center; 5125 overflow:hidden; 5126 position:relative; 5127} 5128.pa-card img { 5129 width:100%; 5130 height:100%; 5131 object-fit:cover; 5132 display:block; 5133} 5134.badge-disc { 5135 position:absolute; 5136 top:8px; 5137 left:8px; 5138 background:#dc2626; 5139 color:#fff; 5140 font-size:.6rem; 5141 padding:4px 6px 3px; 5142 border-radius:6px; 5143 font-weight:700; 5144 letter-spacing:.4px; 5145} 5146 5147.pa-card .body { 5148 padding:.65rem .7rem .8rem; 5149 display:flex; 5150 flex-direction:column; 5151 gap:.45rem; 5152 min-height:90px; 5153} 5154 5155.pa-card .title { 5156 font-size:.75rem; 5157 line-height:1.25; 5158 margin:0; 5159 font-weight:600; 5160 color:#1e293b; 5161 height:2.4em; 5162 overflow:hidden; 5163} 5164 5165.pa-card .prices { 5166 display:flex; 5167 flex-wrap:wrap; 5168 align-items:baseline; 5169 gap:.4rem; 5170} 5171.pa-card .prices .final { 5172 font-size:.83rem; 5173 font-weight:700; 5174 color:#0d6efd; 5175} 5176.pa-card .prices .ori { 5177 font-size:.63rem; 5178 text-decoration:line-through; 5179 color:#94a3b8; 5180 font-weight:500; 5181} 5182 5183.pa-card .cat { 5184 margin-top:auto; 5185 font-size:.55rem; 5186 letter-spacing:.6px; 5187 text-transform:uppercase; 5188 background:#f1f5f9; 5189 border:1px solid #e2e8f0; 5190 padding:.28rem .45rem .25rem; 5191 border-radius:20px; 5192 font-weight:600; 5193 color:#475569; 5194 align-self:flex-start; 5195} 5196.pa-card .meta-line { 5197 margin-top:4px; 5198 font-size:.65rem; 5199 color:#475569; 5200} 5201.pa-card .meta-line .sold { 5202 display:inline-block; 5203 background:#f1f5f9; 5204 padding:2px 6px 3px; 5205 border-radius:4px; 5206 font-weight:600; 5207 letter-spacing:.3px; 5208 color:#334155; 5209} 5210 5211/* Pagination */ 5212.pa-pagination { 5213 margin-top:2rem; 5214 display:flex; 5215 flex-wrap:wrap; 5216 gap:.4rem; 5217 justify-content:center; 5218 font-size:.7rem; 5219 align-items:center; 5220} 5221.pa-pagination a, 5222.pa-pagination span { 5223 min-width:34px; 5224 height:34px; 5225 display:flex; 5226 align-items:center; 5227 justify-content:center; 5228 border-radius:8px; 5229 text-decoration:none; 5230 font-weight:600; 5231 padding:0 .6rem; 5232 box-sizing:border-box; 5233 border:1px solid #e2e8f0; 5234 background:#fff; 5235 color:#334155; 5236 transition:.25s; 5237} 5238.pa-pagination a:hover { 5239 background:#f1f5f9; 5240 color:#0f172a; 5241} 5242.pa-pagination .current { 5243 background:#0d6efd; 5244 color:#fff; 5245 border-color:#0d6efd; 5246} 5247.pa-pagination .gap { 5248 border:none; 5249 background:transparent; 5250 font-weight:400; 5251 color:#94a3b8; 5252} 5253 5254/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5255.pa-mobile-filterbar{display:none} 5256@media (max-width: 640px){ 5257 /* sembunyikan tombol lama */ 5258 .pa-btn-filter{display:none} 5259 5260 .pa-mobile-filterbar{ 5261 display:block; 5262 position:sticky; 5263 top:0; /* sesuaikan jika ada header fixed di atas */ 5264 z-index:5; 5265 background:#fff; 5266 border-bottom:1px solid #e2e8f0; 5267 padding:.4rem .5rem; 5268 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5269 } 5270 .pa-mobile-filterbar .mf-scroll{ 5271 display:flex;align-items:center;gap:.5rem; 5272 overflow-x:auto; 5273 -webkit-overflow-scrolling:touch; 5274 scrollbar-width:none; 5275 } 5276 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5277 5278 .mf-chip{ 5279 flex:0 0 auto; 5280 background:#fff; 5281 color:#334155; 5282 text-decoration:none; 5283 border:1px solid #e2e8f0; 5284 border-radius:999px; 5285 padding:.44rem .7rem; 5286 font-size:.78rem; 5287 font-weight:600; 5288 letter-spacing:.2px; 5289 white-space:nowrap; 5290 }
5291 .mf-chip:active{transform:scale(.98)} 5292 .mf-chip.active{ 5293 background:#eaf2ff; 5294 border-color:#0d6efd; 5295 color:#0d6efd; 5296 } 5297 5298 .mf-filter-btn{ 5299 position:relative; 5300 flex:0 0 auto; 5301 width:38px;height:38px; 5302 border-radius:50%; 5303 border:1.5px solid #0d6efd; 5304 background:#fff; 5305 display:inline-flex;align-items:center;justify-content:center; 5306 color:#0d6efd; 5307 cursor:pointer; 5308 } 5309 .mf-filter-btn:active{transform:scale(.96)} 5310 .mf-filter-btn svg{width:18px;height:18px;display:block} 5311 .mf-filter-btn .mf-badge{ 5312 position:absolute;top:0;right:-4px; 5313 min-width:18px;height:18px;border-radius:9px; 5314 background:#ef4444;color:#fff; 5315 font-size:.65rem;font-weight:700; 5316 display:flex;align-items:center;justify-content:center; 5317 padding:0 4px;line-height:1; 5318 box-shadow:0 2px 6px -2px rgba(239,68,68,.6); 5319 } 5320} 5321 5322/* Responsive */ 5323@media (max-width: 640px) { 5324 .product-archive { padding: 0 1rem 3rem; } 5325 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5326 .pa-search input[type=text]{ min-width:140px; } 5327 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5328 .pa-right { display: none; } 5329 .pa-sort { width: 100%; justify-content:center; } 5330 .pa-mobile-filterbar { display: flex !important; } 5331} 5332 5333.pa-mobile-filterbar { 5334 display: none; 5335 align-items: center; 5336 gap: 8px; 5337 padding: 8px 0 8px 12px; 5338 background: #fff; 5339 width: 100vw; 5340} 5341.pa-mobile-filterbar .mf-scroll { 5342 display: flex; 5343 gap: 8px; 5344 overflow-x: auto; 5345 -webkit-overflow-scrolling: touch; 5346 scrollbar-width: none; /* Firefox */ 5347 flex: 1 1 auto; /* takes remaining width */ 5348} 5349.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5350.pa-mobile-filterbar .mf-chip { 5351 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5352 white-space: nowrap; 5353} 5354.pa-mobile-filterbar .mf-filter-btn { 5355 flex: 0 0 auto; /* do not shrink */ 5356 position: relative; /* keep in place, not inside scroller */ 5357 display: inline-flex; 5358 align-items: center; 5359 gap: 6px; 5360} 5361 5362/* Optional: ensure on small screens the button stays visible with min width */ 5363@media (max-width: 768px) { 5364 .pa-mobile-filterbar .mf-filter-btn { 5365 padding: 6px 10px; 5366 } 5367} 5368 5369/* Product Specification Chips */ 5370.pa-card .spec-chips { 5371 font-size: .6rem; 5372 color: #64748b; 5373 font-weight: 500; 5374 letter-spacing: .2px; 5375 margin-top: 2px; 5376 margin-bottom: 4px; 5377 line-height: 1.2; 5378 opacity: 0.9; 5379 max-width: 100%; 5380 overflow: hidden; 5381 text-overflow: ellipsis; 5382 white-space: nowrap; 5383} 5384 5385.pa-card .spec-chips .spec-divider { 5386 color: #94a3b8; 5387 font-weight: 400; 5388 margin: 0 1px; 5389} 5390</style> 5391 <style data-inlined="site-notice.css"> 5392/** 5393 * Site Notice Banner - Reusable Notification System 5394 * Dismissible top bar for announcements, holidays, maintenance, etc. 5395 */ 5396 5397.holiday-notice { 5398 position: fixed; 5399 top: 0; 5400 left: 0; 5401 right: 0; 5402 z-index: 9999; 5403 background: linear-gradient(135deg, #d32f2f 0%, #f57c00 100%); 5404 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5405 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 5406 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5407 transform-origin: top; 5408} 5409 5410.holiday-notice.hidden { 5411 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5412} 5413 5414@keyframes slideDown { 5415 from { 5416 transform: translateY(-100%); 5417 opacity: 0; 5418 } 5419 to { 5420 transform: translateY(0); 5421 opacity: 1; 5422 } 5423} 5424 5425@keyframes slideUp { 5426 from { 5427 transform: translateY(0); 5428 opacity: 1; 5429 } 5430 to { 5431 transform: translateY(-100%); 5432 opacity: 0; 5433 } 5434} 5435 5436.holiday-notice-container { 5437 max-width: 1400px; 5438 margin: 0 auto; 5439 padding: 12px 20px; 5440 display: flex; 5441 align-items: center; 5442 justify-content: space-between; 5443 gap: 16px; 5444} 5445 5446.holiday-notice-content { 5447 display: flex; 5448 align-items: center; 5449 gap: 12px; 5450 flex: 1; 5451} 5452 5453.holiday-notice-icon { 5454 font-size: 20px; 5455 line-height: 1; 5456 flex-shrink: 0; 5457} 5458 5459.holiday-notice-text { 5460 color: #ffffff; 5461 font-size: 14px; 5462 font-weight: 500; 5463 line-height: 1.5; 5464 margin: 0; 5465 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 5466} 5467 5468.holiday-notice-emoji { 5469 display: inline-block; 5470 animation: bounce 2s ease-in-out infinite; 5471} 5472 5473@keyframes bounce { 5474 0%, 100% { 5475 transform: translateY(0); 5476 } 5477 50% { 5478 transform: translateY(-4px); 5479 } 5480} 5481 5482.holiday-notice-close { 5483 background: rgba(255, 255, 255, 0.2); 5484 border: 1px solid rgba(255, 255, 255, 0.3); 5485 border-radius: 6px; 5486 padding: 6px; 5487 cursor: pointer; 5488 color: #ffffff; 5489 transition: all 0.2s ease; 5490 flex-shrink: 0; 5491 display: flex; 5492 align-items: center; 5493 justify-content: center; 5494 width: 32px; 5495 height: 32px; 5496} 5497 5498.holiday-notice-close:hover { 5499 background: rgba(255, 255, 255, 0.3); 5500 transform: scale(1.05); 5501} 5502 5503.holiday-notice-close:active { 5504 transform: scale(0.95); 5505} 5506 5507.holiday-notice-close:focus { 5508 outline: 2px solid rgba(255, 255, 255, 0.5); 5509 outline-offset: 2px; 5510} 5511 5512/* Body padding adjustment when banner is visible */ 5513body.has-holiday-notice { 5514 padding-top: 52px; 5515} 5516 5517/* Tablet Styles */ 5518@media (max-width: 1024px) { 5519 .holiday-notice-container { 5520 padding: 10px 16px; 5521 } 5522 5523 .holiday-notice-text { 5524 font-size: 13px; 5525 } 5526 5527 .holiday-notice-icon { 5528 font-size: 18px; 5529 } 5530 5531 body.has-holiday-notice { 5532 padding-top: 48px; 5533 } 5534} 5535 5536/* Mobile Styles */ 5537@media (max-width: 640px) { 5538 .holiday-notice-container { 5539 padding: 10px 12px; 5540 gap: 8px; 5541 } 5542 5543 .holiday-notice-content { 5544 gap: 8px; 5545 } 5546 5547 .holiday-notice-text { 5548 font-size: 12px; 5549 line-height: 1.4; 5550 } 5551 5552 .holiday-notice-icon { 5553 font-size: 16px; 5554 } 5555 5556 .holiday-notice-close { 5557 width: 28px; 5558 height: 28px; 5559 } 5560 5561 .holiday-notice-close svg { 5562 width: 16px; 5563 height: 16px; 5564 } 5565 5566 body.has-holiday-notice { 5567 padding-top: 44px; 5568 } 5569} 5570 5571/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5572/* .header, 5573.navbar, 5574header { 5575 position: relative; 5576 z-index: 100; 5577} */ 5578 5579</style> 5580 <style data-inlined="benchmark.css"> 5581/** 5582 * Benchmark Section Styles 5583 * For product detail page benchmark display 5584 */ 5585 5586.pd-benchmark-section { 5587 background: #fff; 5588 border-radius: 12px; 5589 padding: 24px; 5590 margin: 32px 0; 5591 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5592 border: 1px solid #e5e7eb; 5593} 5594 5595.benchmark-header { 5596 margin-bottom: 20px; 5597} 5598 5599.benchmark-title { 5600 font-size: 22px; 5601 font-weight: 700; 5602 color: #1a1a1a; 5603 margin: 0 0 12px 0; 5604 display: flex; 5605 align-items: center; 5606 gap: 8px; 5607} 5608 5609.benchmark-hint { 5610 font-size: 14px; 5611 color: #6b7280; 5612 margin: 0; 5613} 5614 5615/* Preview Badges */ 5616.benchmark-preview-badges { 5617 display: flex; 5618 flex-wrap: wrap; 5619 gap: 10px; 5620 margin-top: 12px; 5621} 5622 5623.benchmark-badge { 5624 display: inline-flex; 5625 align-items: center; 5626 gap: 6px; 5627 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5628 color: #fff; 5629 padding: 8px 16px; 5630 border-radius: 20px; 5631 font-size: 13px; 5632 font-weight: 500; 5633 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5634 transition: all 0.2s ease; 5635} 5636 5637.benchmark-badge:hover { 5638 transform: translateY(-2px); 5639 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5640} 5641 5642.benchmark-badge strong { 5643 font-weight: 600; 5644 opacity: 0.9; 5645} 5646
5647/* Load Button */ 5648.btn-load-benchmark { 5649 display: flex; 5650 align-items: center; 5651 justify-content: center; 5652 gap: 10px; 5653 width: 100%; 5654 max-width: 420px; 5655 padding: 14px 24px; 5656 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5657 color: #fff; 5658 border: none; 5659 border-radius: 8px; 5660 font-size: 15px; 5661 font-weight: 600; 5662 cursor: pointer; 5663 transition: all 0.3s ease; 5664 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5665 margin: 0 auto; 5666} 5667 5668.btn-load-benchmark:hover:not(:disabled) { 5669 transform: translateY(-2px); 5670 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5671} 5672 5673.btn-load-benchmark:disabled { 5674 opacity: 0.6; 5675 cursor: not-allowed; 5676} 5677 5678.btn-load-benchmark .btn-icon { 5679 font-size: 20px; 5680} 5681 5682.btn-load-benchmark.loading .btn-icon { 5683 animation: spin 1s linear infinite; 5684} 5685 5686@keyframes spin { 5687 from { transform: rotate(0deg); } 5688 to { transform: rotate(360deg); } 5689} 5690 5691/* Benchmark Content */ 5692.benchmark-content { 5693 margin-top: 24px; 5694 padding-top: 24px; 5695 border-top: 2px solid #f3f4f6; 5696} 5697 5698.benchmark-loading { 5699 text-align: center; 5700 padding: 40px 20px; 5701 color: #6b7280; 5702} 5703 5704.benchmark-loading-spinner { 5705 width: 48px; 5706 height: 48px; 5707 margin: 0 auto 16px; 5708 border: 4px solid #f3f4f6; 5709 border-top-color: #667eea; 5710 border-radius: 50%; 5711 animation: spin 1s linear infinite; 5712} 5713 5714.benchmark-error { 5715 text-align: center; 5716 padding: 32px 20px; 5717 color: #ef4444; 5718 background: #fee2e2; 5719 border-radius: 8px; 5720} 5721 5722/* Benchmark Data Display */ 5723.benchmark-data-grid { 5724 display: grid; 5725 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5726 gap: 16px; 5727 margin-bottom: 24px; 5728} 5729 5730.benchmark-card { 5731 background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); 5732 border: 1px solid #e5e7eb; 5733 border-radius: 10px; 5734 padding: 18px; 5735 text-align: center; 5736 transition: all 0.2s ease; 5737} 5738 5739.benchmark-card:hover { 5740 transform: translateY(-3px); 5741 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5742} 5743 5744.benchmark-card-label { 5745 font-size: 13px; 5746 color: #6b7280; 5747 font-weight: 500; 5748 margin-bottom: 8px; 5749 text-transform: uppercase; 5750 letter-spacing: 0.5px; 5751} 5752 5753.benchmark-card-value { 5754 font-size: 24px; 5755 font-weight: 700; 5756 color: #1a1a1a; 5757 line-height: 1.2; 5758} 5759 5760.benchmark-card-value.large { 5761 font-size: 28px; 5762} 5763 5764.benchmark-card-subtitle { 5765 font-size: 12px; 5766 color: #9ca3af; 5767 margin-top: 4px; 5768} 5769 5770/* Spec Details */ 5771.benchmark-specs { 5772 background: #f9fafb; 5773 border-radius: 10px; 5774 padding: 20px; 5775 margin-top: 20px; 5776} 5777 5778.benchmark-specs-title { 5779 font-size: 16px; 5780 font-weight: 600; 5781 color: #1a1a1a; 5782 margin: 0 0 16px 0; 5783} 5784 5785.benchmark-spec-row { 5786 display: flex; 5787 justify-content: space-between; 5788 padding: 10px 0; 5789 border-bottom: 1px solid #e5e7eb; 5790} 5791 5792.benchmark-spec-row:last-child { 5793 border-bottom: none; 5794} 5795 5796.benchmark-spec-label { 5797 font-size: 14px; 5798 color: #6b7280; 5799 font-weight: 500; 5800} 5801 5802.benchmark-spec-value { 5803 font-size: 14px; 5804 color: #1a1a1a; 5805 font-weight: 600; 5806 text-align: right; 5807} 5808 5809/* Score Badges */ 5810.benchmark-score-badge { 5811 display: inline-block; 5812 padding: 6px 14px; 5813 border-radius: 20px; 5814 font-size: 13px; 5815 font-weight: 600; 5816 margin-top: 8px; 5817} 5818 5819.benchmark-score-badge.excellent { 5820 background: #10b981; 5821 color: #fff; 5822} 5823 5824.benchmark-score-badge.good { 5825 background: #3b82f6; 5826 color: #fff; 5827} 5828 5829.benchmark-score-badge.average { 5830 background: #f59e0b; 5831 color: #fff; 5832} 5833 5834.benchmark-score-badge.poor { 5835 background: #ef4444; 5836 color: #fff; 5837} 5838 5839/* Attribution */ 5840.benchmark-attribution { 5841 text-align: center; 5842 margin-top: 24px; 5843 padding-top: 20px; 5844 border-top: 1px solid #e5e7eb; 5845 font-size: 13px; 5846 color: #9ca3af; 5847} 5848 5849.benchmark-attribution a { 5850 color: #667eea; 5851 text-decoration: none; 5852 font-weight: 500; 5853} 5854 5855.benchmark-attribution a:hover { 5856 text-decoration: underline; 5857} 5858 5859/* Responsive */ 5860@media (max-width: 768px) { 5861 .pd-benchmark-section { 5862 padding: 16px; 5863 margin: 20px 0; 5864 } 5865 5866 .benchmark-title { 5867 font-size: 18px; 5868 } 5869 5870 .benchmark-preview-badges { 5871 flex-direction: column; 5872 gap: 8px; 5873 } 5874 5875 .benchmark-badge { 5876 justify-content: center; 5877 } 5878 5879 .btn-load-benchmark { 5880 font-size: 14px; 5881 padding: 12px 20px; 5882 } 5883 5884 .benchmark-data-grid { 5885 grid-template-columns: 1fr 1fr; 5886 gap: 12px; 5887 } 5888 5889 .benchmark-card { 5890 padding: 14px; 5891 } 5892 5893 .benchmark-card-value { 5894 font-size: 20px; 5895 } 5896 5897 .benchmark-spec-row { 5898 flex-direction: column; 5899 gap: 4px; 5900 } 5901 5902 .benchmark-spec-value { 5903 text-align: left; 5904 } 5905} 5906 5907@media (max-width: 480px) { 5908 .benchmark-data-grid { 5909 grid-template-columns: 1fr; 5910 } 5911} 5912 5913</style> 5914 <style data-inlined="product-filters.css"> 5915/* ========================================================= 5916 Product Filters (Desktop + Mobile Drawer) 5917 Light Only + FIX: 5918 - Desktop: f-actions selalu tampil (static bottom) 5919 - Mobile: konten filter dapat discroll 5920 ========================================================= */ 5921
5922/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5923.product-archive.with-filters .pa-layout { 5924 display:flex; 5925 gap:24px; 5926 align-items:flex-start; 5927} 5928.pa-main { flex:1; min-width:0; } 5929 5930/* ========================================================= 5931 DESKTOP PANEL 5932 ========================================================= */ 5933.pa-filters { 5934 width:300px; 5935 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5936 border:1px solid #e2e8f0; 5937 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5938 border-radius:18px; 5939 position:sticky; 5940 top:80px; 5941 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5942 font-size:14px; 5943 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5944 backdrop-filter:blur(3px); 5945 -webkit-backdrop-filter:blur(3px); 5946 transition:box-shadow .3s, background .4s; 5947 display:flex; 5948 flex-direction:column; 5949 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5950} 5951.pa-filters::before { 5952 content:""; 5953 position:absolute; 5954 inset:0 auto 0 0; 5955 width:7px; 5956 border-radius:18px 0 0 18px; 5957 background:linear-gradient(180deg,#f59e0b 0%,#ffb53a 35%,#1d6ce0 100%); 5958} 5959.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5960 5961/* Scroll area (desktop/mode shared) */ 5962.filters-scroll { 5963 flex:1; 5964 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5965 overflow:auto; 5966 padding-right:6px; 5967 margin-right:-6px; /* kompensasi scrollbar */ 5968 scrollbar-width:thin; 5969 overscroll-behavior:contain; 5970} 5971.filters-scroll::-webkit-scrollbar { width:8px; } 5972.filters-scroll::-webkit-scrollbar-thumb { 5973 background:#cbd5e1; 5974 border-radius:20px; 5975 border:2px solid #f1f5f9; 5976} 5977.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5978 5979/* Heading */ 5980.pa-filters h2 { 5981 margin:0 0 14px; 5982 font-size:20px; 5983 font-weight:700; 5984 letter-spacing:.4px; 5985 line-height:1.15; 5986 background:linear-gradient(90deg,#0f172a,#1d6ce0 60%,#0f172a); 5987 -webkit-background-clip:text; 5988 color:transparent; 5989 background-size:200% 100%; 5990 animation:titleSheen 8s linear infinite; 5991} 5992@keyframes titleSheen { 5993 0%{background-position:0 0} 5994 50%{background-position:100% 0} 5995 100%{background-position:0 0} 5996} 5997 5998/* Form groups */ 5999.pa-filters .f-group { margin-bottom:18px; position:relative; } 6000.pa-filters label { 6001 display:block; 6002 font-weight:600; 6003 margin:0 0 6px; 6004 font-size:13px; 6005 color:#0f172a; 6006 letter-spacing:.3px; 6007} 6008 6009/* Inputs / selects */ 6010.pa-filters input[type=text], 6011.pa-filters input[type=number], 6012.pa-filters select { 6013 width:100%; 6014 padding:7px 10px; 6015 border:1px solid #cbd5e1; 6016 border-radius:8px; 6017 font-size:13px; 6018 min-height:34px; 6019 background:#ffffff; 6020 box-sizing:border-box; 6021 color:#0f172a; 6022 transition:border-color .25s, box-shadow .25s, background .25s; 6023 -webkit-appearance:none; 6024 -moz-appearance:none; 6025 appearance:none; 6026} 6027.pa-filters select { 6028 background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%230f172a' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); 6029 background-repeat:no-repeat; 6030 background-position:right 10px center; 6031 background-size:12px; 6032 padding-right:32px; 6033} 6034.pa-filters input[type=text]:hover, 6035.pa-filters input[type=number]:hover, 6036.pa-filters select:hover { border-color:#94b5ec; } 6037.pa-filters input[type=text]:focus, 6038.pa-filters input[type=number]:focus, 6039.pa-filters select:focus { 6040 outline:none; 6041 border-color:#1d6ce0; 6042 box-shadow:0 0 0 3px rgba(29,108,224,.25); 6043 background:#f8fbff; 6044} 6045.pa-filters .range { display:flex; gap:10px; } 6046.pa-filters .range input { flex:1; } 6047.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 6048 6049/* Spec blocks */ 6050.f-specs { margin-top:4px; } 6051.spec-attr { 6052 margin:0 0 14px; 6053 padding:12px 12px 10px; 6054 border:1px solid #e2e8f0; 6055 border-radius:14px; 6056 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 6057 position:relative; 6058 transition:border-color .3s, box-shadow .3s, background .4s; 6059} 6060.spec-attr.collapsed { padding-bottom:8px; } 6061.spec-attr.has-selected { 6062 border-color:#1d6ce0; 6063 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.35); 6064 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 6065} 6066.spec-attr-title { 6067 font-weight:600; 6068 margin:0 0 8px; 6069 font-size:13px; 6070 display:flex; 6071 justify-content:space-between; 6072 align-items:center; 6073 cursor:pointer;
6074 color:#0f172a; 6075 user-select:none; 6076} 6077.spec-attr-title .chevron { 6078 font-size:10px; 6079 transition:transform .25s, opacity .25s; 6080 opacity:.65; 6081} 6082.spec-attr.collapsed .spec-attr-title .chevron { 6083 transform:rotate(-90deg); 6084 opacity:.5; 6085} 6086 6087/* Spec options */ 6088.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6089.spec-attr-options label { 6090 display:inline-flex; 6091 align-items:center; 6092 gap:4px; 6093 font-weight:500; 6094 border:1px solid #d4dde7; 6095 padding:6px 9px; 6096 border-radius:30px; 6097 cursor:pointer; 6098 background:#f3f6fa; 6099 font-size:12px; 6100 line-height:1.1; 6101 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6102 position:relative; 6103} 6104.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6105.spec-attr-options input { 6106 margin:0; 6107 position:absolute; 6108 inset:0; 6109 opacity:0; 6110 cursor:pointer; 6111} 6112.spec-attr-options label:has(input:checked) { 6113 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6114 border-color:#1d6ce0; 6115 color:#ffffff; 6116 box-shadow:0 2px 6px -1px rgba(29,108,224,.55); 6117} 6118.spec-attr-options label:has(input:checked)::after { 6119 content:""; 6120 position:absolute; 6121 right:6px; 6122 top:6px; 6123 width:6px; 6124 height:6px; 6125 background:#fff; 6126 border-radius:50%; 6127 box-shadow:0 0 0 4px rgba(0,0,0,.25),0 0 0 7px rgba(29,108,224,.5); 6128 animation:pulseDot 1.9s ease-out infinite; 6129} 6130@keyframes pulseDot { 6131 0%{transform:scale(.8);opacity:.9} 6132 60%{transform:scale(1.4);opacity:.15} 6133 100%{transform:scale(.8);opacity:.9} 6134} 6135 6136/* Badges */ 6137.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6138.pa-filter-badge { 6139 display:inline-flex; 6140 gap:6px; 6141 align-items:center; 6142 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6143 border:1px solid #b4cff3; 6144 padding:5px 10px 5px 12px; 6145 border-radius:22px; 6146 margin:2px 4px 0 0; 6147 font-size:11px; 6148 font-weight:500; 6149 line-height:1; 6150 color:#0f172a; 6151 position:relative; 6152 overflow:hidden; 6153} 6154.pa-filter-badge::before { 6155 content:""; 6156 position:absolute; 6157 inset:0; 6158 background:linear-gradient(90deg,rgba(0,0,0,0),rgba(0,0,0,.35),rgba(0,0,0,0)); 6159 transform:translateX(-100%); 6160 animation:badgeSheen 6s linear infinite; 6161} 6162@keyframes badgeSheen { 6163 0%{transform:translateX(-100%)} 6164 55%{transform:translateX(120%)} 6165 100%{transform:translateX(120%)} 6166} 6167.pa-filter-badge a.clear { 6168 text-decoration:none; 6169 color:#c02828; 6170 font-weight:700; 6171 padding:0 3px; 6172 display:inline-flex; 6173 align-items:center; 6174 justify-content:center; 6175 border-radius:50%; 6176 line-height:1; 6177 transition:background .25s, color .25s; 6178} 6179.pa-filter-badge a.clear:hover { background:#ffeded; color:#a40000; } 6180.pa-btn-filter { width: 100%;} 6181 6182/* ========================================================= 6183 ACTIONS (DESKTOP) - STATIC BOTTOM 6184 ========================================================= */ 6185.f-actions { 6186 margin-top:10px; 6187 display:flex; 6188 gap:12px; 6189 flex-wrap:wrap; 6190 padding:16px 0 4px; 6191 border-top:1px solid #e2e8f0; 6192 background:linear-gradient(180deg,rgba(0,0,0,0) 0%,#f1f5f9 70%); 6193 flex-shrink:0; 6194} 6195.f-actions button { 6196 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6197 color:#fff; 6198 border:none; 6199 padding:10px 20px; 6200 border-radius:12px; 6201 cursor:pointer; 6202 font-size:14px; 6203 font-weight:600; 6204 letter-spacing:.3px; 6205 display:inline-flex; 6206 align-items:center; 6207 gap:6px; 6208 box-shadow:0 4px 14px -2px rgba(29,108,224,.45); 6209 transition:transform .25s, box-shadow .3s; 6210} 6211.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6212.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6213.f-actions .reset { 6214 font-size:12px; 6215 text-decoration:none; 6216 color:#d92d2d; 6217 align-self:center; 6218 font-weight:600; 6219 padding:4px 6px; 6220 border-radius:8px; 6221 transition:background .25s, color .25s; 6222} 6223.f-actions .reset:hover { background:#ffe6e6; color:#b80000; text-decoration:underline; } 6224.pa-filters .close-drawer { display: none; } 6225 6226/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6227.filter-form { 6228 flex: 1; 6229 min-height: 0; 6230 display: flex; 6231 flex-direction: column; 6232 overflow: hidden; 6233} 6234 6235/* ========================================================= 6236 FILTER TOGGLE (MOBILE TRIGGER) 6237 ========================================================= */ 6238.filter-toggle { 6239 display:none; 6240 margin:0 0 14px; 6241 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6242 color:#fff; 6243 border:none; 6244 padding:11px 18px; 6245 font-size:14px; 6246 font-weight:600; 6247 border-radius:14px; 6248 cursor:pointer; 6249 letter-spacing:.4px; 6250 box-shadow:0 4px 14px -2px rgba(29,108,224,.55); 6251 position:relative; 6252 z-index:100;
6253 transition:box-shadow .3s, transform .25s; 6254} 6255.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(29,108,224,.6); transform:translateY(-2px); } 6256.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6257.filter-toggle .count-badge { 6258 display:inline-block; 6259 background:linear-gradient(135deg,#f59e0b 0%,#ffb43d 55%,#f59e0b 100%); 6260 color:#1d2b44; 6261 font-size:11px; 6262 font-weight:700; 6263 padding:3px 7px 4px; 6264 border-radius:30px; 6265 margin-left:8px; 6266 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #1d6ce0; 6267} 6268 6269/* Overlay (mobile) */ 6270.pa-filters-overlay { display:none; } 6271 6272/* ========================================================= 6273 MOBILE DRAWER 6274 ========================================================= */ 6275@media (max-width:768px){ 6276 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6277 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6278 6279 .pa-filters-overlay { 6280 position:fixed; 6281 inset:0; 6282 background:rgba(0,20,45,.55); 6283 z-index:998; 6284 opacity:0; 6285 transition:opacity .35s; 6286 display:block; 6287 pointer-events:none; 6288 backdrop-filter:blur(2px); 6289 } 6290 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6291 6292 .pa-filters { 6293 position:fixed; 6294 top:auto; 6295 left:0; right:0; bottom:0; 6296 max-height:92vh; 6297 height:auto; 6298 width:100%; 6299 border-radius:30px 30px 0 0; 6300 border:1px solid #dbe4ef; 6301 padding:26px 22px 0; 6302 z-index:10000; /* di atas bottom-nav (9998) */ 6303 transform:translateY(102%); 6304 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6305 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6306 overflow:hidden; /* outer hidden, inner scroll */ 6307 } 6308 .pa-filters.active { transform:translateY(0); } 6309 6310 /* Drag handle indicator */ 6311 .pa-filters::after { 6312 content:""; 6313 display:block; 6314 position:absolute; 6315 top:10px; 6316 left:50%; 6317 transform:translateX(-50%); 6318 width:36px; 6319 height:4px; 6320 background:#cbd5e1; 6321 border-radius:4px; 6322 } 6323 body.filter-open { overflow:hidden; touch-action:none; } 6324 6325 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6326 .pa-filters input[type=text], 6327 .pa-filters input[type=number], 6328 .pa-filters select { 6329 font-size:16px; 6330 min-height:44px; 6331 padding:10px 12px; 6332 border-radius:10px; 6333 } 6334 .pa-filters select { padding-right:32px; } 6335 6336 .filters-scroll { 6337 flex:1; 6338 overflow:auto; 6339 padding:0 0 120px; /* ruang di atas bar actions */ 6340 margin:0; 6341 -webkit-overflow-scrolling:touch; 6342 } 6343 6344 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6345 6346 .pa-filters .close-drawer { 6347 display: block; 6348 position:absolute; 6349 right:14px; top:12px; 6350 background:linear-gradient(135deg,#dc2626 0%,#b91c1c 100%); 6351 color:#fff; 6352 padding:7px 14px; 6353 border:none; 6354 border-radius:10px; 6355 font-size:12px; 6356 font-weight:600; 6357 cursor:pointer; 6358 box-shadow:0 3px 10px -2px rgba(220,38,38,.55); 6359 transition:transform .25s, box-shadow .3s; 6360 z-index: 99; 6361 } 6362 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(220,38,38,.6); } 6363 6364 .spec-attr { border-radius:18px; } 6365 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6366 6367 /* Actions fixed di viewport bawah */ 6368 .f-actions { 6369 position:fixed; 6370 left:0; right:0; bottom:0; 6371 margin:0; 6372 border-top:1px solid #d6dee8; 6373 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6374 background:#ffffffea; 6375 backdrop-filter:blur(6px); 6376 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6377 justify-content:space-between; 6378 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6379 } 6380 6381 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6382 body.filter-open .bottom-nav { 6383 display: none !important; 6384 } 6385 .f-actions button { 6386 flex:1 1 auto; 6387 justify-content:center; 6388 padding:14px 14px; 6389 font-size:15px; 6390 border-radius:14px; 6391 } 6392 .f-actions .reset { 6393 order:-1; 6394 font-size:12px; 6395 padding:2px 8px; 6396 } 6397} 6398 6399/* Scrollbar (outer panel desktop only kept for compatibility) */ 6400.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6401 6402/* Utilities */ 6403.hidden { display:none !important; } 6404.gradient-text { 6405 background:linear-gradient(90deg,#1d6ce0,#155ab8,#1d6ce0); 6406 -webkit-background-clip:text; 6407 color:transparent; 6408 animation:titleSheen 10s linear infinite; 6409} 6410.glow-focus:focus {
6411 outline:none !important; 6412 box-shadow:0 0 0 3px rgba(29,108,224,.4) !important; 6413 border-color:#1d6ce0 !important; 6414} 6415 6416/* Simple fade (opsional) */ 6417@keyframes fadeIn { 6418 from { opacity:0; transform:translateY(8px); } 6419 to { opacity:1; transform:translateY(0); } 6420} 6421</style> 6422 <style data-inlined="mega-menu-tiles.css"> 6423/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6424 6425/* Design Tokens */ 6426:root { 6427 --mm2-bg: #ffffff; 6428 --mm2-surface: #ffffff; 6429 --mm2-surface-elevated: rgba(0,0,0,0.98); 6430 --mm2-muted: #f8fafc; 6431 --mm2-border: #e2e8f0; 6432 --mm2-border-strong: #cbd5e1; 6433 --mm2-accent: #821603; 6434 --mm2-accent-hover: #4f46e5; 6435 --mm2-accent-10: rgba(99,102,241,.10); 6436 --mm2-accent-20: rgba(99,102,241,.20); 6437 --mm2-fg: #0f172a; 6438 --mm2-fg-muted: #64748b; 6439 --mm2-fg-light: #94a3b8; 6440 6441 /* Enhanced Shadows - Multi-layer elevation */ 6442 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6443 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6444 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6445 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6446 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6447 6448 /* Border Radius */ 6449 --mm2-radius-xl: 20px; 6450 --mm2-radius-lg: 16px; 6451 --mm2-radius-md: 12px; 6452 --mm2-radius-sm: 10px; 6453 6454 --mm2-z: 60; 6455 6456 /* Animation Timing */ 6457 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6458 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6459 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6460} 6461 6462.navbar { position: relative; z-index: var(--mm2-z); } 6463 6464/* Trigger Button - Enhanced */ 6465.mm2-trigger { 6466 display: inline-flex; align-items: center; gap: 8px; 6467 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6468 border: 1.5px solid var(--mm2-border); 6469 background: var(--mm2-surface); color: var(--mm2-fg); 6470 cursor: pointer; user-select: none; 6471 transition: all var(--mm2-transition-fast); 6472 text-decoration: none; 6473 font-weight: 600; 6474 box-shadow: var(--mm2-shadow-sm); 6475} 6476.mm2-trigger:hover { 6477 border-color: var(--mm2-accent); 6478 background: var(--mm2-muted); 6479 box-shadow: var(--mm2-shadow-md); 6480 transform: translateY(-1px); 6481} 6482.mm2-trigger:active { transform: translateY(0); } 6483.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6484.mm2-trigger:hover svg { transform: scale(1.05); } 6485 6486/* Backdrop - Enhanced Glassmorphism */ 6487.mm2-backdrop { 6488 position: fixed; 6489 inset: 0; 6490 top: 0; 6491 left: 0; 6492 right: 0; 6493 bottom: 0; 6494 background: rgba(15,23,42,.40); 6495 backdrop-filter: blur(8px) saturate(1.2); 6496 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6497 opacity: 0; 6498 pointer-events: none; 6499 z-index: 499; 6500 transition: opacity var(--mm2-transition-base); 6501} 6502.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6503 6504/* Panel - Glassmorphism Enhanced */ 6505.mm2-panel { 6506 position: fixed; 6507 left: 50%; 6508 top: 120px; 6509 transform: translateX(-50%) translateY(-16px); 6510 width: min(1200px, 95vw); 6511 background: var(--mm2-surface-elevated); 6512 backdrop-filter: blur(20px) saturate(1.8); 6513 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6514 border: 1.5px solid var(--mm2-border-strong); 6515 border-radius: var(--mm2-radius-xl); 6516 box-shadow: var(--mm2-shadow-xl); 6517 display: none; 6518 overflow: hidden; 6519 opacity: 0; 6520 z-index: 500; 6521 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6522} 6523.mm2-open .mm2-panel { 6524 display: block; 6525 opacity: 1; 6526 transform: translateX(-50%) translateY(0); 6527} 6528 6529/* View switcher */ 6530.mm2-view { display: none; } 6531.mm2-open .mm2-view.is-active { display: block; } 6532 6533/* Top grid (level-1 tiles) - Enhanced */ 6534.mm2-top { 6535 padding: 24px; 6536 max-height: 75vh; overflow: auto; 6537} 6538.mm2-grid { 6539 display: grid; 6540 grid-template-columns: repeat(4, minmax(0,1fr)); 6541 gap: 14px; 6542} 6543@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6544@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6545 6546/* Stagger animation for cards */ 6547.mm2-open .mm2-card { 6548 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6549} 6550.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6551.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6552.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6553.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6554.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6555.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6556.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6557.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6558.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6559.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6560.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6561.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6562.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6563 6564@keyframes slideInUp { 6565 from { 6566 opacity: 0; 6567 transform: translateY(12px); 6568 } 6569 to { 6570 opacity: 1; 6571 transform: translateY(0); 6572 } 6573} 6574 6575.mm2-card { 6576 position: relative; 6577 display: flex; gap: 14px; align-items: center; 6578 padding: 16px; border-radius: var(--mm2-radius-md); 6579 background: var(--mm2-surface); 6580 border: 1.5px solid var(--mm2-border); 6581 text-decoration: none; color: var(--mm2-fg); 6582 transition: all var(--mm2-transition-fast); 6583 min-height: 72px; 6584 box-shadow: var(--mm2-shadow-sm); 6585} 6586.mm2-card:hover { 6587 border-color: var(--mm2-accent); 6588 background: linear-gradient(135deg, rgba(0,0,0,1), rgba(248,250,252,1)); 6589 box-shadow: var(--mm2-shadow-hover); 6590 transform: translateY(-3px); 6591} 6592.mm2-card:active { transform: translateY(-1px); } 6593 6594/* Icon with category-specific gradients */ 6595.mm2-card .mm2-ico { 6596 width: 48px; height: 48px; border-radius: 12px; 6597 display: inline-flex; align-items: center; justify-content: center; 6598 background: linear-gradient(135deg, #e0e7ff, #fff); 6599 border: 1.5px solid #c7d2fe; 6600 color: #4f46e5; 6601 flex-shrink: 0; 6602 transition: all var(--mm2-transition-fast); 6603 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6604} 6605.mm2-card:hover .mm2-ico { 6606 transform: scale(1.08) rotate(2deg); 6607 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6608} 6609.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6610.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6611 6612.mm2-card .mm2-title { 6613 font-weight: 700; 6614 font-size: 15px; 6615 letter-spacing: -0.01em; 6616 line-height: 1.3; 6617} 6618.mm2-card .mm2-sub { 6619 color: var(--mm2-fg-muted); 6620 font-size: 12.5px; 6621 margin-top: 3px; 6622 display: flex; 6623 align-items: center; 6624 gap: 4px; 6625 white-space: nowrap; 6626 overflow: hidden; 6627 text-overflow: ellipsis; 6628} 6629.mm2-card .mm2-count { 6630 display: inline-flex; 6631 padding: 2px 8px; 6632 border-radius: 6px; 6633 background: var(--mm2-accent-10); 6634 color: var(--mm2-accent); 6635 font-weight: 600; 6636 font-size: 11px; 6637 margin-left: 4px; 6638} 6639 6640/* Detail view (level-2 + level-3) - Enhanced */ 6641.mm2-detail { 6642 position: relative; 6643 padding: 16px 24px 24px; 6644 max-height: 70vh; overflow: auto; 6645} 6646.mm2-detail-header { 6647 display: flex; align-items: center; gap: 14px; 6648 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6649} 6650.mm2-back { 6651 display: inline-flex; align-items: center; justify-content: center; 6652 width: 40px; height: 40px; border-radius: 12px; 6653 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6654 cursor: pointer; 6655 transition: all var(--mm2-transition-fast); 6656 box-shadow: var(--mm2-shadow-sm); 6657} 6658.mm2-back:hover { 6659 border-color: var(--mm2-accent); 6660 background: var(--mm2-muted); 6661 transform: translateX(-3px); 6662 box-shadow: var(--mm2-shadow-md); 6663} 6664.mm2-detail-title { 6665 font-weight: 800; 6666 font-size: 18px; 6667 letter-spacing: -0.02em; 6668} 6669 6670/* Two-pane layout (desktop) - Enhanced */ 6671.mm2-detail-layout { 6672 display: grid; 6673 grid-template-columns: 280px 1fr; 6674 gap: 20px; 6675} 6676
6677/* Left: subnav (level-2 list) - Enhanced */ 6678.mm2-subnav { 6679 position: sticky; top: 8px; 6680 align-self: start; 6681 max-height: calc(70vh - 56px); 6682 overflow: auto; 6683 padding-right: 6px; 6684} 6685.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6686.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6687.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6688.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6689 6690.mm2-subitem { 6691 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6692 width: 100%; 6693 padding: 12px 14px; 6694 border-radius: 12px; 6695 border: 1.5px solid transparent; 6696 background: transparent; 6697 color: var(--mm2-fg); 6698 cursor: pointer; 6699 text-align: left; 6700 transition: all var(--mm2-transition-fast); 6701 margin-bottom: 6px; 6702 font-weight: 600; 6703 font-size: 14px; 6704 position: relative; 6705} 6706.mm2-subitem::before { 6707 content: ''; 6708 position: absolute; 6709 left: 0; 6710 top: 50%; 6711 transform: translateY(-50%); 6712 width: 3px; 6713 height: 0; 6714 background: var(--mm2-accent); 6715 border-radius: 0 2px 2px 0; 6716 transition: height var(--mm2-transition-fast); 6717} 6718.mm2-subitem:hover { 6719 background: var(--mm2-muted); 6720 color: var(--mm2-accent-hover); 6721} 6722.mm2-subitem.active { 6723 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6724 border-color: var(--mm2-accent-20); 6725 color: var(--mm2-accent-hover); 6726 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6727} 6728.mm2-subitem.active::before { 6729 height: 24px; 6730} 6731 6732/* Right: subcontent (level-3 grid) - Enhanced */ 6733.mm2-subcontent { min-height: 240px; } 6734.mm2-links { 6735 display: grid; gap: 12px 14px; 6736 grid-template-columns: repeat(3, minmax(0,1fr)); 6737} 6738@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6739 6740.mm2-link { 6741 display: flex; align-items: center; gap: 12px; 6742 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6743 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6744 min-height: 52px; 6745 font-weight: 500; 6746 font-size: 14px; 6747 transition: all var(--mm2-transition-fast); 6748 box-shadow: var(--mm2-shadow-sm); 6749 position: relative; 6750 justify-content: space-between; 6751} 6752.mm2-link:hover { 6753 background: var(--mm2-muted); 6754 border-color: var(--mm2-accent); 6755 transform: translateX(4px); 6756 box-shadow: var(--mm2-shadow-md); 6757} 6758.mm2-link-text { 6759 flex: 1; 6760} 6761 6762/* Product Thumbnails */ 6763.mm2-thumbs { 6764 display: flex; 6765 gap: 6px; 6766 align-items: center; 6767 margin-left: auto; 6768 flex-shrink: 0; 6769} 6770.mm2-thumb { 6771 width: 36px; 6772 height: 36px; 6773 border-radius: 8px; 6774 object-fit: cover; 6775 border: 1.5px solid var(--mm2-border); 6776 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6777 transition: all var(--mm2-transition-fast); 6778 background: var(--mm2-muted); 6779} 6780.mm2-link:hover .mm2-thumb { 6781 border-color: var(--mm2-accent); 6782 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6783 transform: scale(1.08); 6784} 6785 6786/* Thumbnail Skeleton Loader */ 6787.mm2-thumb-skeleton { 6788 width: 36px; 6789 height: 36px; 6790 border-radius: 8px; 6791 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6792 background-size: 200% 100%; 6793 animation: shimmer 1.5s infinite; 6794 border: 1.5px solid var(--mm2-border); 6795} 6796 6797.mm2-badge { 6798 font-size: 11px; padding: 3px 10px; margin-left: auto; 6799 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6800 font-weight: 700; 6801 border: 1px solid var(--mm2-accent-20); 6802} 6803 6804/* "See all" highlight card - Enhanced */ 6805.mm2-seeall { 6806 border: 2px dashed var(--mm2-accent); 6807 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6808 font-weight: 700; 6809} 6810.mm2-seeall:hover { 6811 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6812 border-style: solid; 6813 transform: translateX(6px) scale(1.02); 6814} 6815 6816/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6817@media (max-width: 960px) { 6818 .mm2-panel { 6819 position: fixed; 6820 left: 0; 6821 right: 0; 6822 bottom: 0; 6823 top: auto; 6824 width: 100%; 6825 max-height: 88vh; 6826 transform: translateX(0) translateY(100%); 6827 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6828 border-bottom: none; 6829 } 6830 .mm2-open .mm2-panel { 6831 transform: translateX(0) translateY(0);
6831 z-index: 9999; 6832 } 6833 6834 /* Enhanced backdrop for mobile */ 6835 .mm2-backdrop { 6836 backdrop-filter: blur(6px) saturate(1.2); 6837 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6838 } 6839 6840 /* Mobile close button - Soft glassmorphism */ 6841 .mm2-mobile-close { 6842 position: absolute; 6843 top: 4px; 6844 right: 8px; 6845 width: 28px; 6846 height: 28px; 6847 border-radius: 50%; 6848 border: 1px solid rgba(226, 232, 240, 0.6); 6849 background: rgba(255, 255, 255, 0.85); 6850 backdrop-filter: blur(12px) saturate(1.8); 6851 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6852 color: var(--mm2-fg-muted); 6853 display: inline-flex; 6854 align-items: center; 6855 justify-content: center; 6856 cursor: pointer; 6857 transition: all var(--mm2-transition-fast); 6858 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6859 z-index: 20; 6860 } 6861 .mm2-mobile-close:hover { 6862 background: rgba(254, 242, 242, 0.95); 6863 border-color: rgba(252, 165, 165, 0.8); 6864 color: #dc2626; 6865 box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); 6866 } 6867 .mm2-mobile-close:active { 6868 transform: scale(0.92); 6869 } 6870 6871 /* Drag handle indicator */ 6872 .mm2-top::before, 6873 .mm2-detail::before { 6874 content: ''; 6875 position: sticky; 6876 top: 0; 6877 left: 50%; 6878 transform: translateX(-50%); 6879 width: 40px; 6880 height: 4px; 6881 background: var(--mm2-border-strong); 6882 border-radius: 999px; 6883 margin: 12px auto 20px; 6884 display: block; 6885 z-index: 10; 6886 transition: background var(--mm2-transition-fast); 6887 } 6888 .mm2-panel:active .mm2-top::before, 6889 .mm2-panel:active .mm2-detail::before { 6890 background: #94a3b8; 6891 } 6892 6893 .mm2-top { 6894 padding: 10px; 6895 } 6896 6897 .mm2-top, .mm2-detail { 6898 max-height: min(82vh, 720px); 6899 padding-top: 8px; 6900 } 6901 6902 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6903 .mm2-detail-layout { display: block; } 6904 .mm2-subnav { 6905 position: static; max-height: none; overflow: visible; 6906 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6907 grid-template-columns: repeat(2, minmax(0,1fr)); 6908 border-bottom: 2px solid var(--mm2-muted); 6909 } 6910 .mm2-subitem { 6911 margin: 0; width: 100%; text-align: center; 6912 border-radius: 14px; 6913 background: #fff; border: 1.5px solid var(--mm2-border); 6914 white-space: normal; line-height: 1.3; min-height: 48px; 6915 justify-content: center; 6916 padding: 12px 10px; 6917 font-size: 13.5px; 6918 box-shadow: var(--mm2-shadow-sm); 6919 } 6920 .mm2-subitem::before { display: none; } 6921 .mm2-subitem:hover { 6922 transform: scale(1.02); 6923 } 6924 .mm2-subitem.active { 6925 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6926 border-color: var(--mm2-accent-hover); 6927 color: white; 6928 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6929 font-weight: 700; 6930 } 6931 6932 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6933 .mm2-links { 6934 grid-template-columns: repeat(2, minmax(0,1fr)); 6935 gap: 10px; 6936 } 6937 6938 /* Mobile: hide thumbnails untuk save space */ 6939 .mm2-thumbs { 6940 display: none; 6941 } 6942 6943 /* Larger touch targets for mobile */ 6944 .mm2-card { 6945 min-height: 80px; 6946 padding: 18px 14px; 6947 } 6948 .mm2-card .mm2-ico { 6949 width: 50px; 6950 height: 50px; 6951 } 6952 .mm2-card .mm2-title { 6953 font-size: 14px; 6954 } 6955 .mm2-card .mm2-sub { 6956 font-size: 11.5px; 6957 } 6958 .mm2-link { 6959 min-height: 56px; 6960 padding: 16px; 6961 } 6962 .mm2-back { 6963 width: 44px; 6964 height: 44px; 6965 } 6966 6967 /* Grid spacing */ 6968 .mm2-grid { 6969 gap: 10px; 6970 } 6971} 6972</style> 6973 <style data-inlined="product-compare.css"> 6974/* Product Compare Feature Styles */ 6975 6976/* Compare Button in Product Detail - Modern Chip Style */ 6977.pd-compare-section { 6978 margin: 0.5rem 0; 6979 display: flex; 6980 align-items: center; 6981 gap: 0.5rem; 6982} 6983 6984.pd-compare-btn { 6985 display: inline-flex; 6986 align-items: center; 6987 gap: 0.4rem; 6988 padding: 0.5rem 0.85rem; 6989 border: 1px solid #cbd5e1; 6990 border-radius: 20px; 6991 background: #fff; 6992 color: #64748b; 6993 font-weight: 600; 6994 font-size: 0.8rem; 6995 cursor: pointer; 6996 transition: all 0.2s ease; 6997 position: relative; 6998 white-space: nowrap; 6999} 7000 7001.pd-compare-btn:hover { 7002 border-color: #3b82f6; 7003 color: #3b82f6; 7004 background: #f0f9ff; 7005 transform: translateY(-1px); 7006 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); 7007} 7008 7009.pd-compare-btn.added { 7010 border-color: #16a34a; 7011 color: #16a34a; 7012 background: #f0fdf4; 7013} 7014 7015.pd-compare-btn .compare-remove { 7016 display: none; 7017} 7018 7019.pd-compare-btn.added .compare-text { 7020 display: none; 7021} 7022 7023.pd-compare-btn.added .compare-remove { 7024 display: flex !important; 7025 align-items: center; 7026 gap: 0.25rem; 7027 font-weight: 600; 7028 font-size: 0.9rem; 7029 color: #dc2626 !important; 7030 opacity: 1 !important; 7031 visibility: visible !important; 7032 z-index: 1; 7033 position: relative; 7034} 7035 7036.pd-compare-btn.added .compare-remove[hidden] { 7037 display: flex !important; 7038} 7039 7040/* Force visibility for added state */ 7041.pd-compare-btn.added .compare-remove { 7042 display: flex !important; 7043} 7044 7045[hidden].compare-remove { 7046 display: none !important; 7047} 7048 7049.pd-compare-btn.added [hidden].compare-remove { 7050 display: flex !important; 7051} 7052 7053/* Ensure the added state text is always visible */ 7054.pd-compare-btn.added { 7055 border-color: #dc2626 !important; 7056 color: #dc2626 !important; 7057 background: #fef2f2 !important; 7058} 7059 7060.pd-compare-btn.added .compare-remove svg {
7061 color: #dc2626 !important; 7062} 7063 7064.pd-compare-btn.added:hover { 7065 border-color: #15803d; 7066 color: #15803d; 7067 background: #dcfce7; 7068 transform: translateY(-1px); 7069 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 7070} 7071 7072.pd-compare-btn .compare-icon { 7073 width: 16px; 7074 height: 16px; 7075 flex-shrink: 0; 7076} 7077 7078.pd-compare-info { 7079 display: inline-flex; 7080 align-items: center; 7081} 7082 7083.compare-note { 7084 color: #94a3b8; 7085 font-size: 0.7rem; 7086 font-weight: 500; 7087 transition: color 0.2s ease; 7088} 7089 7090.pd-compare-btn.added + .pd-compare-info .compare-note { 7091 color: #16a34a; 7092 font-weight: 600; 7093} 7094 7095/* Compare Badge in Navigation */ 7096.compare-indicator { 7097 position: relative; 7098} 7099 7100.compare-indicator .badge { 7101 position: absolute; 7102 top: -8px; 7103 right: -8px; 7104 min-width: 18px; 7105 height: 18px; 7106 padding: 0 5px; 7107 background: #ef4444; 7108 color: white; 7109 border-radius: 9px; 7110 font-size: 11px; 7111 7112 7113/* Floating Compare Badge (Sticky) */ 7114.compare-floating-badge { 7115 position: fixed; 7116 right: 16px; 7117 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7118 width: auto; 7119 min-width: 56px; 7120 height: 56px; 7121 padding: 0 16px; 7122 border-radius: 28px; 7123 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 7124 color: #fff; 7125 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.5); 7126 cursor: pointer; 7127 z-index: 1104; 7128 opacity: 0; 7129 transform: translateY(8px) scale(0.9); 7130 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7131 display: flex; 7132 align-items: center; 7133 justify-content: center; 7134 gap: 8px; 7135 font-weight: 600; 7136 font-size: 0.85rem; 7137 pointer-events: none; 7138} 7139 7140.compare-floating-badge.visible { 7141 opacity: 1; 7142 transform: translateY(0) scale(1); 7143 pointer-events: auto; 7144} 7145 7146.compare-floating-badge:hover { 7147 box-shadow: 0 12px 32px -10px rgba(59, 130, 246, 0.6); 7148 transform: translateY(-2px) scale(1.02); 7149} 7150 7151.compare-floating-badge:active { 7152 transform: translateY(0) scale(0.98); 7153} 7154 7155.compare-floating-badge .cfb-icon { 7156 width: 20px; 7157 height: 20px; 7158 flex-shrink: 0; 7159} 7160 7161.compare-floating-badge .cfb-count { 7162 font-size: 0.9rem; 7163 font-weight: 700; 7164 min-width: 20px; 7165 text-align: center; 7166 background: rgba(255, 255, 255, 0.25); 7167 padding: 2px 8px; 7168 border-radius: 12px; 7169 line-height: 1; 7170 display: flex; 7171 align-items: center; 7172 justify-content: center; 7173 height: 24px; 7174} 7175 7176.compare-floating-badge .cfb-text { 7177 display: none; 7178} 7179 7180@media (min-width: 641px) { 7181 .compare-floating-badge .cfb-text { 7182 display: block; 7183 white-space: nowrap; 7184 } 7185} 7186 7187@media (max-width: 640px) { 7188 .compare-floating-badge { 7189 right: 14px; 7190 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7191 min-width: 48px; 7192 height: 48px; 7193 padding: 0 12px; 7194 border-radius: 24px; 7195 } 7196 7197 .compare-floating-badge .cfb-icon { 7198 width: 18px; 7199 height: 18px; 7200 } 7201 7202 .compare-floating-badge .cfb-count { 7203 font-size: 0.8rem; 7204 padding: 2px 6px; 7205 height: 20px; 7206 } 7207} font-weight: 600; 7208 display: flex; 7209 align-items: center; 7210 justify-content: center; 7211 transform: scale(0); 7212 transition: transform 0.2s ease; 7213} 7214 7215.compare-indicator .badge.active { 7216 transform: scale(1); 7217} 7218 7219/* Compare Sidebar */ 7220.compare-sidebar { 7221 position: fixed; 7222 top: 0; 7223 right: -400px; 7224 width: 400px; 7225 height: 100vh; 7226 background: #ffffff; 7227 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7228 z-index: 9999; 7229 display: flex; 7230 flex-direction: column; 7231 transition: right 0.3s ease; 7232} 7233 7234.compare-sidebar.open { 7235 right: 0; 7236} 7237 7238.compare-overlay { 7239 position: fixed; 7240 top: 0; 7241 left: 0; 7242 width: 100%; 7243 height: 100%; 7244 background: rgba(0, 0, 0, 0.5); 7245 z-index: 9998; 7246 opacity: 0; 7247 visibility: hidden; 7248 transition: all 0.3s ease; 7249} 7250 7251.compare-overlay.active { 7252 opacity: 1; 7253 visibility: visible; 7254} 7255 7256.compare-header { 7257 display: flex; 7258 align-items: center; 7259 justify-content: space-between; 7260 padding: 1.25rem; 7261 border-bottom: 1px solid #e2e8f0; 7262 background: #f8fafc; 7263} 7264 7265.compare-title { 7266 display: flex; 7267 align-items: center; 7268 gap: 0.5rem; 7269 font-size: 1rem; 7270 font-weight: 600; 7271 color: #1e293b; 7272 margin: 0; 7273} 7274 7275.compare-count { 7276 color: #64748b; 7277 font-weight: 400; 7278} 7279 7280.compare-close { 7281 display: flex; 7282 align-items: center; 7283 justify-content: center; 7284 width: 32px; 7285 height: 32px; 7286 border: none; 7287 border-radius: 6px; 7288 background: transparent; 7289 color: #64748b; 7290 cursor: pointer; 7291 transition: all 0.2s ease; 7292} 7293 7294.compare-close:hover { 7295 background: #e2e8f0; 7296 color: #374151; 7297} 7298 7299.compare-content { 7300 flex: 1; 7301 overflow-y: auto; 7302 padding: 1rem; 7303} 7304 7305.compare-empty { 7306 display: flex; 7307 flex-direction: column; 7308 align-items: center; 7309 justify-content: center; 7310 height: 100%; 7311 text-align: center; 7312 padding: 2rem 1rem; 7313} 7314 7315.compare-empty h5 { 7316 margin: 1rem 0 0.5rem; 7317 color: #374151; 7318} 7319 7320.compare-empty p { 7321 color: #64748b; 7322 font-size: 0.875rem; 7323 line-height: 1.5; 7324} 7325 7326.compare-list { 7327 display: flex; 7328 flex-direction: column; 7329 gap: 0.75rem; 7330} 7331 7332.compare-item { 7333 display: flex; 7334 align-items: center; 7335 gap: 0.75rem; 7336 padding: 0.75rem; 7337 border: 1px solid #e2e8f0; 7338 border-radius: 8px; 7339 background: #ffffff; 7340 transition: all 0.2s ease; 7341} 7342 7343.compare-item:hover { 7344 border-color: #cbd5e1; 7345 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7346} 7347 7348.compare-item-image { 7349 flex-shrink: 0; 7350 width: 48px; 7351 height: 48px; 7352 border-radius: 6px; 7353 overflow: hidden; 7354 border: 1px solid #e2e8f0; 7355} 7356 7357.compare-item-image img { 7358 width: 100%; 7359 height: 100%; 7360 object-fit: cover; 7361} 7362 7363.compare-item-info { 7364 flex: 1; 7365 min-width: 0; 7366} 7367 7368.compare-item-name { 7369 font-weight: 500; 7370 font-size: 0.875rem; 7371 color: #1e293b; 7372 margin-bottom: 0.25rem; 7373 line-height: 1.3; 7374 display: -webkit-box; 7375 -webkit-line-clamp: 2; 7376 -webkit-box-orient: vertical; 7377 overflow: hidden; 7378} 7379 7380.compare-item-price { 7381 font-weight: 600; 7382 font-size: 0.8rem; 7383 color: #059669; 7384} 7385 7386.compare-item-remove { 7387 flex-shrink: 0; 7388 display: flex; 7389 align-items: center; 7390 justify-content: center; 7391 width: 28px; 7392 height: 28px; 7393 border: none; 7394 border-radius: 4px; 7395 background: transparent; 7396 color: #dc2626; 7397 cursor: pointer; 7398 transition: all 0.2s ease; 7399} 7400 7401.compare-item-remove:hover { 7402 background: #fef2f2; 7403 color: #991b1b; 7404} 7405 7406.compare-actions { 7407 padding: 1rem; 7408 border-top: 1px solid #e2e8f0; 7409 display: flex; 7410 gap: 0.75rem; 7411} 7412 7413.compare-actions .btn { 7414 flex: 1; 7415 padding: 0.75rem 1rem; 7416 border-radius: 6px; 7417 font-weight: 600; 7418 font-size: 0.875rem; 7419 display: flex; 7420 align-items: center; 7421 justify-content: center; 7422 gap: 0.5rem; 7423 cursor: pointer; 7424 transition: all 0.2s ease; 7425} 7426 7427.btn-outline-danger { 7428 border: 1px solid #dc2626; 7429 background: transparent; 7430 color: #dc2626; 7431} 7432 7433.btn-outline-danger:hover { 7434 background: #dc2626; 7435 color: white; 7436} 7437 7438.btn-primary { 7439 border: 1px solid #3b82f6; 7440 background: #3b82f6; 7441 color: white; 7442} 7443 7444.btn-primary:hover { 7445 background: #2563eb; 7446 border-color: #2563eb; 7447} 7448 7449.btn-primary:disabled { 7450 opacity: 0.5;
7451 cursor: not-allowed; 7452} 7453 7454.btn-primary:disabled:hover { 7455 background: #3b82f6; 7456 border-color: #3b82f6; 7457} 7458 7459/* AI Comparison Modal */ 7460.ai-compare-modal { 7461 position: fixed; 7462 top: 0; 7463 left: 0; 7464 width: 100%; 7465 height: 100%; 7466 z-index: 10000; 7467 display: none; 7468 align-items: center; 7469 justify-content: center; 7470} 7471 7472.ai-compare-modal.open { 7473 display: flex; 7474} 7475 7476.ai-compare-modal .modal-backdrop { 7477 position: absolute; 7478 top: 0; 7479 left: 0; 7480 width: 100%; 7481 height: 100%; 7482 background: rgba(0, 0, 0, 0.6); 7483 backdrop-filter: blur(4px); 7484} 7485 7486.modal-container { 7487 position: relative; 7488 width: 90%; 7489 max-width: 1000px; 7490 max-height: 90vh; 7491 background: #ffffff; 7492 border-radius: 12px; 7493 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7494 display: flex; 7495 flex-direction: column; 7496 overflow: hidden; 7497} 7498 7499.modal-header { 7500 display: flex; 7501 align-items: center; 7502 justify-content: space-between; 7503 padding: 1.5rem 2rem; 7504 border-bottom: 1px solid #e2e8f0; 7505 background: #f8fafc; 7506} 7507 7508.modal-title { 7509 display: flex; 7510 align-items: center; 7511 gap: 0.75rem; 7512 font-size: 1.25rem; 7513 font-weight: 600; 7514 color: #1e293b; 7515 margin: 0; 7516} 7517 7518.modal-close { 7519 display: flex; 7520 align-items: center; 7521 justify-content: center; 7522 width: 36px; 7523 height: 36px; 7524 border: none; 7525 border-radius: 8px; 7526 background: transparent; 7527 color: #64748b; 7528 cursor: pointer; 7529 transition: all 0.2s ease; 7530} 7531 7532.modal-close:hover { 7533 background: #e2e8f0; 7534 color: #374151; 7535} 7536 7537.modal-content { 7538 flex: 1; 7539 overflow-y: auto; 7540 padding: 2rem; 7541} 7542 7543/* AI Loading State */ 7544.ai-loading { 7545 display: flex; 7546 flex-direction: column; 7547 align-items: center; 7548 justify-content: center; 7549 padding: 3rem 2rem; 7550 text-align: center; 7551} 7552 7553.loading-animation { 7554 margin-bottom: 1rem; 7555} 7556 7557.typing-indicator { 7558 display: flex; 7559 align-items: center; 7560 justify-content: center; 7561 gap: 4px; 7562 margin-bottom: 1rem; 7563} 7564 7565.typing-indicator span { 7566 width: 8px; 7567 height: 8px; 7568 background: #3b82f6; 7569 border-radius: 50%; 7570 animation: typing 1.4s infinite ease-in-out; 7571} 7572 7573.typing-indicator span:nth-child(2) { 7574 animation-delay: 0.2s; 7575} 7576 7577.typing-indicator span:nth-child(3) { 7578 animation-delay: 0.4s; 7579} 7580 7581@keyframes typing { 7582 0%, 80%, 100% { 7583 opacity: 0.3; 7584 transform: scale(0.8); 7585 } 7586 40% { 7587 opacity: 1; 7588 transform: scale(1); 7589 } 7590} 7591 7592.ai-loading p { 7593 font-weight: 500; 7594 color: #1e293b; 7595 margin-bottom: 0.5rem; 7596} 7597 7598.ai-loading small { 7599 color: #64748b; 7600} 7601 7602/* AI Error State */ 7603.ai-error { 7604 display: flex; 7605 flex-direction: column; 7606 align-items: center; 7607 justify-content: center; 7608 padding: 3rem 2rem; 7609 text-align: center; 7610} 7611 7612.ai-error h4 { 7613 margin: 1rem 0 0.5rem; 7614 color: #dc2626; 7615} 7616 7617.ai-error p { 7618 color: #64748b; 7619 margin-bottom: 1.5rem; 7620} 7621 7622/* AI Results */ 7623.ai-results > div { 7624 margin-bottom: 2rem; 7625} 7626 7627.ai-summary-card, 7628.ai-comparison-table, 7629.ai-detailed-analysis, 7630.ai-recommendations, 7631.ai-tips { 7632 background: #f8fafc; 7633 border: 1px solid #e2e8f0; 7634 border-radius: 12px; 7635 padding: 1.5rem; 7636} 7637 7638.ai-results h4 { 7639 display: flex; 7640 align-items: center; 7641 gap: 0.5rem; 7642 font-size: 1.1rem; 7643 font-weight: 600; 7644 color: #1e293b; 7645 margin-bottom: 1rem; 7646} 7647 7648.summary-text { 7649 font-size: 1rem; 7650 line-height: 1.6; 7651 color: #374151; 7652 margin-bottom: 1rem; 7653} 7654 7655.winner-badge { 7656 padding: 1rem; 7657 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7658 color: white; 7659 border-radius: 8px; 7660} 7661 7662.winner-badge strong { 7663 display: block; 7664 margin-bottom: 0.25rem; 7665} 7666 7667.winner-badge small { 7668 opacity: 0.9; 7669 font-size: 0.875rem; 7670} 7671 7672/* Comparison Table */ 7673.comparison-grid { 7674 display: grid; 7675 gap: 1rem; 7676 margin-top: 1rem; 7677} 7678 7679.comparison-category { 7680 background: white; 7681 border: 1px solid #e2e8f0; 7682 border-radius: 8px; 7683 overflow: hidden; 7684} 7685 7686.category-header { 7687 background: #f1f5f9; 7688 padding: 0.75rem 1rem; 7689 font-weight: 600; 7690 color: #1e293b; 7691 border-bottom: 1px solid #e2e8f0; 7692} 7693 7694.category-products { 7695 display: grid; 7696 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7697 gap: 1px; 7698 background: #e2e8f0; 7699} 7700 7701.product-score { 7702 background: white; 7703 padding: 1rem; 7704 text-align: center; 7705} 7706 7707.score-value { 7708 display: inline-flex; 7709 align-items: center; 7710 gap: 0.25rem; 7711 margin-bottom: 0.5rem; 7712} 7713 7714.score-stars { 7715 color: #fbbf24; 7716} 7717 7718.score-note { 7719 font-size: 0.8rem; 7720 color: #64748b; 7721 line-height: 1.3; 7722} 7723 7724/* Product name header in comparison table */ 7725.product-name-header { 7726 margin-bottom: 0.5rem; 7727} 7728 7729.product-name-header strong { 7730 font-size: .8rem; /* h5 size (18px) */ 7731 font-weight: 600; 7732 color: #1e293b; 7733 line-height: 1.3; 7734 display: block; 7735 word-break: break-word; 7736} 7737 7738.product-price { 7739 font-size: 0.8rem; 7740 font-weight: 600; 7741 color: #059669; 7742} 7743 7744/* Detailed Analysis */ 7745.products-analysis { 7746 display: grid; 7747 gap: 1rem; 7748 margin-top: 1rem; 7749} 7750 7751.product-analysis { 7752 background: white; 7753 border: 1px solid #e2e8f0; 7754 border-radius: 8px; 7755 padding: 1rem; 7756} 7757 7758.product-analysis h5 { 7759 color: #1e293b; 7760 margin-bottom: 0.75rem; 7761 font-weight: 600; 7762} 7763 7764.analysis-section { 7765 margin-bottom: 0.75rem; 7766} 7767 7768.analysis-section:last-child { 7769 margin-bottom: 0; 7770} 7771 7772.analysis-section h6 { 7773 font-size: 0.875rem; 7774 font-weight: 600; 7775 margin-bottom: 0.5rem; 7776} 7777 7778.analysis-section h6.strengths { 7779 color: #059669; 7780} 7781 7782.analysis-section h6.weaknesses { 7783 color: #dc2626; 7784} 7785 7786.analysis-section h6.best-for { 7787 color: #7c3aed; 7788} 7789 7790.analysis-section ul { 7791 list-style: none; 7792 padding: 0; 7793 margin: 0; 7794} 7795 7796.analysis-section li { 7797 display: flex; 7798 align-items: flex-start; 7799 gap: 0.5rem; 7800 margin-bottom: 0.25rem; 7801 font-size: 0.875rem; 7802 line-height: 1.4; 7803} 7804 7805.analysis-section li:before { 7806 content: "â¢";
7807 color: #64748b; 7808 font-weight: bold; 7809 margin-top: 0.1rem; 7810} 7811 7812.best-for-text { 7813 font-size: 0.875rem; 7814 color: #374151; 7815 font-style: italic; 7816} 7817 7818/* Recommendations */ 7819.recommendations-grid { 7820 display: grid; 7821 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7822 gap: 1rem; 7823 margin-top: 1rem; 7824} 7825 7826.recommendation-card { 7827 background: white; 7828 border: 1px solid #e2e8f0; 7829 border-radius: 8px; 7830 padding: 1rem; 7831 text-align: center; 7832} 7833 7834.recommendation-card h5 { 7835 font-size: 1rem; 7836 margin-bottom: 0.75rem; 7837 color: #1e293b; 7838} 7839 7840.recommendation-card p { 7841 font-size: 0.875rem; 7842 color: #374151; 7843 line-height: 1.5; 7844} 7845 7846/* Tips */ 7847.tips-list { 7848 list-style: none; 7849 padding: 0; 7850 margin: 1rem 0 0 0; 7851} 7852 7853.tips-list li { 7854 display: flex; 7855 align-items: flex-start; 7856 gap: 0.75rem; 7857 margin-bottom: 0.75rem; 7858 font-size: 0.9rem; 7859 line-height: 1.5; 7860 color: #374151; 7861} 7862 7863.tips-list li:before { 7864 content: "ð¡"; 7865 font-size: 1rem; 7866 margin-top: 0.1rem; 7867} 7868 7869/* Modal Footer */ 7870.modal-footer { 7871 display: flex; 7872 justify-content: space-between; 7873 align-items: center; 7874 padding: 1.5rem 2rem; 7875 border-top: 1px solid #e2e8f0; 7876 background: #f8fafc; 7877} 7878 7879.btn-outline-secondary { 7880 border: 1px solid #64748b; 7881 background: transparent; 7882 color: #64748b; 7883 padding: 0.75rem 1.5rem; 7884 border-radius: 6px; 7885 font-weight: 600; 7886 display: flex; 7887 align-items: center; 7888 gap: 0.5rem; 7889 cursor: pointer; 7890 transition: all 0.2s ease; 7891} 7892 7893.btn-outline-secondary:hover { 7894 background: #64748b; 7895 color: white; 7896} 7897 7898/* Responsive for chip button */ 7899@media (max-width: 880px) { 7900 .pd-compare-section { 7901 margin: 1rem 0; 7902 display: block; 7903 } 7904 7905 .pd-compare-btn { 7906 width: 100%; 7907 font-size: 0.85rem; 7908 padding: 0.65rem 1rem; 7909 border-radius: 10px; 7910 justify-content: center; 7911 min-height: 44px; 7912 } 7913 7914 .pd-compare-btn .compare-icon { 7915 width: 16px; 7916 height: 16px; 7917 } 7918 7919 .pd-compare-info { 7920 margin-top: 0.5rem; 7921 display: block; 7922 text-align: center; 7923 } 7924 7925 .compare-note { 7926 font-size: 0.7rem; 7927 display: block; 7928 } 7929} 7930 7931/* Responsive Design */ 7932@media (max-width: 768px) { 7933 .compare-sidebar { 7934 width: 100%; 7935 right: -100%; 7936 /* Fix for mobile viewport height changes */ 7937 height: 100vh; 7938 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7939 } 7940 7941 .compare-actions { 7942 /* Ensure actions are always visible */ 7943 position: sticky; 7944 bottom: 0; 7945 background: white; 7946 border-top: 2px solid #e2e8f0; 7947 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7948 z-index: 10; 7949 } 7950 7951 .compare-actions .btn { 7952 /* Larger touch targets for mobile */ 7953 min-height: 48px; 7954 font-size: 0.9rem; 7955 font-weight: 700; 7956 } 7957 7958 .modal-container { 7959 width: 95%; 7960 max-height: 95vh; 7961 max-height: 95dvh; /* Dynamic viewport height */ 7962 } 7963 7964 .modal-header, 7965 .modal-content, 7966 .modal-footer { 7967 padding: 1rem; 7968 } 7969 7970 .recommendations-grid { 7971 grid-template-columns: 1fr; 7972 } 7973 7974 .category-products { 7975 grid-template-columns: 1fr; 7976 } 7977 7978 .modal-footer { 7979 flex-direction: column; 7980 gap: 0.75rem; 7981 } 7982 7983 .modal-footer .btn { 7984 width: 100%; 7985 } 7986 7987 /* Ensure compare button is visible on mobile */ 7988 .pd-compare-section { 7989 display: block !important; 7990 visibility: visible !important; 7991 opacity: 1 !important; 7992 margin: 1rem 0; 7993 padding: 0; 7994 position: relative !important; 7995 z-index: 1 !important; 7996 max-width: 100%; 7997 overflow: hidden; 7998 } 7999 8000 .pd-compare-btn { 8001 display: flex !important; 8002 visibility: visible !important; 8003 opacity: 1 !important; 8004 width: 100%; 8005 padding: 0.75rem 1rem; 8006 font-size: 0.9rem; 8007 min-height: 48px; /* Minimum touch target for mobile */ 8008 border: 2px solid #e2e8f0 !important; 8009 background: #fff !important; 8010 position: relative !important; 8011 z-index: 2 !important; 8012 box-sizing: border-box; 8013 white-space: normal !important; /* Allow text wrapping on mobile */ 8014 flex-wrap: wrap; 8015 gap: 0.5rem; 8016 } 8017 8018 .pd-compare-btn .compare-text { 8019 display: inline !important; 8020 visibility: visible !important; 8021 font-weight: 600; 8022 color: #475569 !important; 8023 } 8024 8025 .pd-compare-btn .compare-remove { 8026 display: none !important; /* Hidden by default */ 8027 } 8028 8029 .pd-compare-btn.added .compare-remove { 8030 display: flex !important; /* Only show when added */ 8031 flex-wrap: wrap; 8032 align-items: center; 8033 gap: 0.4rem; 8034 white-space: normal !important; 8035 word-break: break-word; 8036 } 8037 8038 .pd-compare-btn .compare-icon { 8039 display: inline-block !important; 8040 width: 18px; 8041 height: 18px; 8042 flex-shrink: 0; 8043 } 8044 8045 .pd-compare-info { 8046 margin-top: 0.5rem; 8047 } 8048 8049 .compare-note { 8050 font-size: 0.85rem !important; 8051 display: block !important; 8052 visibility: visible !important; 8053 color: #64748b !important; 8054 } 8055} 8056 8057/* iOS Safari specific fixes */ 8058@media (max-width: 480px) { 8059 #compareBtn { 8060 width:46px; 8061 height:46px; 8062 } 8063 8064 .pd-compare-section { 8065 -webkit-transform: translateZ(0); 8066 transform: translateZ(0); 8067 } 8068 8069 .pd-compare-btn { 8070 -webkit-appearance: none !important; 8071 appearance: none !important; 8072 -webkit-tap-highlight-color: transparent; 8073 touch-action: manipulation; 8074 } 8075 8076 /* Fix sidebar height for iOS address bar */ 8077 .compare-sidebar { 8078 /* Use fixed positioning that accounts for iOS address bar */ 8079 height: 100vh; 8080 height: -webkit-fill-available; /* iOS Safari compatibility */ 8081 min-height: 100vh; 8082 min-height: -webkit-fill-available; 8083 } 8084 8085 /* Ensure compare actions stay at bottom */ 8086 .compare-actions { 8087 position: sticky; 8088 bottom: 0; 8089 background: white; 8090 border-top: 2px solid #e2e8f0; 8091 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8092 padding: 1.25rem 1rem; 8093 z-index: 20; 8094 } 8095 8096 .compare-actions .btn { 8097 min-height: 52px; /* Larger for easier tapping */ 8098 font-size: 1rem; 8099 font-weight: 700; 8100 border-radius: 8px; 8101 } 8102 8103 /* Adjust content padding to account for sticky actions */ 8104 .compare-content { 8105 padding-bottom: 6rem; /* Extra space for sticky actions */ 8106 } 8107} 8108 8109/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8110@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8111 .pd-compare-section { 8112 display: block !important; 8113 } 8114 8115 .pd-compare-btn { 8116 display: flex !important; 8117 min-height: 44px; 8118 font-size: 16px; /* Prevent zoom on iOS */ 8119 } 8120 8121 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8122 .compare-sidebar { 8123 /* Ensure full height coverage */ 8124 height: 100vh; 8125 height: 100dvh; /* Dynamic viewport height */ 8126 height: -webkit-fill-available; /* iOS fallback */ 8127 max-height: 100vh; 8128 max-height: 100dvh; 8129 max-height: -webkit-fill-available; 8130 overflow: hidden; /* Prevent scroll issues */ 8131 } 8132 8133 .compare-content { 8134 /* Flexible content area */ 8135 flex: 1; 8136 overflow-y: auto; 8137 -webkit-overflow-scrolling: touch; 8138 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8139 } 8140 8141 .compare-actions { 8142 /* Force actions to stay visible */ 8143 position: fixed; 8144 bottom: 0; 8145 left: 0; 8146 right: 0; 8147 width: 100%; 8148 background: white; 8149 border-top: 2px solid #e2e8f0; 8150 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8151 padding: 1rem; 8152 z-index: 9999; 8153 display: flex !important; 8154 gap: 0.75rem; 8155 } 8156 8157 .compare-actions .btn { 8158 flex: 1; 8159 min-height: 30px; /* Extra large for high-DPI screens */ 8160 font-size: .8rem; 8161 font-weight: 600; 8162 border-radius: 12px; 8163 display: flex !important;
8164 align-items: center; 8165 justify-content: center; 8166 gap: 0.5rem; 8167 } 8168 8169 /* Ensure AI analyze button is always visible and prominent */ 8170 #compareAnalyze { 8171 background: #3b82f6 !important; 8172 border-color: #3b82f6 !important; 8173 color: white !important; 8174 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8175 } 8176 8177 #compareAnalyze:not(:disabled) { 8178 background: #2563eb !important; 8179 border-color: #2563eb !important; 8180 transform: scale(1.02); 8181 } 8182 8183 #compareAnalyze:disabled { 8184 opacity: 0.6 !important; 8185 background: #9ca3af !important; 8186 border-color: #9ca3af !important; 8187 } 8188} 8189 8190/* AI Disclaimer Styles */ 8191.ai-disclaimer { 8192 display: flex; 8193 align-items: flex-start; 8194 gap: 0.75rem; 8195 padding: 1rem; 8196 background: #fef3c7; 8197 border: 1px solid #fbbf24; 8198 border-radius: 8px; 8199 margin: 0; 8200} 8201 8202.disclaimer-icon { 8203 font-size: 1.25rem; 8204 flex-shrink: 0; 8205 margin-top: 0.125rem; 8206} 8207 8208.disclaimer-text { 8209 font-size: 0.85rem; 8210 line-height: 1.4; 8211 color: #92400e; 8212} 8213 8214.disclaimer-text strong { 8215 color: #78350f; 8216 font-weight: 600; 8217} 8218 8219/* Compare Disclaimer in Sidebar */ 8220.compare-disclaimer { 8221 margin-top: 1rem; 8222 padding: 1rem; 8223 background: #f0f9ff; 8224 border: 1px solid #0ea5e9; 8225 border-radius: 8px; 8226} 8227 8228.compare-disclaimer h6 { 8229 display: flex; 8230 align-items: center; 8231 gap: 0.5rem; 8232 font-size: 0.875rem; 8233 font-weight: 600; 8234 color: #0c4a6e; 8235 margin-bottom: 0.5rem; 8236} 8237 8238.compare-disclaimer p { 8239 font-size: 0.8rem; 8240 line-height: 1.4; 8241 color: #075985; 8242 margin-bottom: 0.5rem; 8243} 8244 8245.compare-disclaimer p:last-child { 8246 margin-bottom: 0; 8247} 8248 8249.compare-disclaimer ul { 8250 list-style: none; 8251 padding: 0; 8252 margin: 0.5rem 0 0 0; 8253} 8254 8255.compare-disclaimer li { 8256 display: flex; 8257 align-items: flex-start; 8258 gap: 0.5rem; 8259 margin-bottom: 0.25rem; 8260 font-size: 0.75rem; 8261 line-height: 1.3; 8262 color: #075985; 8263} 8264 8265.compare-disclaimer li:before { 8266 content: "â¢"; 8267 color: #0ea5e9; 8268 font-weight: bold; 8269 margin-top: 0.1rem; 8270 flex-shrink: 0; 8271} 8272 8273/* Mobile Browser Address Bar Fixes */ 8274@supports (height: 100dvh) { 8275 /* Modern browsers with dynamic viewport support */ 8276 @media (max-width: 768px) { 8277 .compare-sidebar { 8278 height: 100dvh !important; 8279 max-height: 100dvh !important; 8280 } 8281 8282 .modal-container { 8283 max-height: 95dvh !important; 8284 } 8285 } 8286} 8287 8288@supports (height: -webkit-fill-available) { 8289 /* iOS Safari specific support */ 8290 @media (max-width: 768px) { 8291 .compare-sidebar { 8292 height: -webkit-fill-available !important; 8293 max-height: -webkit-fill-available !important; 8294 } 8295 } 8296} 8297 8298/* Fallback for older browsers */ 8299@media (max-width: 768px) and (orientation: portrait) { 8300 .compare-sidebar { 8301 /* Fixed height that works across different mobile browsers */ 8302 min-height: 600px; 8303 height: calc(100vh - env(keyboard-inset-height, 0px)); 8304 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8305 } 8306 8307 /* Ensure actions are always accessible */ 8308 .compare-actions { 8309 position: sticky !important; 8310 bottom: env(safe-area-inset-bottom, 0px) !important; 8311 margin-bottom: 0 !important; 8312 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8313 } 8314} 8315 8316/* PWA and fullscreen app support */ 8317@media (display-mode: standalone) { 8318 .compare-sidebar { 8319 height: 100vh !important; 8320 height: 100dvh !important; 8321 } 8322} 8323 8324/* Utilities */ 8325.btn-sm { 8326 padding: 0.5rem 0.75rem; 8327 font-size: 0.8rem; 8328} 8329 8330[hidden] { 8331 display: none !important; 8332} 8333 8334/* ===== UX ENHANCEMENTS ===== */ 8335 8336/* Compare Button Highlight Animation */ 8337.pulse-highlight { 8338 animation: pulse-glow 2s ease-in-out 3; 8339 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8340} 8341 8342@keyframes pulse-glow { 8343 0% { 8344 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8345 } 8346 50% { 8347 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8348 } 8349 100% { 8350 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); 8351 } 8352} 8353 8354/* Flying Product Animation */ 8355.flying-product { 8356 position: fixed; 8357 z-index: 9999; 8358 pointer-events: none; 8359} 8360 8361.flying-product img { 8362 border-radius: 8px; 8363 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8364} 8365 8366/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8367
8368/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8369</style> 8370 <style data-inlined="product-card-actions.css"> 8371/** 8372 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8373 * Tokopedia-inspired design 8374 */ 8375 8376/* ===== 3 Dot Button on Product Card ===== */ 8377.prod-card { 8378 position: relative; 8379} 8380 8381.prod-card-actions-btn { 8382 position: absolute; 8383 top: 8px; 8384 right: 8px; 8385 width: 32px; 8386 height: 32px; 8387 border-radius: 50%; 8388 background: rgba(255, 255, 255, 0.95); 8389 border: 1px solid rgba(0, 0, 0, 0.08); 8390 display: inline-flex; 8391 align-items: center; 8392 justify-content: center; 8393 cursor: pointer; 8394 z-index: 3; 8395 transition: all 0.2s ease; 8396 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8397 -webkit-tap-highlight-color: transparent; 8398} 8399 8400.prod-card-actions-btn:hover { 8401 background: #fff; 8402 border-color: rgba(0, 0, 0, 0.15); 8403 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8404 transform: scale(1.05); 8405} 8406 8407.prod-card-actions-btn:active { 8408 transform: scale(0.95); 8409} 8410 8411.prod-card-actions-btn svg { 8412 width: 20px; 8413 height: 20px; 8414 color: #64748b; 8415 display: block; 8416} 8417 8418/* Desktop: Show on hover */ 8419@media (min-width: 641px) { 8420 .prod-card-actions-btn { 8421 opacity: 0; 8422 pointer-events: none; 8423 } 8424 8425 .prod-card:hover .prod-card-actions-btn { 8426 opacity: 1; 8427 pointer-events: auto; 8428 } 8429} 8430 8431/* Mobile: Always visible but smaller */ 8432@media (max-width: 640px) { 8433 .prod-card-actions-btn { 8434 width: 28px; 8435 height: 28px; 8436 top: 6px; 8437 right: 6px; 8438 opacity: 95%; 8439 } 8440 8441 .prod-card-actions-btn svg { 8442 width: 18px; 8443 height: 18px; 8444 } 8445} 8446 8447/* ===== Bottom Sheet Overlay ===== */ 8448.pca-overlay { 8449 position: fixed; 8450 inset: 0; 8451 background: rgba(15, 23, 42, 0.6); 8452 backdrop-filter: blur(4px); 8453 z-index: 99998; 8454 opacity: 0; 8455 visibility: hidden; 8456 transition: all 0.3s ease; 8457} 8458 8459.pca-overlay.active { 8460 opacity: 1; 8461 visibility: visible; 8462} 8463 8464/* ===== Bottom Sheet Container ===== */ 8465.pca-bottom-sheet { 8466 position: fixed; 8467 bottom: 0; 8468 left: 0; 8469 right: 0; 8470 background: #fff; 8471 border-radius: 20px 20px 0 0; 8472 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8473 z-index: 99999; 8474 max-width: 600px; 8475 margin: 0 auto; 8476 transform: translateY(100%); 8477 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8478} 8479 8480.pca-bottom-sheet.active { 8481 transform: translateY(0); 8482} 8483 8484/* Drag Handle */ 8485.pca-sheet-handle { 8486 width: 40px; 8487 height: 4px; 8488 background: #cbd5e1; 8489 border-radius: 999px; 8490 margin: 12px auto 8px; 8491} 8492 8493/* Product Preview */ 8494.pca-sheet-product { 8495 display: flex; 8496 gap: 12px; 8497 padding: 12px 20px; 8498 border-bottom: 1px solid #f1f5f9; 8499} 8500 8501.pca-product-image { 8502 width: 60px; 8503 height: 60px; 8504 border-radius: 8px; 8505 object-fit: cover; 8506 flex-shrink: 0; 8507 border: 1px solid #e2e8f0; 8508} 8509 8510.pca-product-info { 8511 flex: 1; 8512 min-width: 0; 8513} 8514 8515.pca-product-name { 8516 font-size: 14px; 8517 font-weight: 600; 8518 color: #0f172a; 8519 margin: 0 0 4px 0; 8520 display: -webkit-box; 8521 -webkit-line-clamp: 2; 8522 -webkit-box-orient: vertical; 8523 overflow: hidden; 8524 line-height: 1.3; 8525} 8526 8527.pca-product-price { 8528 font-size: 15px; 8529 font-weight: 700; 8530 color: #0ea5e9; 8531 margin: 0; 8532} 8533 8534.pca-product-stock { 8535 font-size: 12px; 8536 color: #64748b; 8537 margin: 2px 0 0 0; 8538} 8539 8540.pca-product-stock.in { 8541 color: #16a34a; 8542} 8543 8544.pca-product-stock.low { 8545 color: #ea580c; 8546} 8547 8548.pca-product-stock.out { 8549 color: #dc2626; 8550} 8551 8552/* Action Menu */ 8553.pca-sheet-actions { 8554 padding: 8px 0 20px; 8555} 8556 8557.pca-action-item { 8558 display: flex; 8559 align-items: center; 8560 gap: 16px; 8561 padding: 16px 20px; 8562 background: transparent; 8563 border: 0; 8564 width: 100%; 8565 cursor: pointer; 8566 transition: background-color 0.15s ease; 8567 -webkit-tap-highlight-color: transparent; 8568} 8569 8570.pca-action-item:hover { 8571 background: #f8fafc; 8572} 8573 8574.pca-action-item:active { 8575 background: #f1f5f9; 8576} 8577 8578.pca-action-item:disabled { 8579 opacity: 0.4; 8580 cursor: not-allowed; 8581} 8582 8583.pca-action-item:disabled:hover { 8584 background: transparent; 8585} 8586 8587.pca-action-icon { 8588 width: 24px; 8589 height: 24px; 8590 color: #475569; 8591 flex-shrink: 0; 8592} 8593 8594.pca-action-content { 8595 flex: 1; 8596 text-align: left; 8597} 8598 8599.pca-action-label { 8600 font-size: 15px; 8601 font-weight: 600; 8602 color: #0f172a; 8603 margin: 0 0 2px 0; 8604} 8605 8606.pca-action-desc { 8607 font-size: 12px; 8608 color: #64748b; 8609 margin: 0; 8610} 8611 8612.pca-action-badge { 8613 display: inline-flex; 8614 align-items: center; 8615 justify-content: center;
8616 min-width: 20px; 8617 height: 20px; 8618 padding: 0 6px; 8619 background: #0ea5e9; 8620 color: #fff; 8621 font-size: 11px; 8622 font-weight: 700; 8623 border-radius: 999px; 8624 margin-left: auto; 8625} 8626 8627.pca-action-badge.compare { 8628 background: #8b5cf6; 8629} 8630 8631/* Action Item Colors */ 8632.pca-action-item.cart .pca-action-icon { 8633 color: #0ea5e9; 8634} 8635 8636.pca-action-item.compare .pca-action-icon { 8637 color: #8b5cf6; 8638} 8639 8640.pca-action-item.compare.active { 8641 background: rgba(139, 92, 246, 0.05); 8642} 8643 8644.pca-action-item.compare.active .pca-action-label { 8645 color: #8b5cf6; 8646} 8647 8648.pca-action-item.compare.active .pca-action-icon { 8649 color: #8b5cf6; 8650} 8651 8652.pca-action-item.share .pca-action-icon { 8653 color: #64748b; 8654} 8655 8656/* Loading State */ 8657.pca-action-item.loading { 8658 pointer-events: none; 8659} 8660 8661.pca-action-item.loading .pca-action-icon { 8662 animation: spin 1s linear infinite; 8663} 8664 8665@keyframes spin { 8666 from { transform: rotate(0deg); } 8667 to { transform: rotate(360deg); } 8668} 8669 8670/* Success State Animation */ 8671.pca-action-item.success .pca-action-icon { 8672 animation: checkmark 0.5s ease; 8673} 8674 8675@keyframes checkmark { 8676 0% { transform: scale(0); } 8677 50% { transform: scale(1.2); } 8678 100% { transform: scale(1); } 8679} 8680 8681/* ===== Safe Area for Mobile Devices ===== */ 8682@supports (padding-bottom: env(safe-area-inset-bottom)) { 8683 .pca-sheet-actions { 8684 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8685 } 8686} 8687 8688/* ===== Responsive Tweaks ===== */ 8689@media (max-width: 640px) { 8690 .pca-bottom-sheet { 8691 border-radius: 16px 16px 0 0; 8692 } 8693 8694 .pca-sheet-product { 8695 padding: 10px 16px; 8696 } 8697 8698 .pca-product-image { 8699 width: 50px; 8700 height: 50px; 8701 } 8702 8703 .pca-product-name { 8704 font-size: 13px; 8705 } 8706 8707 .pca-product-price { 8708 font-size: 14px; 8709 } 8710 8711 .pca-action-item { 8712 padding: 14px 16px; 8713 gap: 12px; 8714 } 8715 8716 .pca-action-icon { 8717 width: 22px; 8718 height: 22px; 8719 } 8720 8721 .pca-action-label { 8722 font-size: 14px; 8723 } 8724} 8725 8726/* ===== Animation for Flying Product ===== */ 8727.flying-product { 8728 position: fixed; 8729 pointer-events: none; 8730 z-index: 99999; 8731 animation: fadeOut 0.8s ease forwards; 8732} 8733 8734@keyframes fadeOut { 8735 to { 8736 opacity: 0; 8737 transform: scale(0.5); 8738 } 8739} 8740 8741/* ===== Desktop Dropdown (Alternative) ===== */ 8742@media (min-width: 1024px) { 8743 /* Optional: Could add desktop dropdown menu here */ 8744 /* For now, using bottom sheet for consistency */ 8745} 8746 8747/* ===== Print Styles ===== */ 8748@media print { 8749 .prod-card-actions-btn, 8750 .pca-overlay, 8751 .pca-bottom-sheet { 8752 display: none !important; 8753 } 8754} 8755 8756</style> 8757 <style data-inlined="modal.css"> 8758/* Modern Modal Dialog for Cart Operations */ 8759.modal-overlay { 8760 position: fixed; 8761 top: 0; 8762 left: 0; 8763 right: 0; 8764 bottom: 0; 8765 background: rgba(0, 0, 0, 0.5); 8766 backdrop-filter: blur(4px); 8767 display: flex; 8768 align-items: center; 8769 justify-content: center; 8770 z-index: 9999; 8771 opacity: 0; 8772 visibility: hidden; 8773 transition: all 0.2s ease; 8774} 8775 8776.modal-overlay.show { 8777 opacity: 1; 8778 visibility: visible; 8779} 8780 8781.modal-dialog { 8782 background: white; 8783 border-radius: 16px; 8784 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8785 max-width: 400px; 8786 width: 90%; 8787 max-height: 90vh; 8788 overflow-y: auto; 8789 transform: scale(0.95); 8790 transition: transform 0.2s ease; 8791} 8792 8793.modal-overlay.show .modal-dialog { 8794 transform: scale(1); 8795} 8796 8797.modal-header { 8798 padding: 24px 24px 16px; 8799 border-bottom: 1px solid #f1f5f9; 8800} 8801 8802.modal-title { 8803 font-size: 20px; 8804 font-weight: 700; 8805 color: #0f172a; 8806 margin: 0; 8807 display: flex; 8808 align-items: center; 8809 gap: 12px; 8810} 8811 8812.modal-body { 8813 padding: 16px 24px 24px; 8814} 8815 8816.modal-message { 8817 font-size: 16px; 8818 line-height: 1.5; 8819 color: #475569; 8820 margin: 0; 8821} 8822 8823.modal-actions { 8824 display: flex; 8825 gap: 12px; 8826 margin-top: 24px; 8827} 8828 8829.modal-btn { 8830 flex: 1; 8831 padding: 12px 16px; 8832 border: none; 8833 border-radius: 8px; 8834 font-size: 14px; 8835 font-weight: 600; 8836 cursor: pointer; 8837 transition: all 0.2s ease; 8838 text-align: center; 8839} 8840 8841.modal-btn-secondary { 8842 background: #f8fafc; 8843 color: #64748b; 8844 border: 1px solid #e2e8f0; 8845} 8846 8847.modal-btn-secondary:hover { 8848 background: #f1f5f9; 8849 border-color: #cbd5e1; 8850} 8851 8852.modal-btn-danger { 8853 background: #ef4444; 8854 color: white; 8855} 8856 8857.modal-btn-danger:hover { 8858 background: #dc2626; 8859} 8860 8861.modal-btn-primary { 8862 background: #3b82f6; 8863 color: white; 8864} 8865 8866.modal-btn-primary:hover { 8867 background: #2563eb; 8868} 8869 8870.modal-icon { 8871 font-size: 24px; 8872 width: 32px; 8873 height: 32px; 8874 display: flex; 8875 align-items: center; 8876 justify-content: center; 8877 border-radius: 8px; 8878} 8879 8880.modal-icon.danger { 8881 background: #fef2f2; 8882 color: #ef4444; 8883} 8884 8885.modal-icon.warning { 8886 background: #fffbeb; 8887 color: #f59e0b; 8888} 8889 8890.modal-icon.success { 8891 background: #f0fdf4; 8892 color: #22c55e; 8893} 8894 8895.modal-icon.info { 8896 background: #eff6ff; 8897 color: #3b82f6; 8898} 8899 8900/* Mobile responsive */ 8901@media (max-width: 640px) { 8902 .modal-dialog { 8903 margin: 16px; 8904 max-width: none; 8905 width: auto; 8906 } 8907 8908 .modal-header, 8909 .modal-body { 8910 padding-left: 20px; 8911 padding-right: 20px; 8912 } 8913 8914 .modal-actions { 8915 flex-direction: column; 8916 } 8917 8918 .modal-btn { 8919 flex: none; 8920 } 8921} 8922</style> 8923 8924 <style data-inlined="reviews-points.css"> 8925/** 8926 * Reviews & Points System - Styles 8927 * Product reviews, rating display, and point redemption UI 8928 */ 8929
8930/* =================================== 8931 PRODUCT REVIEWS 8932 =================================== */ 8933 8934.product-tabs { 8935 display: flex; 8936 border-bottom: 2px solid #e5e7eb; 8937 margin: 24px 0 16px; 8938 gap: 8px; 8939} 8940 8941.tab-btn { 8942 padding: 12px 20px; 8943 border: none; 8944 background: none; 8945 cursor: pointer; 8946 font-size: 15px; 8947 font-weight: 500; 8948 color: #6b7280; 8949 border-bottom: 3px solid transparent; 8950 transition: all 0.2s ease; 8951 position: relative; 8952 top: 2px; 8953} 8954 8955.tab-btn:hover { 8956 color: #111827; 8957} 8958 8959.tab-btn.active { 8960 color: #2563eb; 8961 border-bottom-color: #2563eb; 8962} 8963 8964.tab-content { 8965 padding: 20px 0; 8966} 8967 8968/* Review Statistics */ 8969.review-stats { 8970 display: grid; 8971 grid-template-columns: 200px 1fr; 8972 gap: 32px; 8973 padding: 24px; 8974 background: #f9fafb; 8975 border-radius: 12px; 8976 margin-bottom: 24px; 8977} 8978 8979.rating-summary { 8980 text-align: center; 8981} 8982 8983.avg-rating { 8984 display: block; 8985 font-size: 48px; 8986 font-weight: 700; 8987 color: #111827; 8988 line-height: 1; 8989} 8990 8991.rating-summary .stars { 8992 font-size: 24px; 8993 color: #fbbf24; 8994 margin: 8px 0; 8995} 8996 8997.total-reviews { 8998 display: block; 8999 font-size: 14px; 9000 color: #6b7280; 9001 margin-top: 4px; 9002} 9003 9004.rating-breakdown { 9005 display: flex; 9006 flex-direction: column; 9007 gap: 8px; 9008} 9009 9010.rating-bar { 9011 display: flex; 9012 align-items: center; 9013 gap: 12px; 9014} 9015 9016.rating-label { 9017 font-size: 14px; 9018 color: #374151; 9019 width: 32px; 9020 text-align: right; 9021} 9022 9023.bar-container { 9024 flex: 1; 9025 height: 8px; 9026 background: #e5e7eb; 9027 border-radius: 4px; 9028 overflow: hidden; 9029} 9030 9031.bar-fill { 9032 height: 100%; 9033 background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%); 9034 transition: width 0.3s ease; 9035} 9036 9037.rating-count { 9038 font-size: 14px; 9039 color: #6b7280; 9040 width: 40px; 9041 text-align: right; 9042} 9043 9044/* Review Filters */ 9045.review-filters { 9046 display: flex; 9047 justify-content: space-between; 9048 align-items: center; 9049 margin-bottom: 20px; 9050} 9051 9052.review-sort { 9053 padding: 8px 12px; 9054 border: 1px solid #d1d5db; 9055 border-radius: 8px; 9056 font-size: 14px; 9057 background: white; 9058} 9059 9060.write-review-btn { 9061 padding: 10px 20px; 9062 background: #2563eb; 9063 color: white; 9064 border: none; 9065 border-radius: 8px; 9066 font-weight: 500; 9067 cursor: pointer; 9068 transition: background 0.2s; 9069} 9070 9071.write-review-btn:hover { 9072 background: #1d4ed8; 9073} 9074 9075/* Review List */ 9076.reviews-list { 9077 display: flex; 9078 flex-direction: column; 9079 gap: 16px; 9080} 9081 9082.review-item { 9083 padding: 20px; 9084 background: white; 9085 border: 1px solid #e5e7eb; 9086 border-radius: 12px; 9087 transition: box-shadow 0.2s; 9088} 9089 9090.review-item:hover { 9091 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9092} 9093 9094.review-header { 9095 display: flex; 9096 justify-content: space-between; 9097 align-items: flex-start; 9098 margin-bottom: 8px; 9099} 9100 9101.review-author { 9102 display: flex; 9103 align-items: center; 9104 gap: 8px; 9105} 9106 9107.review-author strong { 9108 font-size: 15px; 9109 color: #111827; 9110} 9111 9112.verified-badge { 9113 display: inline-flex; 9114 align-items: center; 9115 gap: 4px; 9116 font-size: 12px; 9117 color: #059669; 9118 background: #d1fae5; 9119 padding: 2px 8px; 9120 border-radius: 4px; 9121 font-weight: 500; 9122} 9123 9124.anonymous-badge { 9125 display: inline-flex; 9126 align-items: center; 9127 font-size: 12px; 9128 color: #6b7280; 9129 background: #f3f4f6; 9130 padding: 2px 6px; 9131 border-radius: 4px; 9132 margin-left: 4px; 9133 cursor: help; 9134} 9135 9136.review-rating .stars { 9137 font-size: 16px; 9138 color: #fbbf24; 9139} 9140 9141.review-date { 9142 font-size: 13px; 9143 color: #9ca3af; 9144 margin-bottom: 12px; 9145} 9146 9147.review-text { 9148 font-size: 15px; 9149 line-height: 1.6; 9150 color: #374151; 9151 margin-bottom: 12px; 9152} 9153 9154.review-images { 9155 display: flex; 9156 gap: 8px; 9157 margin-bottom: 12px; 9158 flex-wrap: wrap; 9159} 9160 9161.review-img { 9162 width: 80px; 9163 height: 80px; 9164 object-fit: cover; 9165 border-radius: 8px; 9166 cursor: pointer; 9167 transition: transform 0.2s; 9168} 9169 9170.review-img:hover { 9171 transform: scale(1.05); 9172} 9173 9174.review-actions { 9175 display: flex; 9176 gap: 12px; 9177 padding-top: 12px; 9178 border-top: 1px solid #f3f4f6; 9179} 9180 9181.btn-helpful { 9182 display: flex; 9183 align-items: center; 9184 gap: 6px; 9185 padding: 6px 12px; 9186 background: #f9fafb; 9187 border: 1px solid #e5e7eb; 9188 border-radius: 6px; 9189 font-size: 13px; 9190 color: #6b7280; 9191 cursor: pointer; 9192 transition: all 0.2s; 9193} 9194 9195.btn-helpful:hover { 9196 background: #f3f4f6; 9197 color: #111827; 9198 border-color: #d1d5db; 9199} 9200 9201.btn-helpful.voted { 9202 background: #dbeafe; 9203 color: #1d4ed8; 9204 border-color: #93c5fd; 9205} 9206 9207.helpful-icon { 9208 font-size: 14px; 9209} 9210 9211.no-reviews { 9212 text-align: center; 9213 padding: 60px 20px; 9214 color: #9ca3af; 9215 font-size: 15px; 9216} 9217 9218.load-more-btn { 9219 display: block; 9220 margin: 24px auto 0; 9221 padding: 12px 32px; 9222 background: white; 9223 border: 1px solid #d1d5db; 9224 border-radius: 8px; 9225 font-size: 14px; 9226 font-weight: 500; 9227 color: #374151; 9228 cursor: pointer; 9229 transition: all 0.2s; 9230} 9231 9232.load-more-btn:hover { 9233 background: #f9fafb; 9234 border-color: #9ca3af; 9235} 9236 9237/* =================================== 9238 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9239 =================================== */ 9240 9241.point-balance-card { 9242 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 9243 color: white; 9244 padding: 24px; 9245 border-radius: 16px; 9246 margin-bottom: 24px; 9247 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9248} 9249 9250.point-header { 9251 display: flex; 9252 justify-content: space-between; 9253 align-items: center; 9254 margin-bottom: 12px; 9255} 9256 9257.point-header h3 { 9258 font-size: 18px; 9259 font-weight: 600; 9260 margin: 0; 9261 display: flex; 9262 align-items: center; 9263 gap: 8px; 9264} 9265 9266.point-header .balance { 9267 font-size: 32px; 9268 font-weight: 700; 9269} 9270 9271.point-value { 9272 font-size: 14px; 9273 opacity: 0.9; 9274 margin: 8px 0 16px; 9275} 9276 9277.point-stats { 9278 display: flex; 9279 gap: 20px; 9280 font-size: 13px; 9281 opacity: 0.85; 9282 margin-bottom: 16px; 9283} 9284 9285.btn-view-history { 9286 display: inline-block; 9287 padding: 10px 20px; 9288 background: rgba(255, 255, 255, 0.2); 9289 color: white; 9290 text-decoration: none; 9291 border-radius: 8px; 9292 font-size: 14px; 9293 font-weight: 500; 9294 transition: background 0.2s; 9295} 9296 9297.btn-view-history:hover { 9298 background: rgba(255, 255, 255, 0.3); 9299} 9300 9301/* =================================== 9302 CHECKOUT POINT REDEMPTION 9303 =================================== */ 9304 9305.checkout-points { 9306 padding: 16px; 9307 background: #fef3c7; 9308 border: 2px solid #fbbf24; 9309 border-radius: 12px; 9310 margin-bottom: 16px; 9311} 9312 9313.points-header { 9314 display: flex; 9315 align-items: center; 9316 gap: 10px; 9317 margin-bottom: 12px; 9318} 9319 9320.points-header input[type="checkbox"] { 9321 width: 18px; 9322 height: 18px; 9323 cursor: pointer; 9324} 9325 9326.points-header label { 9327 font-size: 15px; 9328 color: #78350f; 9329 cursor: pointer; 9330 margin: 0; 9331} 9332 9333#points-slider { 9334 padding: 16px; 9335 background: white; 9336 border-radius: 8px; 9337 margin-top: 12px; 9338} 9339 9340#points-slider label { 9341 display: block; 9342 font-size: 14px; 9343 color: #374151; 9344 margin-bottom: 8px; 9345 font-weight: 500; 9346} 9347 9348#points-amount { 9349 width: 100%; 9350 margin-bottom: 8px; 9351} 9352 9353#points-display { 9354 display: flex; 9355 justify-content: space-between; 9356 font-size: 14px; 9357 color: #111827; 9358 margin-top: 8px; 9359} 9360 9361#points-display .discount-amount { 9362 color: #059669; 9363 font-weight: 600; 9364} 9365 9366.price-row.point-discount { 9367 color: #059669; 9368 font-weight: 500; 9369} 9370 9371/* =================================== 9372 REVIEWABLE ORDERS 9373 =================================== */ 9374 9375.reviewable-orders { 9376 background: white; 9377 border: 1px solid #e5e7eb; 9378 border-radius: 12px; 9379 padding: 20px; 9380 margin-bottom: 24px; 9381} 9382 9383.reviewable-orders h3 { 9384 font-size: 18px; 9385 margin-bottom: 16px; 9386 color: #111827; 9387} 9388 9389.order-card { 9390 display: flex; 9391 gap: 16px; 9392 padding: 16px; 9393 background: #f9fafb; 9394 border-radius: 8px; 9395 margin-bottom: 12px; 9396} 9397 9398.order-card:last-child { 9399 margin-bottom: 0; 9400} 9401 9402.order-card img { 9403 width: 80px; 9404 height: 80px; 9405 object-fit: cover; 9406 border-radius: 8px; 9407} 9408 9409.order-info { 9410 flex: 1; 9411} 9412 9413.order-info h4 { 9414 font-size: 15px; 9415 margin: 0 0 4px; 9416 color: #111827; 9417} 9418 9419.order-info p { 9420 font-size: 13px; 9421 color: #6b7280; 9422 margin: 0 0 12px; 9423} 9424 9425.btn-review { 9426 padding: 8px 16px; 9427 background: #059669; 9428 color: white; 9429 border: none; 9430 border-radius: 6px; 9431 font-size: 14px; 9432 font-weight: 500; 9433 cursor: pointer; 9434 transition: background 0.2s; 9435} 9436 9437.btn-review:hover { 9438 background: #047857; 9439} 9440 9441/* =================================== 9442 TOAST NOTIFICATIONS 9443 =================================== */ 9444 9445.review-toast { 9446 position: fixed; 9447 bottom: 24px; 9448 right: 24px; 9449 padding: 14px 20px; 9450 background: #111827; 9451 color: white; 9452 border-radius: 8px; 9453 font-size: 14px; 9454 line-height: 1.4; 9455 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9456 transform: translateY(100px); 9457 opacity: 0; 9458 transition: all 0.3s ease; 9459 z-index: 10000; 9460 max-width: 400px; 9461 word-wrap: break-word; 9462} 9463 9464.review-toast.show { 9465 transform: translateY(0); 9466 opacity: 1; 9467} 9468 9469/* Mobile: Position above bottom-nav */ 9470@media (max-width: 768px) { 9471 .review-toast { 9472 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9473 right: 16px; 9474 left: 16px; 9475 max-width: calc(100% - 32px); 9476 } 9477} 9478 9479.review-toast.toast-success { 9480 background: #059669; 9481} 9482 9483.review-toast.toast-error { 9484 background: #dc2626; 9485} 9486 9487.review-toast.toast-info { 9488 background: #2563eb; 9489} 9490 9491/* =================================== 9492 RESPONSIVE 9493 =================================== */ 9494 9495@media (max-width: 768px) { 9496 .point-balance-card { 9497 border-radius: 0; 9498 } 9499 9500 .review-stats { 9501 grid-template-columns: 70px 1fr; 9502 gap: 20px; 9503 padding: 16px; 9504 } 9505 9506 .rating-summary { 9507 text-align: center; 9508 } 9509 9510 .rating-summary .avg-rating { 9511 font-size: 32px; 9512 } 9513 9514 .rating-summary .stars { 9515 font-size: 18px; 9516 margin: 4px 0; 9517 } 9518 9519 .total-reviews { 9520 font-size: 11px; 9521 } 9522 9523 /* Rating breakdown - more compact */ 9524 .rating-breakdown { 9525 gap: 2px; 9526 } 9527 9528 .rating-breakdown .rating-bar { 9529 gap: 4px; 9530 } 9531 9532 .rating-breakdown .rating-label { 9533 font-size: 11px; 9534 width: 16px; 9535 min-width: 16px; 9536 text-align: left; 9537 } 9538 9539 .rating-breakdown .bar-container { 9540 height: 6px; 9541 } 9542 9543 .rating-breakdown .rating-count { 9544 font-size: 11px; 9545 width: 16px; 9546 min-width: 16px; 9547 } 9548 9549 .review-header { 9550 flex-direction: column; 9551 gap: 8px; 9552 align-items: flex-start; 9553 } 9554 9555 .review-rating { 9556 order: -1; 9557 margin-bottom: 4px; 9558 } 9559 9560 .review-rating .stars { 9561 font-size: 14px; 9562 } 9563 9564 .review-filters { 9565 flex-direction: column; 9566 gap: 12px; 9567 align-items: stretch; 9568 } 9569 9570 .review-sort { 9571 width: 100%; 9572 } 9573 9574 .point-header .balance { 9575 font-size: 24px; 9576 } 9577 9578 .point-stats { 9579 flex-direction: column; 9580 gap: 8px; 9581 } 9582 9583 .order-card { 9584 flex-direction: column; 9585 } 9586 9587 .order-card img { 9588 width: 100%; 9589 height: 200px; 9590 } 9591} 9592 9593/* =================================== 9594 LOADING & ERROR STATES 9595 =================================== */ 9596 9597.review-loading { 9598 text-align: center; 9599 padding: 40px; 9600 color: #9ca3af; 9601 font-size: 14px; 9602} 9603 9604.review-error { 9605 text-align: center; 9606 padding: 40px; 9607 color: #dc2626; 9608 font-size: 14px; 9609 background: #fee2e2; 9610 border-radius: 8px; 9611} 9612 9613.review-empty { 9614 text-align: center; 9615 padding: 10px; 9616 color: #6b7280; 9617 font-size: 14px; 9618} 9619 9620/* =================================== 9621 REVIEWABLE ORDERS LIST 9622 =================================== */ 9623 9624.reviewable-orders-list { 9625 display: flex; 9626 flex-direction: column; 9627 gap: 16px; 9628} 9629 9630.reviewable-order-item { 9631 display: grid; 9632 grid-template-columns: 80px 1fr auto; 9633 gap: 16px; 9634 padding: 16px; 9635 background: #fff; 9636 border: 1px solid #e5e7eb; 9637 border-radius: 12px; 9638 align-items: center; 9639 transition: box-shadow 0.2s; 9640} 9641 9642.reviewable-order-item:hover { 9643 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9644} 9645 9646.reviewable-order-item .roi-image { 9647 width: 80px; 9648 height: 80px; 9649 object-fit: cover; 9650 border-radius: 8px; 9651 border: 1px solid #e5e7eb; 9652} 9653 9654.reviewable-order-item .roi-info { 9655 display: flex; 9656 flex-direction: column; 9657 gap: 4px; 9658} 9659 9660.reviewable-order-item .roi-info h4 { 9661 margin: 0; 9662 font-size: 15px; 9663 font-weight: 600; 9664 color: #111827; 9665 line-height: 1.4; 9666} 9667 9668.reviewable-order-item .roi-meta { 9669 margin: 0; 9670 font-size: 13px; 9671 color: #6b7280; 9672} 9673 9674.reviewable-order-item .btn-write-review { 9675 white-space: nowrap; 9676 padding: 10px 20px; 9677 font-size: 14px; 9678} 9679 9680@media (max-width: 640px) { 9681 .reviewable-order-item { 9682 grid-template-columns: 60px 1fr; 9683 gap: 12px; 9684 } 9685 9686 .reviewable-order-item .roi-image { 9687 width: 60px; 9688 height: 60px; 9689 } 9690 9691 .reviewable-order-item .btn-write-review { 9692 grid-column: 1 / -1; 9693 width: 100%; 9694 } 9695} 9696 9697/* =================================== 9698 REVIEW SUBMISSION MODAL 9699 =================================== */ 9700
9701/* Modal Overlay Background */ 9702#review-modal-overlay { 9703 position: fixed; 9704 top: 0; 9705 left: 0; 9706 right: 0; 9707 bottom: 0; 9708 background: rgba(0, 0, 0, 0.6) !important; 9709 backdrop-filter: blur(8px); 9710 display: flex; 9711 align-items: center; 9712 justify-content: center; 9713 z-index: 99999 !important; 9714 padding: 20px; 9715} 9716 9717.review-modal { 9718 max-width: 600px !important; 9719 width: 90% !important; 9720 background: white; 9721 border-radius: 16px !important; 9722 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9723 max-height: 90vh; 9724 display: flex; 9725 flex-direction: column; 9726 overflow: hidden !important; 9727} 9728 9729#review-modal-overlay .modal-content, 9730.review-modal .modal-content { 9731 padding: 0 !important; 9732 flex: initial !important; 9733 overflow-y: visible !important; 9734} 9735 9736.review-modal .modal-header { 9737 display: flex; 9738 justify-content: space-between; 9739 align-items: center; 9740 padding: 20px 24px; 9741 border-bottom: 1px solid #e5e7eb; 9742} 9743 9744.review-modal .modal-title { 9745 margin: 0; 9746 font-size: 20px; 9747 font-weight: 700; 9748 color: #111827; 9749} 9750 9751.review-modal .modal-close { 9752 background: none; 9753 border: none; 9754 cursor: pointer; 9755 padding: 4px; 9756 color: #6b7280; 9757 transition: color 0.2s; 9758 display: flex; 9759 align-items: center; 9760 justify-content: center; 9761} 9762
9763.review-modal .modal-close:hover { 9764 color: #111827; 9765} 9766 9767.review-modal .modal-body { 9768 padding: 24px; 9769 max-height: calc(90vh - 140px); 9770 overflow-y: auto; 9771} 9772 9773/* Custom Scrollbar for Modal */ 9774.review-modal .modal-body::-webkit-scrollbar { 9775 width: 8px; 9776} 9777 9778.review-modal .modal-body::-webkit-scrollbar-track { 9779 background: #f1f5f9; 9780 border-radius: 4px; 9781} 9782 9783.review-modal .modal-body::-webkit-scrollbar-thumb { 9784 background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); 9785 border-radius: 4px; 9786} 9787 9788.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9789 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9790} 9791 9792/* Firefox Scrollbar */ 9793.review-modal .modal-body { 9794 scrollbar-width: thin; 9795 scrollbar-color: #3b82f6 #f1f5f9; 9796} 9797 9798.review-modal .modal-footer { 9799 display: flex; 9800 gap: 12px; 9801 margin-top: 24px; 9802 padding-top: 20px; 9803 border-top: 1px solid #f1f5f9; 9804} 9805 9806.review-product-info { 9807 display: flex; 9808 gap: 16px; 9809 padding: 16px; 9810 background: #f9fafb; 9811 border-radius: 8px; 9812 margin-bottom: 24px; 9813} 9814 9815.review-product-thumb { 9816 width: 80px; 9817 height: 80px; 9818 object-fit: cover; 9819 border-radius: 6px; 9820 border: 1px solid #e5e7eb; 9821} 9822 9823.review-product-details h4 { 9824 margin: 0 0 8px 0; 9825 font-size: 16px; 9826 font-weight: 600; 9827 color: #111827; 9828} 9829 9830.review-product-details p { 9831 margin: 0; 9832 font-size: 13px; 9833 color: #6b7280; 9834} 9835 9836/* Star Rating Input */ 9837.star-rating-input { 9838 display: inline-flex; 9839 gap: 8px; 9840 margin-right: 12px; 9841 vertical-align: middle; 9842} 9843 9844.star-btn { 9845 background: none; 9846 border: none; 9847 font-size: 32px; 9848 color: #d1d5db; 9849 cursor: pointer; 9850 transition: all 0.2s; 9851 padding: 0; 9852 line-height: 1; 9853} 9854 9855.star-btn:hover, 9856.star-btn.active { 9857 color: #fbbf24; 9858 transform: scale(1.1); 9859} 9860 9861.rating-label { 9862 display: inline-block; 9863 font-size: 15px; 9864 color: #6b7280; 9865 margin: 0; 9866 vertical-align: middle; 9867 min-width: 150px; 9868} 9869 9870/* Form Elements */ 9871.review-form .form-group { 9872 margin-bottom: 20px; 9873} 9874 9875.review-form .form-label { 9876 display: block; 9877 font-size: 14px; 9878 font-weight: 600; 9879 color: #374151; 9880 margin-bottom: 8px; 9881} 9882 9883.review-form .form-control { 9884 width: 100%; 9885 padding: 12px; 9886 border: 1px solid #d1d5db; 9887 border-radius: 8px; 9888 font-size: 14px; 9889 font-family: inherit; 9890 transition: border-color 0.2s; 9891} 9892 9893.review-form .form-control:focus { 9894 outline: none; 9895 border-color: #2563eb; 9896 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 9897} 9898 9899.review-form .form-hint { 9900 font-size: 13px; 9901 color: #6b7280; 9902 margin-top: 8px; 9903} 9904 9905/* Image Upload */ 9906.btn-upload-trigger { 9907 display: inline-flex; 9908 align-items: center; 9909 gap: 8px; 9910 padding: 10px 16px; 9911 background: #f9fafb; 9912 border: 2px dashed #d1d5db; 9913 border-radius: 8px; 9914 color: #374151; 9915 cursor: pointer; 9916 font-size: 14px; 9917 font-weight: 500; 9918 transition: all 0.2s; 9919} 9920 9921.btn-upload-trigger:hover { 9922 background: #f3f4f6; 9923 border-color: #2563eb; 9924 color: #2563eb; 9925} 9926 9927.image-preview-grid { 9928 display: grid; 9929 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9930 gap: 12px; 9931 margin-top: 16px; 9932} 9933 9934.image-preview-item { 9935 position: relative; 9936 aspect-ratio: 1; 9937 border-radius: 8px; 9938 overflow: hidden; 9939 border: 1px solid #e5e7eb; 9940} 9941 9942.image-preview-item img { 9943 width: 100%; 9944 height: 100%; 9945 object-fit: cover; 9946} 9947 9948.remove-image { 9949 position: absolute; 9950 top: 4px; 9951 right: 4px; 9952 background: rgba(0, 0, 0, 0.7); 9953 border: none; 9954 color: white; 9955 width: 24px; 9956 height: 24px; 9957 border-radius: 50%; 9958 cursor: pointer; 9959 display: flex; 9960 align-items: center; 9961 justify-content: center; 9962 transition: background 0.2s; 9963} 9964 9965.remove-image:hover { 9966 background: rgba(0, 0, 0, 0.9); 9967} 9968 9969/* Point Reward Info */ 9970.point-reward-info { 9971 background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); 9972 border: 2px solid #fbbf24; 9973 border-radius: 12px; 9974 padding: 16px; 9975 margin: 20px 0; 9976} 9977 9978.pri-header { 9979 display: flex; 9980 align-items: center; 9981 gap: 8px; 9982 font-size: 16px; 9983 font-weight: 700; 9984 color: #92400e; 9985 margin-bottom: 12px; 9986} 9987 9988.pri-list { 9989 list-style: none; 9990 padding: 0; 9991 margin: 0 0 12px 0; 9992} 9993 9994.pri-list li { 9995 padding: 6px 0; 9996 font-size: 14px; 9997 color: #78350f; 9998 transition: opacity 0.2s; 9999} 10000 10001.pri-total { 10002 padding-top: 12px; 10003 border-top: 2px solid rgba(251, 191, 36, 0.3); 10004 font-size: 16px; 10005 color: #92400e; 10006 font-weight: 600; 10007} 10008 10009.form-error { 10010 background: #fee2e2; 10011 border: 1px solid #f87171; 10012 color: #991b1b; 10013 padding: 12px; 10014 border-radius: 8px; 10015 font-size: 14px; 10016 margin-top: 16px; 10017} 10018 10019/* Buttons */ 10020.review-form .btn { 10021 padding: 12px 24px; 10022 border: none; 10023 border-radius: 8px; 10024 font-size: 15px; 10025 font-weight: 600; 10026 cursor: pointer; 10027 transition: all 0.2s; 10028} 10029 10030.review-form .btn-primary { 10031 background: #2563eb; 10032 color: white; 10033} 10034 10035.review-form .btn-primary:hover { 10036 background: #1d4ed8; 10037} 10038 10039.review-form .btn-primary:disabled { 10040 background: #94a3b8;
10041 cursor: not-allowed; 10042} 10043 10044.review-form .btn-secondary { 10045 background: #f1f5f9; 10046 color: #64748b; 10047} 10048 10049.review-form .btn-secondary:hover { 10050 background: #e2e8f0; 10051 color: #475569; 10052} 10053 10054@media (max-width: 768px) { 10055 #review-modal-overlay { padding: 0; } 10056 .review-modal { max-width: 100%; width: 100% !important; } 10057} 10058 10059/* =================================== 10060 POINT SUMMARY GRID (Profile Page) 10061 =================================== */ 10062 10063.point-summary-grid { 10064 display: grid; 10065 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 10066 gap: 12px; 10067 margin-bottom: 32px; 10068} 10069 10070.point-summary-card { 10071 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 10072 border: 1px solid #e5e7eb; 10073 border-radius: 12px; 10074 padding: 16px 20px; 10075 display: flex; 10076 align-items: center; 10077 justify-content: space-between; 10078 gap: 12px; 10079 transition: all 0.2s; 10080} 10081 10082.point-summary-card:hover { 10083 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10084 transform: translateY(-2px); 10085} 10086 10087.point-summary-card .psc-left { 10088 display: flex; 10089 flex-direction: column; 10090 gap: 2px; 10091} 10092 10093.point-summary-card .psc-icon { 10094 font-size: 24px; 10095 margin-bottom: 4px; 10096} 10097 10098.point-summary-card .psc-label { 10099 font-size: 13px; 10100 color: #6b7280; 10101 font-weight: 500; 10102 text-transform: uppercase; 10103 letter-spacing: 0.5px; 10104} 10105 10106.point-summary-card .psc-value { 10107 font-size: 26px; 10108 font-weight: 700; 10109 color: #111827; 10110 flex-shrink: 0; 10111} 10112 10113/* Point History Section */ 10114.point-history-section { 10115 margin-top: 32px; 10116} 10117 10118.point-history-section h3 { 10119 font-size: 18px; 10120 font-weight: 600; 10121 color: #111827; 10122 margin-bottom: 16px; 10123} 10124 10125.point-history-list { 10126 display: flex; 10127 flex-direction: column; 10128 gap: 12px; 10129} 10130 10131.point-history-item { 10132 display: grid; 10133 grid-template-columns: 40px 1fr auto; 10134 gap: 16px; 10135 padding: 16px; 10136 background: white; 10137 border: 1px solid #e5e7eb; 10138 border-radius: 10px; 10139 align-items: center; 10140 transition: all 0.2s; 10141} 10142 10143.point-history-item:hover { 10144 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10145} 10146 10147.point-history-item.credit { 10148 border-left: 3px solid #10b981; 10149} 10150 10151.point-history-item.debit { 10152 border-left: 3px solid #f59e0b; 10153} 10154 10155.point-history-item .phi-icon { 10156 width: 40px; 10157 height: 40px; 10158 display: flex; 10159 align-items: center; 10160 justify-content: center; 10161 font-size: 20px; 10162 border-radius: 50%; 10163 background: #f9fafb; 10164} 10165 10166.point-history-item.credit .phi-icon { 10167 background: #d1fae5; 10168} 10169 10170.point-history-item.debit .phi-icon { 10171 background: #fef3c7; 10172} 10173
10174.point-history-item .phi-details { 10175 display: flex; 10176 flex-direction: column; 10177 gap: 4px; 10178} 10179 10180.point-history-item .phi-desc { 10181 font-size: 15px; 10182 font-weight: 500; 10183 color: #111827; 10184} 10185 10186.point-history-item .phi-date { 10187 font-size: 13px; 10188 color: #9ca3af; 10189} 10190 10191.point-history-item .phi-amount { 10192 font-size: 18px; 10193 font-weight: 700; 10194} 10195 10196.point-history-item .phi-amount.credit { 10197 color: #10b981; 10198} 10199 10200.point-history-item .phi-amount.debit { 10201 color: #f59e0b; 10202} 10203 10204@media (max-width: 640px) { 10205 .point-summary-grid { 10206 grid-template-columns: 1fr; 10207 } 10208 10209 .point-summary-card .psc-value { 10210 font-size: 22px; 10211 } 10212 10213 .point-history-item { 10214 grid-template-columns: 36px 1fr; 10215 gap: 12px; 10216 } 10217 10218 .point-history-item .phi-amount { 10219 grid-column: 2; 10220 text-align: right; 10221 margin-top: 8px; 10222 } 10223} 10224 10225/* ââ Pending points: compact widget indicator ââ */ 10226.pbc-pending { 10227 font-size: 12px; 10228 opacity: 0.85; 10229 margin-top: 4px; 10230 color: #fde68a; 10231} 10232 10233/* ââ Pending points: summary card ââ */ 10234.point-summary-card.psc-pending-card { 10235 background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); 10236 border-color: #fcd34d; 10237} 10238 10239.point-summary-card.psc-pending-card .psc-value { 10240 color: #b45309; 10241} 10242 10243.point-summary-card .psc-sub { 10244 font-size: 11px; 10245 color: #9ca3af; 10246 margin-top: 4px; 10247} 10248 10249/* ââ Pending points: history item ââ */ 10250.point-history-item.phi-pending { 10251 opacity: 0.75; 10252 border-left-color: #fcd34d; 10253 background: #fffbeb; 10254} 10255 10256.point-history-item.phi-pending .phi-icon { 10257 background: #fef3c7; 10258} 10259 10260.phi-pending-badge { 10261 display: inline-block; 10262 font-size: 11px; 10263 font-weight: 600; 10264 padding: 2px 7px; 10265 border-radius: 10px; 10266 background: #fef3c7; 10267 color: #92400e; 10268 border: 1px solid #fcd34d; 10269 margin-left: 8px; 10270 vertical-align: middle; 10271} 10272 10273/* ââ Reversed points: history item ââ */ 10274.point-history-item.phi-reversed { 10275 opacity: 0.55; 10276 border-left-color: #d1d5db; 10277 background: #f9fafb; 10278} 10279 10280.point-history-item.phi-reversed .phi-icon { 10281 background: #f3f4f6; 10282} 10283 10284.point-history-item.phi-reversed .phi-amount { 10285 text-decoration: line-through; 10286 color: #9ca3af !important; 10287} 10288 10289.phi-reversed-badge { 10290 display: inline-block; 10291 font-size: 11px; 10292 font-weight: 600; 10293 padding: 2px 7px; 10294 border-radius: 10px; 10295 background: #fee2e2; 10296 color: #991b1b; 10297 border: 1px solid #fca5a5; 10298 margin-left: 8px; 10299 vertical-align: middle; 10300} 10301 10302</style> 10303 10304</head> 10305<body> 10306<header class="topbar"> 10307 <div class="container topbar-inner"> 10308 <div class="topbar-left"> 10309 LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL 10310 </div> 10311 <div class="topbar-right"> 10312 <a href="https://loklokpro.com/">IWANTOGEL</a> 10313 <a href="https://loklokpro.com/">TOTO SLOT</a> 10314 <a href="https://loklokpro.com/">TOTO TOGEL</a> 10315 <a href="https://loklokpro.com/">SITUS PALING MUDAH MENANG</a> 10316 </div> 10317 </div> 10318</header> 10319<nav class="navbar"> 10320 <nav class="navbar-logo" style="width: 100%;"> 10321 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10322 <a href="https://loklokpro.com/" class="logo-mobile" title="IWANTOGEL"> 10323 <img src="https://area-terpercaya.b-cdn.net/newlogo.gif" alt="IWANTOGEL" height="40"> 10324 </a> 10325 </div> 10326</nav> 10327 <div class="container nav-inner"> 10328 <a href="https://loklokpro.com/" class="logo" title="IWANTOGEL"> 10329 <img src="https://area-terpercaya.b-cdn.net/newlogo.gif" alt="IWANTOGEL" height="40"> 10330 </a> 10331 10332 10333 <form class="search" method="get" action="#"> 10334 <input type="hidden" name="page" value="produk"> 10335 10336 <!-- Search Icon (Desktop) --> 10337 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10338 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10339 <circle cx="11" cy="11" r="8"/> 10340 <path d="m21 21-4.35-4.35"/> 10341 </svg> 10342 </button> 10343 10344 <input type="text" name="q" id="searchInput" placeholder="Cari kami Di google : Iwantogel.com" 10345 value="" autocomplete="off"> 10346 10347 <div class="search-suggest" id="searchSuggest" hidden></div> 10348 </form> 10349 10350 <div class="nav-actions"> 10351 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10352 <button class="icon-btn cart-indicator" id="cartBtn" title="Keranjang"> 10353 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10354 </button> 10355 10356 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10357 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10358 </button> 10359 10360 <!-- Dynamic login/logout button --> 10361 <button class="login-btn" id="loginBtn" aria-label="Masuk">
10362 <span class="login-text">Masuk</span> 10363 <span class="login-ico" aria-hidden="true">ð</span> 10364 </button> 10365 </div> 10366 </div> 10367 10368 <div class="container" style="display: flex; align-items: center;"> 10369 10370 10371 <div class="mega-cats-wrapper"> 10372 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10373 <a class="" href="https://loklokpro.com/"> 10374 IWANTOGEL </a> 10375 <a class="" href="https://loklokpro.com/"> 10376 TOTO SLOT ONLINE </a> 10377 <a class="" href="https://loklokpro.com/"> 10378 TOTO TOGEL ONLINE </a> 10379 <a class="" href="https://loklokpro.com/"> 10380 SLOT GACOR MUDAH MENANG </a> 10381 <a class="" href="https://loklokpro.com/"> 10382 BONUS MELIMPAH </a> 10383 <a class="" href="https://loklokpro.com/"> 10384 CLAIM SEKARANG JUGA </a> 10385 </div> 10386 <div class="mega-cats-fade" id="megaCatsFade"></div> 10387 </div> 10388 </div> 10389</nav> 10390 10391<!-- Panel Mega Menu - Outside navbar for full-screen --> 10392<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10393 <div class="mm2-view is-active" data-mm2="top"></div> 10394 <div class="mm2-view" data-mm2="detail"></div> 10395</div> 10396 10397<!-- Backdrop - Outside navbar for full-screen --> 10398<div class="mm2-backdrop" data-mm2="backdrop"></div> 10399 10400<!-- Mobile Bottom Navigation --> 10401<nav class="bottom-nav"> 10402 <div class="bottom-nav-inner"> 10403 <!-- Home --> 10404 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10405 <div class="bottom-nav-item-icon"> 10406 <!-- Outlined icon (default) --> 10407 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10408 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10409 <polyline points="9 22 9 12 15 12 15 22"/> 10410 </svg> 10411 <!-- Filled icon (active) --> 10412 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10413 <path d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 010 1.06l-.03.03a.75.75 0 01-1.06 0l-8.16-8.16-8.16 8.16a.75.75 0 01-1.06 0l-.03-.03a.75.75 0 010-1.06l8.69-8.69z"/> 10414 <path d="M12 5.5l-7.5 7.5v8.25c0 .414.336.75.75.75h3.75v-6h6v6h3.75a.75.75 0 00.75-.75V13l-7.5-7.5z"/> 10415 </svg> 10416 </div> 10417 <span class="bottom-nav-item-label">Home</span> 10418 </a> 10419 10420 <!-- Kategori --> 10421 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10422 <div class="bottom-nav-item-icon"> 10423 <!-- Outlined icon (default) --> 10424 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10425 <line x1="3" y1="12" x2="21" y2="12"/> 10426 <line x1="3" y1="6" x2="21" y2="6"/> 10427 <line x1="3" y1="18" x2="21" y2="18"/> 10428 </svg> 10429 <!-- Filled icon (active) --> 10430 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10431 <path d="M3 5.25A.75.75 0 013.75 4.5h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 5.25zM3 12a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 12zm0 6.75a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75a.75.75 0 01-.75-.75z"/> 10432 </svg> 10433 </div> 10434 <span class="bottom-nav-item-label">Kategori</span> 10435 </button> 10436 10437 <!-- Keranjang --> 10438 <button class="bottom-nav-item" id="bottomCartBtn"> 10439 <div class="bottom-nav-item-icon"> 10440 <!-- Outlined icon (default) --> 10441 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10442 <circle cx="9" cy="21" r="1"/> 10443 <circle cx="20" cy="21" r="1"/> 10444 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10445 </svg> 10446 <!-- Filled icon (active) -->
10447 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10448 <path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"/> 10449 </svg> 10450 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10451 </div> 10452 <span class="bottom-nav-item-label">Keranjang</span> 10453 </button> 10454 10455 <!-- Compare --> 10456 <button class="bottom-nav-item" id="bottomCompareBtn"> 10457 <div class="bottom-nav-item-icon"> 10458 <!-- Outlined icon (default) --> 10459 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10460 <polyline points="17 11 21 7 17 3"/> 10461 <path d="M21 7H9"/> 10462 <polyline points="7 21 3 17 7 13"/> 10463 <path d="M15 17H3"/> 10464 </svg> 10465 <!-- Filled icon (active) --> 10466 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10467 <path d="M21.75 6.75a.75.75 0 00-.75-.75H9a.75.75 0 000 1.5h10.19L15.22 11.47a.75.75 0 001.06 1.06l4.5-4.5a.75.75 0 00.22-.53.75.75 0 00-.22-.53l-.03-.03zM8.78 12.47L4.28 16.97a.75.75 0 000 1.06l4.5 4.5a.75.75 0 001.06-1.06L6.06 18H15a.75.75 0 000-1.5H4.81l3.97-3.97a.75.75 0 00-1.06-1.06z"/> 10468 </svg> 10469 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10470 </div> 10471 <span class="bottom-nav-item-label">Compare</span> 10472 </button> 10473 10474 <!-- Chat - Always show, check login in JavaScript --> 10475 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10476 <div class="bottom-nav-item-icon"> 10477 <!-- Outlined icon (default) --> 10478 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10479 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10480 </svg> 10481 <!-- Filled icon (active) --> 10482 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10483 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10484 </svg> 10485 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10486 </div> 10487 <span class="bottom-nav-item-label">Chat</span> 10488 </button> 10489 10490 <!-- Akun --> 10491 <button class="bottom-nav-item" id="bottomLoginBtn"> 10492 <div class="bottom-nav-item-icon"> 10493 <!-- Outlined icon (default) --> 10494 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10495 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10496 <circle cx="12" cy="7" r="4"/> 10497 </svg> 10498 <!-- Filled icon (active) --> 10499 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10500 <path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/> 10501 </svg> 10502 </div> 10503 <span class="bottom-nav-item-label">Login</span> 10504 </button> 10505 </div> 10506</nav> 10507 10508<!-- Mobile User Menu Bottom Sheet --> 10509
10510<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10511(function(){ 10512 const btn = document.getElementById('megaMoreBtn'); 10513 const dd = document.getElementById('megaMoreDropdown'); 10514 if(!btn || !dd) return; 10515 10516 function openDD(){ 10517 dd.hidden = false; 10518 btn.setAttribute('aria-expanded','true'); 10519 document.addEventListener('click', outside, {capture:true}); 10520 document.addEventListener('keydown', esc); 10521 } 10522 function closeDD(){ 10523 if(dd.hidden) return; 10524 dd.hidden = true; 10525 btn.setAttribute('aria-expanded','false'); 10526 document.removeEventListener('click', outside, {capture:true}); 10527 document.removeEventListener('keydown', esc); 10528 } 10529 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10530 function outside(e){ 10531 if(!dd.contains(e.target) && e.target !== btn){ 10532 closeDD(); 10533 } 10534 } 10535 function esc(e){ 10536 if(e.key === 'Escape'){ 10537 closeDD(); 10538 btn.focus(); 10539 } 10540 } 10541 btn.addEventListener('click', toggle); 10542})(); 10543 10544// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10545(function(){ 10546 const megaCats = document.getElementById('megaCatsScroll'); 10547 const fade = document.getElementById('megaCatsFade'); 10548 if (!megaCats || !fade) return; 10549 10550 function updateFade() { 10551 const scrollLeft = megaCats.scrollLeft; 10552 const scrollWidth = megaCats.scrollWidth; 10553 const clientWidth = megaCats.clientWidth; 10554 const maxScroll = scrollWidth - clientWidth; 10555 10556 // Hide fade when scrolled to end (within 10px tolerance) 10557 if (maxScroll - scrollLeft <= 10) { 10558 fade.style.opacity = '0'; 10559 } else { 10560 fade.style.opacity = '1'; 10561 } 10562 } 10563 10564 // Update on scroll 10565 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10566 10567 // Update on resize 10568 window.addEventListener('resize', updateFade, { passive: true }); 10569 10570 // Initial check 10571 setTimeout(updateFade, 100); 10572})(); 10573 10574document.addEventListener('DOMContentLoaded', function () { 10575 var input = document.getElementById('searchInput'); 10576 if (!input) return; 10577 input.addEventListener('keydown', function (e) { 10578 if (e.key === 'Enter') { 10579 e.preventDefault(); 10580 var form = input.form; 10581 if (!form) return; 10582 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10583 else form.submit(); 10584 } 10585 }); 10586}); 10587 10588 10589(function(){ 10590 const cartBtn = document.getElementById('cartBtn'); 10591 if (cartBtn) { 10592 cartBtn.addEventListener('click', function(e) { 10593 e.preventDefault(); 10594 10595 const checkLogin = () => { 10596 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10597 }; 10598 10599 if (checkLogin()) { 10600 window.location.href = '/keranjang'; 10601 } else { 10602 showCartLoginModal(); 10603 } 10604 }); 10605 } 10606 10607 const loginBtn = document.getElementById('loginBtn'); 10608 if (loginBtn) { 10609 loginBtn.addEventListener('click', function(e) { 10610 e.preventDefault(); 10611 window.location.href = 'https://loklokpro.com/'; 10612 }); 10613 } 10614 10615 const userMenuBtn = document.getElementById('userMenuBtn'); 10616 const userDropdown = document.getElementById('userDropdown'); 10617 10618 if (userMenuBtn && userDropdown) { 10619 function openDropdown() { 10620 userDropdown.hidden = false; 10621 userMenuBtn.setAttribute('aria-expanded', 'true'); 10622 document.addEventListener('click', closeOnOutside, { capture: true }); 10623 document.addEventListener('keydown', closeOnEscape); 10624 } 10625 10626 function closeDropdown() { 10627 if (userDropdown.hidden) return; 10628 userDropdown.hidden = true; 10629 userMenuBtn.setAttribute('aria-expanded', 'false'); 10630 document.removeEventListener('click', closeOnOutside, { capture: true }); 10631 document.removeEventListener('keydown', closeOnEscape); 10632 } 10633 10634 function closeOnOutside(e) { 10635 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10636 closeDropdown(); 10637 } 10638 } 10639 10640 function closeOnEscape(e) { 10641 if (e.key === 'Escape') { 10642 closeDropdown(); 10643 userMenuBtn.focus(); 10644 } 10645 } 10646 10647 userMenuBtn.addEventListener('click', function(e) { 10648 e.preventDefault(); 10649 userDropdown.hidden ? openDropdown() : closeDropdown(); 10650 }); 10651 10652 // Close dropdown when clicking profile links 10653 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10654 link.addEventListener('click', function() { 10655 closeDropdown(); 10656 }); 10657 }); 10658 } 10659 10660 // Bottom nav profile button - toggle mobile bottom sheet 10661 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10662 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10663 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10664 10665 if (bottomProfileBtn && mobileUserSheet) { 10666 function openMobileSheet() { 10667 mobileUserSheet.hidden = false;
10668 document.body.style.overflow = 'hidden'; 10669 // Trigger animation 10670 requestAnimationFrame(() => { 10671 mobileUserSheet.classList.add('active'); 10672 }); 10673 } 10674 10675 function closeMobileSheet() { 10676 mobileUserSheet.classList.remove('active'); 10677 document.body.style.overflow = ''; 10678 setTimeout(() => { 10679 mobileUserSheet.hidden = true; 10680 }, 300); 10681 } 10682 10683 bottomProfileBtn.addEventListener('click', function(e) { 10684 e.preventDefault(); 10685 openMobileSheet(); 10686 }); 10687 10688 if (mobileUserSheetBackdrop) { 10689 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10690 } 10691 10692 // Close on escape key 10693 document.addEventListener('keydown', function(e) { 10694 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10695 closeMobileSheet(); 10696 } 10697 }); 10698 10699 // Close sheet when clicking profile links 10700 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10701 link.addEventListener('click', function() { 10702 closeMobileSheet(); 10703 }); 10704 }); 10705 } 10706 10707 // Handle Google avatar error in bottom-nav (fallback to initials) 10708 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10709 if (bottomNavAvatar) { 10710 bottomNavAvatar.addEventListener('error', function() { 10711 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10712 const initialsDiv = document.createElement('div'); 10713 initialsDiv.className = 'bottom-nav-avatar-initials'; 10714 initialsDiv.textContent = initials; 10715 this.parentNode.replaceChild(initialsDiv, this); 10716 }); 10717 } 10718 10719 // Mobile logout button 10720 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10721 if (mobileLogoutBtn) { 10722 mobileLogoutBtn.addEventListener('click', async function(e) { 10723 e.preventDefault(); 10724 10725 // Check push notification before logout 10726 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10727 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10728 if (!confirmLogout) { 10729 return; // User cancelled logout 10730 } 10731 } 10732 10733 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10734 10735 try { 10736 const response = await fetch('/auth/logout', { 10737 method: 'POST', 10738 credentials: 'same-origin', 10739 headers: { 10740 'Content-Type': 'application/json', 10741 'X-CSRF-Token': getCsrfToken() 10742 }, 10743 body: JSON.stringify({ 10744 csrf_token: getCsrfToken() 10745 }) 10746 }); 10747 10748 if (response.ok) { 10749 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10750 window.guestCart.clear(); 10751 } 10752 window.location.href = '/login?logged_out=1'; 10753 } else { 10754 console.error('Logout failed'); 10755 window.location.href = '/logout'; 10756 } 10757 } catch (error) { 10758 console.error('Logout error:', error); 10759 window.location.href = '/logout'; 10760 } 10761 }); 10762 } 10763 10764 const logoutBtn = document.getElementById('logoutBtn'); 10765 if (logoutBtn) { 10766 logoutBtn.addEventListener('click', async function(e) { 10767 e.preventDefault(); 10768 10769 // Check push notification before logout 10770 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10771 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10772 if (!confirmLogout) { 10773 return; // User cancelled logout 10774 } 10775 } 10776 10777 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10778 10779 try { 10780 const response = await fetch('/auth/logout', { 10781 method: 'POST', 10782 credentials: 'same-origin', 10783 headers: { 10784 'Content-Type': 'application/json', 10785 'X-CSRF-Token': getCsrfToken() 10786 }, 10787 body: JSON.stringify({ 10788 csrf_token: getCsrfToken() 10789 }) 10790 }); 10791 10792 if (response.ok) { 10793 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10794 window.guestCart.clear(); 10795 } 10796 10797 window.location.href = '/login?logged_out=1'; 10798 } else { 10799 console.error('Logout failed'); 10800 window.location.href = '/logout'; 10801 } 10802 } catch (error) { 10803 console.error('Logout error:', error); 10804 window.location.href = '/logout'; 10805 } 10806 }); 10807 } 10808 10809 const wishlistBtn = document.getElementById('wishlistBtn'); 10810 const notifBtn = document.getElementById('notifBtn'); 10811 10812 if (wishlistBtn) { 10813 wishlistBtn.addEventListener('click', function(e) { 10814 e.preventDefault(); 10815 alert('Fitur wishlist akan segera tersedia!'); 10816 }); 10817 } 10818 10819 if (notifBtn) { 10820 notifBtn.addEventListener('click', function(e) { 10821 e.preventDefault(); 10822 alert('Fitur notifikasi akan segera tersedia!'); 10823 }); 10824 } 10825})(); 10826 10827// ========== MOBILE BOTTOM NAVIGATION ========== 10828(function initBottomNav() { 10829 // Sync cart badge with bottom nav 10830 function syncBottomCartBadge() { 10831 const mainBadge = document.getElementById('cartCount'); 10832 const bottomBadge = document.getElementById('bottomCartCount'); 10833 if (mainBadge && bottomBadge) { 10834 const count = mainBadge.textContent; 10835 bottomBadge.textContent = count; 10836 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10837 } 10838 } 10839 10840 // Sync compare badge with bottom nav 10841 function syncBottomCompareBadge() { 10842 const mainBadge = document.getElementById('compareCount'); 10843 const bottomBadge = document.getElementById('bottomCompareCount'); 10844 if (mainBadge && bottomBadge) { 10845 const count = mainBadge.textContent; 10846 bottomBadge.textContent = count; 10847 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10848 } 10849 } 10850 10851 // Initial sync 10852 syncBottomCartBadge(); 10853 syncBottomCompareBadge(); 10854 10855 // Observe badge changes 10856 const observer = new MutationObserver(function() { 10857 syncBottomCartBadge(); 10858 syncBottomCompareBadge(); 10859 }); 10860 10861 const cartBadge = document.getElementById('cartCount'); 10862 const compareBadge = document.getElementById('compareCount'); 10863 10864 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10865 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10866 10867 // Bottom Cart Button 10868 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10869 if (bottomCartBtn) { 10870 bottomCartBtn.addEventListener('click', function(e) { 10871 e.preventDefault(); 10872 10873 const checkLogin = () => { 10874 return window.isUserLoggedIn ? window.isUserLoggedIn() : false;
10875 }; 10876 10877 if (checkLogin()) { 10878 window.location.href = '/keranjang'; 10879 } else { 10880 showCartLoginModal(); 10881 } 10882 }); 10883 } 10884 10885 // Bottom Compare Button 10886 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10887 if (bottomCompareBtn) { 10888 bottomCompareBtn.addEventListener('click', function(e) { 10889 e.preventDefault(); 10890 10891 // Close chat widget if open 10892 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10893 window.plazaChat.closeWidget(); 10894 } 10895 10896 const mainCompareBtn = document.getElementById('compareBtn'); 10897 if (mainCompareBtn) { 10898 mainCompareBtn.click(); 10899 } 10900 }); 10901 } 10902 10903 // Bottom Category Button - triggers mega menu 10904 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10905 if (bottomCategoryBtn) { 10906 bottomCategoryBtn.addEventListener('click', function(e) { 10907 e.preventDefault(); 10908 // Trigger desktop mega menu 10909 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10910 if (desktopTrigger) { 10911 desktopTrigger.click(); 10912 } 10913 }); 10914 } 10915 10916 // Bottom Login Button 10917 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10918 if (bottomLoginBtn) { 10919 bottomLoginBtn.addEventListener('click', function(e) { 10920 e.preventDefault(); 10921 window.location.href = '/login'; 10922 }); 10923 } 10924 10925 // Bottom Chat Button 10926 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10927 if (bottomChatBtn) { 10928 bottomChatBtn.addEventListener('click', function(e) { 10929 e.preventDefault(); 10930 10931 // Trigger chat widget (try multiple times if needed) 10932 const triggerChat = () => { 10933 const chatToggle = document.getElementById('chat-toggle-btn'); 10934 if (chatToggle) { 10935 chatToggle.click(); 10936 return true; 10937 } 10938 return false; 10939 }; 10940 10941 // Immediate attempt 10942 if (!triggerChat()) { 10943 // Retry after 100ms if not found 10944 setTimeout(() => { 10945 if (!triggerChat()) { 10946 // Last resort: manually show popup 10947 const popup = document.getElementById('chat-popup'); 10948 if (popup) { 10949 popup.style.display = 'block'; 10950 } 10951 } 10952 }, 100); 10953 } 10954 }); 10955 } 10956 10957 // Sync chat unread badge 10958 function syncBottomChatBadge() { 10959 const chatBadge = document.getElementById('chat-unread-badge'); 10960 const bottomChatBadge = document.getElementById('bottomChatCount'); 10961 if (chatBadge && bottomChatBadge) { 10962 const count = chatBadge.textContent; 10963 bottomChatBadge.textContent = count; 10964 bottomChatBadge.style.display = chatBadge.style.display; 10965 } 10966 } 10967 10968 // Observe chat badge changes 10969 const chatBadge = document.getElementById('chat-unread-badge'); 10970 if (chatBadge) { 10971 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10972 syncBottomChatBadge(); 10973 } 10974 10975 // Active state management based on current page 10976 function updateActiveState() { 10977 const currentPath = window.location.pathname; 10978 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10979 10980 bottomNavItems.forEach(item => { 10981 item.classList.remove('active'); 10982 10983 const itemId = item.id; 10984 const page = item.getAttribute('data-page'); 10985 const href = item.getAttribute('href'); 10986 10987 // Check by ID for specific pages 10988 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10989 item.classList.add('active'); 10990 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10991 item.classList.add('active'); 10992 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10993 item.classList.add('active'); 10994 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10995 item.classList.add('active'); 10996 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10997 item.classList.add('active'); 10998 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 10999 item.classList.add('active'); 11000 } 11001 // Fallback: check by data-page attribute 11002 else if (page === 'home' && currentPath === '/') { 11003 item.classList.add('active'); 11004 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 11005 item.classList.add('active'); 11006 } 11007 // Fallback: check by href 11008 else if (href && currentPath === href) { 11009 item.classList.add('active'); 11010 } 11011 }); 11012 } 11013 11014 // Update active state on load 11015 updateActiveState(); 11016 11017 // Bottom nav always visible - auto-hide disabled for better UX 11018 // const bottomNav = document.querySelector('.bottom-nav'); 11019 // if (bottomNav && window.innerWidth <= 640) { 11020 // // Auto-hide feature disabled 11021 // } 11022})(); 11023// Cart Login Modal Handler (Global scope) 11024function showCartLoginModal() { 11025 const modal = document.getElementById('cartLoginModal'); 11026 if (!modal) return; 11027 11028 modal.classList.add('show'); 11029 11030 const confirmBtn = document.getElementById('cartLoginConfirm'); 11031 const registerBtn = document.getElementById('cartRegisterBtn'); 11032 const cancelBtn = document.getElementById('cartLoginCancel'); 11033 11034 const handleLogin = () => { 11035 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 11036 }; 11037 11038 const handleRegister = () => { 11039 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 11040 }; 11041 11042 const handleCancel = () => { 11043 modal.classList.remove('show'); 11044 confirmBtn.removeEventListener('click', handleLogin); 11045 registerBtn.removeEventListener('click', handleRegister); 11046 cancelBtn.removeEventListener('click', handleCancel); 11047 }; 11048 11049 confirmBtn.removeEventListener('click', handleLogin); 11050 registerBtn.removeEventListener('click', handleRegister); 11051 cancelBtn.removeEventListener('click', handleCancel); 11052 11053 confirmBtn.addEventListener('click', handleLogin); 11054 registerBtn.addEventListener('click', handleRegister); 11055 cancelBtn.addEventListener('click', handleCancel); 11056 11057 modal.addEventListener('click', (e) => { 11058 if (e.target === modal) { 11059 handleCancel(); 11060 } 11061 }); 11062} 11063</script>
vendor: 1 bytes, line 11063
11063
11064<script data-inlined="cart-badge.js"> 11065(function(){ 11066 function selBadge(){ 11067 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 11068 } 11069 function setBadge(n){ 11070 const el = selBadge(); 11071 if (!el) return; 11072 const count = Math.max(0, parseInt(n||0,10) || 0); 11073 el.textContent = String(count); 11074 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 11075 if (count <= 0) { el.classList.add('is-empty'); } 11076 else { el.classList.remove('is-empty'); } 11077 } 11078 async function refresh(){ 11079 try{ 11080 // Primary: Use server-side endpoint (handles both logged and guest users) 11081 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11082 if (res.ok) { 11083 const data = await res.json(); 11084 if (data && data.ok) { 11085 setBadge(data.items_count || 0); 11086 return; 11087 } 11088 } 11089 11090 // Fallback hanya untuk guest user: check localStorage 11091 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11092 document.body.classList.contains('logged-in') || 11093 window.userLoggedIn === true; 11094 11095 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11096 setBadge(window.guestCart.getItemsCount()); 11097 } 11098 }catch(e){ 11099 // Final fallback hanya untuk guest 11100 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11101 if (!isLoggedIn) { 11102 try { 11103 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11104 const items = guestCartData.items || {}; 11105 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11106 setBadge(count); 11107 } catch(e2) { 11108 setBadge(0); 11109 } 11110 } else { 11111 setBadge(0); 11112 } 11113 } 11114 } 11115 11116 // Ekspos untuk dipanggil dari skrip lain (opsional) 11117 window.CartBadge = { set: setBadge, refresh }; 11118 window.updateCartCounter = refresh; // Alias for compatibility 11119 11120 // Update saat halaman load 11121 if (document.readyState === 'loading') { 11122 document.addEventListener('DOMContentLoaded', refresh); 11123 } else { 11124 refresh(); 11125 } 11126 11127 // Dengarkan event global dari add/remove/update 11128 document.addEventListener('cart:updated', function(ev){ 11129 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11130 ? ev.detail.items_count : null; 11131 if (c !== null) setBadge(c); 11132 else refresh(); 11133 }); 11134 11135 // Dengarkan event khusus cart remove untuk update badge 11136 document.addEventListener('cart:removed', function(ev){ 11137 // Langsung refresh untuk memastikan data terbaru 11138 refresh(); 11139 }); 11140 11141 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11142 const prevAdd = window.addToCart; 11143 if (typeof prevAdd === 'function'){ 11144 window.addToCart = async function(pid, qty, vid, meta){ 11145 const res = await prevAdd(pid, qty, vid, meta); 11146 try{ 11147 const cnt = res && res.cart ? res.cart.items_count : null; 11148 if (cnt !== null) setBadge(cnt); 11149 // tetap siarkan event agar listener lain ikut sync 11150 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11151 }catch(_){} 11152 return res; 11153 }; 11154 } 11155 11156 // Helper function to check if user is logged in 11157 function isUserLoggedIn() { 11158 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11159 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11160 } 11161 11162 // Make login check globally accessible 11163 window.isUserLoggedIn = isUserLoggedIn; 11164})(); 11165</script>
11165 11166 11167<!-- Product Compare Sidebar --> 11168<div class="compare-sidebar" id="compareSidebar"> 11169 <div class="compare-header"> 11170 <h4 class="compare-title"> 11171 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11172 <path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"/> 11173 </svg> 11174 Bandingkan Produk 11175 <span class="compare-count" id="compareCountSidebar">(0)</span> 11176 </h4> 11177 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11178 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11179 <path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7a1 1 0 1 0-1.4 1.4L10.6 12l-4.9 4.9a1 1 0 0 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z"/> 11180 </svg> 11181 </button> 11182 </div> 11183 11184 <div class="compare-content"> 11185 <div class="compare-empty" id="compareEmpty"> 11186 <div class="empty-icon"> 11187 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11188 <path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"/> 11189 </svg> 11190 </div> 11191 <h5>Belum ada produk dipilih</h5> 11192 <p>Pilih produk yang ingin dibandingkan dengan klik tombol "Bandingkan Produk" di halaman produk.</p> 11193 </div> 11194 11195 <div class="compare-list" id="compareList"> 11196 <!-- Products will be populated here --> 11197 </div> 11198 </div> 11199 11200 <div class="compare-actions" id="compareActions"> 11201 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11202 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11203 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 11204 </svg> 11205 Hapus Semua 11206 </button> 11207 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11208 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11209 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/> 11210 </svg> 11211 Analisis AI 11212 </button> 11213 </div> 11214 11215 <!-- AI Disclaimer in Sidebar --> 11216 <div class="compare-disclaimer"> 11217 <div class="ai-disclaimer"> 11218 <div class="disclaimer-text"> 11219 <strong>ð¡ Catatan:</strong> Analisis ini dihasilkan secara otomatis dan dapat memiliki keterbatasan. 11220 Gunakan sebagai referensi tambahan dengan tetap mempertimbangkan kebutuhan Anda. 11221 Untuk informasi lebih lanjut, silakan hubungi tim IWANTOGEL. 11222 </div> 11223 </div> 11224 </div> 11225</div><style data-inlined="pd-lightbox.css"> 11226/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11227.pd-lb { 11228 position: fixed; inset: 0; z-index: 99999; 11229 background: rgba(0,0,0,.95); 11230 display: none; 11231 color: #fff; 11232 /* Transition untuk swipe-down feedback */ 11233 transition: transform .18s ease, opacity .18s ease; 11234} 11235.pd-lb.show { display: block; } 11236 11237/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11238.pd-lb-canvas { 11239 position: absolute; inset: 56px 0 56px 0; 11240 display: flex; align-items: center; justify-content: center; 11241 overflow: hidden; 11242 touch-action: none; 11243 z-index: 1; 11244} 11245 11246.pd-lb-img { 11247 max-width: 96vw; max-height: 86vh; 11248 will-change: transform; 11249 user-select: none; 11250 pointer-events: none; 11251 transform-origin: 50% 50%; 11252} 11253 11254.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11255 position: absolute; 11256 z-index: 2; 11257} 11258 11259.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11260 background: rgba(17,24,39,.7); 11261 color: #fff; border: 0; cursor: pointer; 11262 width: 40px; height: 40px; border-radius: 999px; 11263 display: inline-flex; align-items: center; justify-content: center; 11264 transition: background-color .15s ease, transform .05s ease; 11265 -webkit-tap-highlight-color: transparent; 11266} 11267.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11268.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11269 11270.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11271.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11272.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11273 11274.pd-lb-counter { 11275 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11276 transform: translateX(-50%); 11277 font-weight: 700; font-size: 13px; letter-spacing: .4px; 11278 background: rgba(17,24,39,.65); 11279 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(0,0,0,.1); 11280} 11281 11282@media (max-width: 640px){ 11283 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11284 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11285 .pd-lb-close { width: 38px; height: 38px; } 11286} 11287</style> 11288<style>.paylater-promo{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#f0fdf4 0,#dcfce7 100%);border:1px solid #86efac;border-radius:8px;padding:8px 12px;font-size:13px;line-height:1.4;margin:8px 0}.paylater-promo-sm{padding:6px 10px;font-size:11px;gap:6px}.paylater-promo-md{padding:8px 12px;font-size:13px;gap:8px}.paylater-promo-lg{padding:10px 16px;font-size:14px;gap:10px}.paylater-icon{font-size:1.2em;flex-shrink:0}.paylater-text{display:flex;flex-direction:column;gap:2px}.paylater-text strong{color:#15803d;font-weight:600}.paylater-text small{color:#16a34a;font-size:.85em;opacity:.9}.paylater-desktop-only{display:none}.paylater-mobile-only{display:flex}@media (min-width:641px){.paylater-desktop-only{display:flex}.paylater-mobile-only{display:none}}@media (max-width:640px){.paylater-promo{font-size:12px;padding:7px 10px}.paylater-promo-lg{font-size:13px;padding:8px 12px}}@media print{.paylater-promo{display:none!important}}.paylater-compact{display:inline-block;font-size:11px;font-weight:600;color:#059669;background:#d1fae5;padding:3px 8px;border-radius:4px;margin:6px 0 4px;line-height:1.3}@media (max-width:640px){.paylater-compact{font-size:10px;padding:2px 6px;margin:5px 0 3px}}@media print{.paylater-compact{display:none!important}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.pd-voucher-minimal{margin:10px 0;display:flex;flex-direction:column;gap:6px}.pdv-min-item{display:flex;align-items:center;gap:8px;padding:0;height:32px;transition:all .2s ease}.pdv-min-hidden{display:none}.pdv-min-disabled{opacity:.5}.pdv-badge-btn{display:inline-flex;align-items:center;gap:8px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border:none;border-radius:8px;padding:3px;
11288cursor:pointer;transition:all .2s ease;width:auto}.pdv-badge-btn:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,.3)}.pdv-badge-btn:disabled{opacity:.6;cursor:not-allowed}.pdv-badge-icon{color:#fff;flex-shrink:0}.pdv-badge-text{font-size:14px;font-weight:700;color:#fff;white-space:nowrap}.pdv-badge-code{font-family:'Courier New',monospace;font-size:13px;font-weight:700;color:#fff;background:rgba(0,0,0,.2);padding:4px 8px;border-radius:4px;border:none;white-space:nowrap}.pdv-min-copy,.pdv-min-more{padding:4px 12px;font-size:12px;font-weight:600;border-radius:5px;border:1px solid #e2e8f0;background:#fff;color:#667eea;cursor:pointer;transition:all .2s ease;white-space:nowrap}.pdv-min-copy:hover:not(:disabled),.pdv-min-more:hover{background:#667eea;color:#fff;border-color:#667eea}.pdv-min-copy:disabled{opacity:.4;cursor:not-allowed}.pdv-min-copy.copied{background:#22c55e;color:#fff;border-color:#22c55e}.pdv-min-more{margin-left:auto}.modal-close{position:absolute;top:20px;right:20px;background:none;border:none;cursor:pointer;color:#64748b;padding:4px;transition:color .2s ease}.modal-close:hover{color:#1e293b}.modal-body{margin-bottom:8px}.voucher-modal-content{text-align:center}.voucher-modal-badge{display:inline-flex;flex-direction:row;align-items:center;gap:6px;background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:10px;padding:8px 16px;margin-bottom:12px}.vmb-value{font-size:18px;font-weight:700;color:#fff;line-height:1}.vmb-label{font-size:18px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.5px}.voucher-modal-code{font-family:'Courier New',monospace;font-size:14px;font-weight:700;color:#667eea;background:rgba(102,126,234,.1);padding:6px 12px;border-radius:6px;border:2px dashed rgba(102,126,234,.3);display:inline-block;margin-bottom:10px}.voucher-modal-name{font-size:14px;font-weight:600;color:#1e293b;margin-bottom:16px}.voucher-modal-details{text-align:left;background:#f8fafc;border-radius:8px;padding:12px;margin-bottom:16px}.vmd-item{display:flex;align-items:flex-start;gap:8px;padding:6px 0;font-size:13px;color:#64748b;border-bottom:1px solid #e2e8f0}.vmd-item:last-child{border-bottom:none}.vmd-item svg{color:#667eea;flex-shrink:0;margin-top:2px}.vmd-item strong{color:#1e293b}.modal-actions{padding:20px}.modal-btn-primary{background:linear-gradient(135deg,#667eea 0,#764ba2 100%);color:#fff}.modal-btn-primary:hover{opacity:.9;transform:translateY(-1px)}@media (max-width:640px){.pdv-min-item{gap:6px}.pdv-badge-btn{padding:3px;gap:6px}.pdv-badge-text{font-size:13px}.pdv-badge-code{font-size:12px;padding:3px 6px}.pdv-min-copy,.pdv-min-more{padding:3px 10px;font-size:11px}.modal-dialog{max-width:95%;margin:20px}.vmb-value{font-size:16px}.vmb-label{font-size:16px}.voucher-modal-code{font-size:13px;padding:5px 10px}}</style><style nonce="se3iyzEHMc9FQEB4wSyd0w==">.promo-endyear-mini-banner{display:flex;align-items:center;gap:12px;padding:12px 16px;background:rgba(0,0,0,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(242,140,26,.2);border-radius:12px;margin:16px 0;box-shadow:0 4px 16px rgba(0,0,0,.08);transition:all .3s ease}.promo-endyear-mini-banner:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.12);border-color:rgba(242,140,26,.4)}.pey-icon{font-size:32px;line-height:1;flex-shrink:0}.pey-content{flex:1;min-width:0}.pey-content strong{display:block;font-size:14px;font-weight:700;color:#F28C1A;margin-bottom:2px}.pey-content span{display:block;font-size:13px;color:#64748b;line-height:1.4}.pey-link{flex-shrink:0;padding:8px 16px;background:linear-gradient(135deg,#F28C1A,#FFB84D);color:#fff;text-decoration:none;font-size:13px;font-weight:700;border-radius:8px;white-space:nowrap;transition:all .2s ease;box-shadow:0 2px 8px rgba(242,140,26,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(242,140,26,.35)}@media(max-width:640px){.promo-endyear-mini-banner{flex-wrap:wrap;gap:8px;padding:10px 12px}.pey-icon{font-size:28px;align-self:flex-start}.pey-content{flex:1;min-width:0}.pey-content strong{font-size:12px}.pey-content span{font-size:11px;line-height:1.3}.pey-link{width:100%;flex-basis:100%;font-size:11px;padding:6px 12px;text-align:center}}@media print{.promo-endyear-mini-banner{display:none!important}}</style> 11289<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11290.pd-title-row{position:relative;margin-bottom:12px;padding-right:52px}.pd-title{margin:0!important}.pd-share-btn{position:absolute;top:0;right:0;width:40px;height:40px;
11290display:inline-flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid #e2e8f0;background:#fff;color:#64748b;cursor:pointer;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.pd-share-btn:hover{background:#f8fafc;border-color:#cbd5e1;color:#0f172a;transform:translateY(-1px)}.pd-share-btn:active{transform:translateY(0)}.pd-share-btn svg{display:block} 11291 11292.pd-share-modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.75);backdrop-filter:blur(4px);z-index:99999;display:none;align-items:center;justify-content:center;padding:0;animation:fadeIn .2s ease}.pd-share-modal-overlay.show{display:flex}.pd-share-modal{background:#fff;border-radius:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:480px;width:100%;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}} 11293 11294.pd-share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f1f5f9}.pd-share-title{font-size:18px;font-weight:700;color:#0f172a;margin:0}.pd-share-close{width:32px;height:32px;border-radius:8px;border:0;background:transparent;color:#64748b;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s ease}.pd-share-close:hover{background:#f1f5f9;color:#0f172a}.pd-share-body{padding:24px;overflow-y:auto} 11295 11296.pd-share-preview{display:flex;gap:12px;padding:16px;background:#f8fafc;border-radius:12px;margin-bottom:24px}.pd-share-preview-img{width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0;border:1px solid #e2e8f0}.pd-share-preview-info{flex:1;min-width:0}.pd-share-preview-name{font-size:14px;font-weight:600;color:#0f172a;margin:0 0 4px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.pd-share-preview-price{font-size:16px;font-weight:700;color:#0ea5e9;margin:0} 11297 11298.pd-share-label{font-size:13px;font-weight:600;color:#475569;margin:0 0 12px 0;text-transform:uppercase;letter-spacing:.5px}.pd-share-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}.pd-share-option{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 8px;border:1.5px solid #e2e8f0;border-radius:12px;background:#fff;cursor:pointer;transition:all .2s ease;text-decoration:none;color:inherit}.pd-share-option:hover{border-color:#94a3b8;background:#f8fafc;transform:translateY(-2px)}.pd-share-option:active{transform:translateY(0)} 11299 11300.pd-share-icon{width:40px;height:40px;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;position:relative}.pd-share-icon svg{width:24px;height:24px;display:block}.pd-share-icon-whatsapp{background:linear-gradient(135deg,#25D366 0%,#128C7E 100%);color:#fff}.pd-share-icon-facebook{background:linear-gradient(135deg,#1877F2 0%,#0c63d4 100%);color:#fff}.pd-share-icon-twitter{background:linear-gradient(135deg,#1DA1F2 0%,#0d8bd9 100%);color:#fff}.pd-share-icon-telegram{background:linear-gradient(135deg,#0088cc 0%,#0077b3 100%);color:#fff}.pd-share-icon-email{background:linear-gradient(135deg,#EA4335 0%,#c5331f 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#f1db63 0%,#e5d146 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11301 11302.pd-share-copy-section{background:#f8fafc;padding:16px;border-radius:12px}.pd-share-copy-input-wrap{display:flex;gap:8px}.pd-share-copy-input{flex:1;padding:12px 14px;border:1px solid #e2e8f0;border-radius:10px;font-size:14px;color:#0f172a;background:#fff;font-family:'Segoe UI',system-ui,sans-serif}.pd-share-copy-input:focus{outline:0;border-color:#0ea5e9;box-shadow:0 0 0 3px rgba(14,165,233,.1)}.pd-share-copy-btn{padding:12px 20px;border:0;border-radius:10px;background:linear-gradient(135deg,#0ea5e9 0%,#0284c7 100%);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}.pd-share-copy-btn:hover{background:linear-gradient(135deg,#0284c7 0%,#0369a1 100%);transform:translateY(-1px);box-shadow:0 4px 12px rgba(14,165,233,.3)}
11302.pd-share-copy-btn:active{transform:translateY(0)}.pd-share-copy-btn svg{width:16px;height:16px}.pd-share-copy-btn.copied{background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);pointer-events:none} 11303 11304@media(max-width:640px){.pd-share-modal{border-radius:20px 20px 0 0;margin-top:auto;max-height:85vh}.pd-share-grid{grid-template-columns:repeat(3,1fr)}.pd-share-header{padding:16px 20px}.pd-share-body{padding:20px}.pd-share-btn{width:36px;height:36px}.pd-share-btn svg{width:18px;height:18px}}@media(max-width:480px){.pd-share-grid{grid-template-columns:repeat(3,1fr);gap:10px}.pd-share-option{padding:12px 6px}.pd-share-icon{width:36px;height:36px}.pd-share-icon svg{width:20px;height:20px}.pd-share-name{font-size:11px}} 11305 11306.mf-share{background:#fff;color:#fb923c;border:2px solid #fb923c;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11307 11308@media(max-width:640px){.pd-share-btn{display:none!important}} 11309 11310.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#821603;color:#ffffff;border-radius:999px;padding:4px 10px;font-weight:700} 11311 11312.stock-info-btn{--c:#0ea5e9;--b:#bae6fd;--bg:#f0f9ff;--bg-h:#e0f2fe;--b-h:#0ea5e9;margin-left:0;display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;padding:4px 10px;border-radius:999px;border:1.5px solid var(--b);background:var(--bg);color:var(--c);cursor:pointer;font-size:12px;font-weight:600;transition:all .2s ease;-webkit-tap-highlight-color:transparent}.stock-info-btn:hover{background:var(--bg-h);border-color:var(--b-h);color:#0284c7;transform:translateY(-1px);box-shadow:0 2px 8px rgba(14,165,233,.15)}.stock-info-btn:active{transform:translateY(0)}.stock-info-btn:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(14,165,233,.2)}.stock-info-btn svg{width:14px;height:14px;flex-shrink:0}.stock-info-btn .btn-text{white-space:nowrap} 11313 11314.stock-popover{position:absolute;z-index:9999;min-width:280px;max-width:92vw;background:#000000;color:#eef3ff;border:1px solid rgba(0,0,0,.08);border-radius:14px;box-shadow:0 20px 40px rgba(12,12,20,.45);padding:12px 12px 8px;display:none}.stock-popover.show{display:block}.stock-popover::before{content:"";position:absolute;top:-8px;left:24px;width:14px;height:14px;background:#000000;border-left:1px solid rgba(0,0,0,.08);border-top:1px solid rgba(0,0,0,.08);transform:rotate(45deg)}.sp-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.sp-title{font-weight:800;font-size:14px}.sp-close{background:transparent;border:0;color:#98a2b3;cursor:pointer} 11315 11316.sp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.sp-item{display:grid;grid-template-columns:1fr auto 32px;column-gap:12px;align-items:center;padding:10px 10px;min-height:44px;border-radius:10px;background:rgba(0,0,0,.03)}.sp-left{display:flex;align-items:center;gap:10px}.sp-dot{width:10px;height:10px;border-radius:999px;background:var(--dot,#60a5fa);display:inline-block}.sp-label{font-weight:600;font-size:13px} 11317 11318 11319.sp-qty{grid-column:2;justify-self:end;min-width:40px;text-align:right;font-weight:800;font-size:13px;color:#cfe4ff;font-variant-numeric:tabular-nums} 11320 11321.sp-wa{ 11322 grid-column:3; justify-self:end; 11323 width:32px; height:32px; min-width:32px; min-height:32px; 11324 box-sizing:border-box; 11325 padding:0; line-height:0; text-decoration:none; outline:0; 11326 display:inline-flex; align-items:center; justify-content:center; 11327 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11328 background:rgba(37,211,102,.08); color:#25D366; 11329 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11330 -webkit-tap-highlight-color:transparent; 11331} 11332.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11333.sp-wa:active{transform:translateY(.5px) scale(.98)} 11334 11335.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11336.stock-popover a{text-decoration:none} 11337 11338#deskripsi img{max-width:450px!important;height:unset!important}@media(max-width:640px){.stock-popover{position:fixed;left:0;right:0;bottom:max(12px,env(safe-area-inset-bottom,12px));margin:0 10px;padding:14px 12px 10px;border-radius:16px}.stock-popover::before{display:none}.stock-info-btn{padding:4px 8px;gap:4px;border:0;background:transparent}.stock-info-btn:hover{background:transparent;box-shadow:none;transform:none}.stock-info-btn .btn-text{display:none}.stock-info-btn svg{width:16px;height:16px}.sp-item{grid-template-columns:1fr auto 36px}.sp-wa{width:36px;height:36px;min-width:36px;min-height:36px}.sp-wa svg{width:19px;height:19px}} 11339 11340.stock-dot{position:absolute;bottom:8px;right:8px;width:8px;height:8px;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,0.4);z-index:2;cursor:help}.stock-dot.ready{background:#10b981}.stock-dot.low{background:#f59e0b}.stock-dot.out{background:#ef4444}.stock-dot:hover{transform:scale(1.2);transition:transform 0.2s ease}@media(max-width:640px){.stock-dot{width:7px;height:7px;bottom:6px;right:6px}} 11341 11342.pd-related-card .spec-chips{font-size:11px;color:#64748b;margin:4px 0 6px 0;line-height:1.3;font-weight:500}.pd-related-card .spec-chips .spec-divider{margin:0 4px;opacity:0.6}@media(max-width:640px){.pd-related-card .spec-chips{font-size:10px;margin:3px 0 5px 0}#deskripsi img{max-width:300px!important;height:unset!important;justify-self:center!important}} 11343 11344.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#fbbf24 0%,#f59e0b 100%);color:#78350f;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11345.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border-radius:12px;border:2px solid #fbbf24}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#f59e0b;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#78350f}.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#92400e;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#92400e}.pwp-spinner{width:40px;height:40px;border:3px solid #fbbf24;border-top-color:#f59e0b;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11346.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #fbbf24;display:flex;align-items:center;padding:8px;gap:10px}.pwp-deal-card:hover{background:#fef3c7}.pwp-deal-thumb{flex-shrink:0;width:50px;height:50px;background:#f8fafc;overflow:hidden;border-radius:4px}.pwp-deal-thumb img{width:100%;height:100%;object-fit:cover}.pwp-deal-badge{background:#dc2626;color:#fff;padding:2px 5px;border-radius:3px;font-size:9px;font-weight:700;margin-left:auto}.pwp-deal-info{flex:1;display:flex;flex-direction:column;gap:4px}.pwp-deal-name{font-size:12px;font-weight:600;color:#0f172a;margin:0;line-height:1.2}.pwp-deal-prices{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.pwp-deal-price{font-size:14px;font-weight:800;color:#dc2626;white-space:nowrap}.pwp-deal-original{font-size:11px;color:#64748b;text-decoration:line-through;white-space:nowrap}.pwp-cart-note{margin-top:8px;padding:6px 10px;background:#e0f2fe;border-left:2px solid#0ea5e9;font-size:11px;color:#0c4a6e;border-radius:4px} 11347@media(max-width:640px){.pwp-deals-section{margin:12px 0;padding:10px;border-radius:10px}.pwp-deals-header{gap:8px}.pwp-text .pwp-title{font-size:13px}.pwp-text .pwp-subtitle{font-size:10px}.pwp-deal-card{padding:6px;gap:8px}.pwp-deal-thumb{width:45px;height:45px}.pwp-deal-name{font-size:11px}.pwp-deal-price{font-size:13px}.pwp-deal-original{font-size:10px}} 11348 11349.preorder-badge{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;background:linear-gradient(135deg,#8b5cf6 0%,#7c3aed 100%);color:#fff;border-radius:8px;font-size:13px;font-weight:700;border:0;box-shadow:0 2px 8px rgba(139,92,246,.25)}.preorder-badge svg{width:16px;height:16px;flex-shrink:0}.preorder-info{margin:12px 0;padding:14px;background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 100%);border:1.5px solid #c4b5fd;border-radius:12px}.preorder-info-header{display:flex;align-items:center;gap:8px;margin-bottom:10px}.preorder-info-icon{width:20px;height:20px;color:#7c3aed;flex-shrink:0}.preorder-info-title{font-size:14px;font-weight:700;color:#5b21b6;margin:0}.preorder-info-items{display:flex;flex-direction:column;gap:8px}.preorder-info-row{display:flex;align-items:center;justify-content:space-between;font-size:13px}.preorder-info-label{color:#6b21a8;font-weight:600}.preorder-info-value{color:#5b21b6;font-weight:700}.preorder-info-value.highlight{color:#dc2626;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #f59e0b;font-size:12px;color:#78350f;border-radius:6px;line-height:1.4}@media(max-width:640px){.preorder-badge{font-size:12px;padding:5px 10px}.preorder-info{margin:10px 0;padding:12px}.preorder-info-title{font-size:13px}.preorder-info-row{font-size:12px}.preorder-info-note{font-size:11px;padding:6px 8px}} 11350</style> 11351 11352<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="IWANTOGEL" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bers
11352ama IWANTOGEL </h2><div class="pd-sticky-price-row"><span class="pd-sticky-price" id="pd-sticky-price">Rp 5.000,-</span><span class="pd-sticky-price-original" id="pd-sticky-price-original">Rp 1.000.000</span><span class="pd-sticky-discount" id="pd-sticky-discount">-99.5%</span></div></div><div class="pd-sticky-actions"><button type="button" class="pd-sticky-share-btn" id="pd-sticky-share-btn" aria-label="Bagikan produk" title="Bagikan"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button><button type="button" class="pd-sticky-btn pd-sticky-btn-chat" id="pd-sticky-chat"><svg viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg>Tanya</button><button type="button" class="pd-sticky-btn pd-sticky-btn-cart" id="pd-sticky-add-cart" disabled>+ Keranjang</button><button type="button" class="pd-sticky-btn pd-sticky-btn-buy" id="pd-sticky-buy" disabled>Beli Sekarang</button></div></div></div> 11353 11354<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://loklokpro.com/">IWANTOGEL</a><span>-</span><a href="https://loklokpro.com/">TOTO SLOT ONLINE</a><span>-</span><a href="https://loklokpro.com/">TOTO TOGEL ONLINE</a><span>-</span><a href="https://loklokpro.com/">SLOT GACOR</a><span>-</span><a href="https://loklokpro.com/">BONUS MELIMPAH</a><span>-</span><span class="current"> 11355 11356</span></nav> 11357 11358<div class="pd-top-layout"><section class="pd-media-card"><figure class="pd-main-image" id="pd-main-figure"><img id="pd-main-img" src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOTO TOGEL" data-slug="" loading="eager"> 11359</figure> 11360 11361<div class="pd-mobile-slider-wrapper"> 11362<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="eager"></div><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11363<div class="pd-thumb-gallery" id="pd-thumb-gallery" aria-label="Galeri gambar produk"><button type="button" class="pd-thumb-btn active" data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt="TOGEL ONLINE" data-slug="" aria-label="Gambar 1"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt
11363="TOGEL ONLINE" data-slug="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" aria-label="Gambar 2"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp" aria-label="Gambar 3"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp" aria-label="Gambar 4"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp" aria-label="Gambar 5"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" data-alt="TOGEL ONLINE" data-slug="lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp" aria-label="Gambar 6"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE" loading="lazy"></button></div></section><section class="pd-info-card"><div id="pd-variant-combined" class="pd-variant-combined" hidden></div><div class="pd-title-row"> 11364 <h1 class="pd-title" id="pd-title">LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL </h1><button type="button" class="pd-share-btn" id="pd-share-btn" aria-label="Bagikan produk" title="Bagikan"><svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button></div> 11365<div id="pd-price-anchor"> 11366</div> 11367<div class="pd-price-qr-container"> 11368 <div class="pd-price-block" id="pd-price-block"> 11369 <div class="pd-price-row"> 11370 <span class="pd-price-current" id="pd-price-current" >Rp 5.000</span> 11371 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11372 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11373 <div class="pd-meta-row"> 11374 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11375 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11376 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11377 <path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>Bisa PWP</span></div> 11378 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">TOTO SLOT</span> 11379 <span class="sku">SKU: <span id="pd-sku">TOTO TOGEL</span></span></div> 11380<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11381 <span class="paylater-icon">ð³</span> 11382 <span class="paylater-text"> 11383 <strong>TOGEL</strong> 11384 <small>ONLINE</small> 11385 </span> 11386</div> 11387 11388<div class="stock-popover" id="pd-stock-popover" role="dialog" aria-hidden="true" aria-label="Informasi stok cabang"><div class="sp-head"><div class="sp-title">Stok Cabang</div><button type="button" class="sp-close" id="pd-stock-popover-close" aria-label="Tutup"><svg width="18" height="18" viewBox="0 0 24 24">
11388<path fill="currentColor" d="M18.3 5.71L12 12.01l-6.3-6.3-1.4 1.41 6.3 6.3-6.3 6.3 1.4 1.41 6.3-6.3 6.3 6.3 1.41-1.41-6.3-6.3 6.3-6.3z"/></svg></button></div><ul class="sp-list"> 11389<li class="sp-item"><div class="sp-left"><span class="sp-dot" style="--dot:#60a5fa;"></span><span class="sp-label">Asia Mega Mas</span></div><span class="sp-qty" id="sp-qty-meg">0</span><a class="sp-wa" href="https://wa.me/6281380009898?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Asia%20Mega%20Mas%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" target="_blank" rel="noopener" aria-label="WhatsApp Asia Mega Mas"><svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/></svg></a></li> 11390 11391 <li class="sp-item"> 11392 <div class="sp-left"> 11393 <span class="sp-dot" style="--dot:#34d399;"></span> 11394 <span class="sp-label">Medan Fair</span> 11395 </div> 11396 <span class="sp-qty" id="sp-qty-mdf">0</span> 11397 <a class="sp-wa" 11398 href="https://wa.me/6285388992009?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Medan%20Fair%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" 11399 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11400 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11401 <path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/> 11402 </svg> 11403 </a> 11404 </li> 11405 11406 <li class="sp-item"> 11407 <div class="sp-left"> 11408 <span class="sp-dot" style="--dot:#f59e0b;"></span>
11409 <span class="sp-label">Banda Aceh</span> 11410 </div> 11411 <span class="sp-qty" id="sp-qty-bda">0</span> 11412 <a class="sp-wa" 11413 href="https://wa.me/628116833411?text=Halo%2C%20saya%20ingin%20tanya%20stok%20di%20Banda%20Aceh%20untuk%20produk%20https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" 11414 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11415 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11416 <path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/> 11417 </svg> 11418 </a> 11419 </li> 11420 </ul> 11421 </div> 11422 11423 </div> 11424 <div class="pd-qr-print" style="display:none;"><div class="pd-qr-label">Scan untuk lihat produk:</div><img src="https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https%3A%2F%2Fplazait.co.id%2Fproduct%2Flenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-black" alt="QR Code" class="pd-qr-image"></div> 11425 </div> 11426 <div id="pd-variants-anchor"></div> 11427 11428 11429 11430 <div class="pd-compare-section"> 11431 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11432 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11433 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11434 </svg> 11435 <span class="compare-text">Bandingkan Produk</span> 11436 <span class="compare-remove" hidden> 11437 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11438 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 11439 </svg> 11440 Hapus dari Perbandingan 11441 </span> 11442 </button> 11443 <div class="pd-compare-info"> 11444 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11445 </div> 11446 </div> 11447 11448 <div class="pd-short-desc"><p><strong>Informasi IWANTOGEL:</strong></p><ul><li>Minimal Deposit: Rp.5.000,-</li><li>Jenis Permainan: Slot , Togel , Casino , parlay/Mixparlay</li><li>Metode Pembayaran: Bank, QRIS, E-Wallet</li><li>Mata Uang: IDR (Indonesian Rupiah)</li><li>Jam Operasional: 24 Jam Nonstop</li><li>Total Rating: 957.845.986</li><li>Rating: âââââ</li><li>Daftar Sekarang juga: <a href="https://slot-gacor.loklokpro.com/">Klik Disini</a></li></ul></div><div class="IWANTOGEL-container-v1 mini"> 11449<div class="IWANTOGEL-container"> 11450 11451 <!-- HEADER --> 11452<div class="togel-container-v3 mini"> 11453 <div class="togel-machine-v3"> 11454 <h2 class="togel-title-v3">Simulasi Prediksi Togel</h2> 11455
11456 <div class="togel-controls-v3"> 11457 <label> 11458 PASARAN 11459 <select id="marketFilter"> 11460 <option value="ALL">ALL PASARAN</option> 11461 </select> 11462 </label> 11463 11464 <label> 11465 TIPE 11466 <select id="typeFilter"> 11467 <option value="4D">4D</option> 11468 <option value="3D">3D</option> 11469 <option value="2D">2D</option> 11470 </select> 11471 </label> 11472 11473 <label> 11474 MIN CONF 11475 <input id="minConf" type="number" min="50" max="99" step="1" value="70"> 11476 </label> 11477 11478 <button id="btnTogelRandom" type="button"> 11479 <span class="icon">ð²</span> GENERATE 11480 </button> 11481 </div> 11482 11483 <div class="togel-main-result-v3" id="togelResult"> 11484 <div class="togel-status-v3" id="togelStatus">â READY â</div> 11485 11486 <div class="togel-highlight-box"> 11487 <div class="togel-col-left"> 11488 <div class="k">Pasaran Rekomendasi</div> 11489 <div class="v-game" id="rMarket">-</div> 11490 11491 <div class="provider-info"> 11492 <span class="k-small">Tipe:</span> 11493 <span class="v-small" id="rType">-</span> 11494 <span class="k-small" style="margin-left:10px;">Conf:</span> 11495 <span class="v-small" id="rConf">-</span> 11496 </div> 11497 </div> 11498 11499 <div class="togel-col-right"> 11500 <div class="k-rtp">Angka Utama</div> 11501 <div class="v-rtp-huge" id="rAngka">-</div> 11502 </div> 11503 </div> 11504 11505 <div class="togel-details-v3"> 11506 <div class="togel-detail-item"> 11507 <div class="k-detail">BBFS</div> 11508 <div class="v-detail" id="rBBFS">-</div> 11509 </div> 11510 <div class="togel-detail-item"> 11511 <div class="k-detail">Angka Ikut</div> 11512 <div class="v-detail" id="rIkut">-</div> 11513 </div> 11514 <div class="togel-detail-item"> 11515 <div class="k-detail">Shio</div> 11516 <div class="v-detail" id="rShio">-</div> 11517 </div> 11518 </div> 11519 11520 <div class="togel-tip-v3" id="rTip"> 11521 Tips: Ini simulasi hiburan. Tetap bijak & disiplin. 11522 </div> 11523 </div> 11524 11525 <div class="togel-alts-v3"> 11526 <div class="togel-alts-title-v3">3 Alternatif Angka</div> 11527 <div class="togel-alt-list-v3" id="togelAltList"></div> 11528 </div> 11529 11530 <div class="togel-note-v3"> 11531 NB : Ini hanyalah Simulasi Untuk predisi Bukan Hasil keluaran Angka Togel. 11532 </div> 11533 </div> 11534 </div> 11535 11536 <style> 11537 @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11538 11539 /* =============================== 11540 WRAPPER 11541================================ */ 11542 .togel-container-v3 { 11543 display: flex; 11544 justify-content: center; 11545 padding: 16px 10px; 11546 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 11547 } 11548 11549 /* =============================== 11550 MACHINE CORE (RED ELEGANT) 11551================================ */ 11552 .togel-machine-v3 { 11553 position: relative; 11554 width: 100%; 11555 max-width: 520px; 11556 padding: 16px; 11557 border-radius: 16px; 11558 overflow: hidden; 11559 isolation: isolate; 11560 background: radial-gradient(circle at 15% 0%, rgba(161 0 0), transparent 48%), radial-gradient(circle at 85% 100%, rgba(180, 0, 0, 0.18), transparent 55%), linear-gradient(123deg, #000000, #821603); 11561 border: 1px solid rgba(185 0 0); 11562 box-shadow: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 11563 color: #dbdbdb; 11564 } 11565 11566 /* HUD SWEEP */ 11567 .togel-machine-v3::before { 11568 content: ""; 11569 position: absolute; 11570 inset: 0; 11571 background: linear-gradient(120deg, transparent 40%, rgba(255, 0, 0, .10), transparent 65%); 11572 animation: hudSweep 7s linear infinite; 11573 pointer-events: none; 11574 opacity: .8; 11575 } 11576 11577 /* =============================== 11578 TITLE 11579================================ */ 11580 .togel-title-v3 { 11581 text-align: center; 11582 margin: 0 0 12px; 11583 font-size: 15px; 11584 font-weight: 900; 11585 letter-spacing: 1px; 11586 text-shadow: 0 0 6px rgba(167 0 0 / 90%); 11587 } 11588 11589 /* =============================== 11590 CONTROLS 11591================================ */
11592 .togel-controls-v3 { 11593 display: grid; 11594 grid-template-columns: 1fr 1fr; 11595 gap: 10px; 11596 padding: 12px; 11597 border-radius: 14px; 11598 11599 background: rgba(0, 0, 0, 0.4); 11600 border: 1px solid rgba(255, 60, 60, 0.45); 11601 box-shadow: inset 0 0 14px rgba(205 0 0 / 22%); 11602 } 11603 11604 .togel-controls-v3 label { 11605 display: grid; 11606 gap: 6px; 11607 font-size: 10px; 11608 letter-spacing: .8px; 11609 text-transform: uppercase; 11610 color: #cd0000; 11611 } 11612 11613 .togel-controls-v3 select, 11614 .togel-controls-v3 input { 11615 background: rgba(0 0 0 / 92%); 11616 color: #c5c5c5; 11617 border: 1px solid rgba(205 0 0); 11618 border-radius: 10px; 11619 padding: 7px 9px; 11620 font-weight: 700; 11621 font-size: 12px; 11622 outline: none; 11623 box-shadow: inset 0 0 10px rgba(207 3 3); 11624 } 11625 11626 /* BUTTON */ 11627 #btnTogelRandom { 11628 grid-column: span 2; 11629 display: flex; 11630 align-items: center; 11631 justify-content: center; 11632 gap: 8px; 11633 11634 background: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 11635 border: none; 11636 color: #000000; 11637 font-weight: 900; 11638 letter-spacing: .8px; 11639 border-radius: 14px; 11640 cursor: pointer; 11641 padding: 10px 12px; 11642 font-size: 12px; 11643 11644 box-shadow: 0 0 16px rgba(255, 40, 40, 0.75); 11645 transition: .2s ease; 11646 } 11647 11648 #btnTogelRandom:active { 11649 transform: scale(.98); 11650 } 11651 11652 #btnTogelRandom:disabled { 11653 opacity: .75; 11654 cursor: not-allowed; 11655 } 11656 11657 /* =============================== 11658 MAIN RESULT 11659================================ */ 11660 .togel-main-result-v3 { 11661 margin-top: 12px; 11662 padding: 12px; 11663 border-radius: 14px; 11664 11665 background: rgba(30, 0, 0, 0.78); 11666 border: 1px solid rgba(223 0 0); 11667 box-shadow: inset 0 0 16px rgba(215 0 0); 11668 } 11669 11670 .togel-status-v3 { 11671 text-align: center; 11672 font-weight: 800; 11673 letter-spacing: .8px; 11674 color: #cd0000; 11675 margin-bottom: 10px; 11676 font-size: 11px; 11677 } 11678 11679 .togel-highlight-box { 11680 display: flex; 11681 justify-content: space-between; 11682 gap: 12px; 11683 padding-bottom: 10px; 11684 margin-bottom: 10px; 11685 border-bottom: 1px dashed rgba(255, 80, 80, 0.3); 11686 } 11687 11688 .k, 11689 .k-rtp { 11690 font-size: 10px; 11691 color: #c7c7c7; 11692 text-transform: uppercase; 11693 } 11694 11695 .v-game { 11696 font-size: 14px; 11697 font-weight: 900; 11698 color: #ffffff; 11699 margin-top: 6px; 11700 line-height: 1.2; 11701 } 11702 11703 .provider-info { 11704 margin-top: 8px; 11705 font-size: 11px; 11706 color: #d9d9d9; 11707 opacity: .95; 11708 } 11709 11710 .k-small { 11711 color: #cd0000; 11712 font-size: 10px; 11713 } 11714 11715 .v-small {
11716 color: #fff0f0; 11717 font-weight: 900; 11718 } 11719 11720 .v-rtp-huge { 11721 font-size: 2.4em; 11722 font-weight: 900; 11723 color: #d70000; 11724 text-shadow: 0 0 10px rgba(0 0 0); 11725 line-height: 1; 11726 letter-spacing: 1px; 11727 } 11728 11729 /* DETAILS */ 11730 .togel-details-v3 { 11731 display: grid; 11732 grid-template-columns: repeat(3, 1fr); 11733 gap: 8px; 11734 background: rgba(0, 0, 0, .30); 11735 padding: 10px; 11736 border-radius: 12px; 11737 } 11738 11739 .k-detail { 11740 font-size: 9px; 11741 color: #cd0000; 11742 text-transform: uppercase; 11743 } 11744 11745 .v-detail { 11746 font-size: 12px; 11747 font-weight: 800; 11748 color: #ffffff; 11749 margin-top: 4px; 11750 letter-spacing: .8px; 11751 } 11752 11753 .togel-tip-v3 { 11754 margin-top: 10px; 11755 font-size: 10px; 11756 color: #d9d9d9; 11757 opacity: .9; 11758 text-align: center; 11759 } 11760 11761 /* ALTS */ 11762 .togel-alts-v3 { 11763 margin-top: 12px; 11764 padding: 12px; 11765 border-radius: 14px; 11766 background: rgba(0, 0, 0, .40); 11767 border: 1px solid rgba(223 0 0); 11768 } 11769 11770 .togel-alts-title-v3 { 11771 font-weight: 900; 11772 color: #cd0000; 11773 margin-bottom: 10px; 11774 font-size: 12px; 11775 } 11776 11777 .togel-alt-list-v3 { 11778 display: grid; 11779 grid-template-columns: 1fr; 11780 gap: 8px; 11781 } 11782 11783 .togel-alt-item { 11784 background: rgba(30, 0, 0, .86); 11785 border-radius: 12px; 11786 padding: 10px; 11787 border-left: 3px solid rgba(167 0 0 / 90%); 11788 } 11789 11790 .togel-alt-item .g { 11791 font-weight: 900; 11792 color: #c5c5c5; 11793 font-size: 12px; 11794 } 11795 11796 .togel-alt-item .p { 11797 font-size: 10px; 11798 color: #cd0000; 11799 margin-top: 4px; 11800 } 11801 11802 .togel-alt-item .r { 11803 margin-top: 6px; 11804 font-weight: 900; 11805 color: #d70000; 11806 letter-spacing: .8px; 11807 font-size: 12px; 11808 } 11809 11810 .togel-note-v3 { 11811 margin-top: 10px; 11812 font-size: 10px; 11813 color: #d9d9d9; 11814 opacity: .85; 11815 text-align: center; 11816 } 11817 11818 /* MOBILE */ 11819 @media(max-width:420px) { 11820 .togel-highlight-box { 11821 flex-direction: column 11822 } 11823 11824 .togel-details-v3 { 11825 grid-template-columns: 1fr 11826 } 11827 } 11828 11829 /* ANIM */ 11830 @keyframes hudSweep { 11831 0% { 11832 transform: translateX(-110%) 11833 } 11834 11835 100% { 11836 transform: translateX(110%) 11837 } 11838 } 11839 11840 @keyframes pulse { 11841 0% { 11842 opacity: .55 11843 } 11844 11845 50% { 11846 opacity: 1 11847 } 11848 11849 100% { 11850 opacity: .55 11851 } 11852 } 11853 </style> 11854 11855
11855<script> 11856 /* =============================== 11857 DATASET TOGEL (SIMULASI) 11858 ================================ */ 11859 const TOGEL_MARKETS = [ 11860 { market: "Singapore", code: "SGP", open: "17:45", zone: "WIB" }, 11861 { market: "Hongkong", code: "HK", open: "22:45", zone: "WIB" }, 11862 { market: "Sydney", code: "SDY", open: "13:45", zone: "WIB" }, 11863 { market: "Macau", code: "MC", open: "16:00", zone: "WIB" }, 11864 { market: "Cambodia", code: "KHM", open: "10:45", zone: "WIB" }, 11865 { market: "Japan", code: "JP", open: "11:30", zone: "WIB" } 11866 ]; 11867 11868 const SHIO = ["Tikus", "Kerbau", "Macan", "Kelinci", "Naga", "Ular", "Kuda", "Kambing", "Monyet", "Ayam", "Anjing", "Babi"]; 11869 11870 const $ = (q) => document.querySelector(q); 11871 const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 11872 11873 function uniqueMarkets() { 11874 return TOGEL_MARKETS.map(m => m.market); 11875 } 11876 11877 function genDigits(len) { 11878 let s = ""; 11879 for (let i = 0; i < len; i++) s += String(randInt(0, 9)); 11880 return s; 11881 } 11882 11883 function genBBFS() { 11884 const pool = [...Array(10)].map((_, i) => i); 11885 const out = []; 11886 for (let i = 0; i < 7; i++) { 11887 const idx = randInt(0, pool.length - 1); 11888 out.push(pool.splice(idx, 1)[0]); 11889 } 11890 return out.join(""); 11891 } 11892 11893 function genIkut() { 11894 const pool = [...Array(10)].map((_, i) => i); 11895 const out = []; 11896 for (let i = 0; i < 4; i++) { 11897 const idx = randInt(0, pool.length - 1); 11898 out.push(pool.splice(idx, 1)[0]); 11899 } 11900 return out.join(" ⢠"); 11901 } 11902 11903 function simulateConfidence() { 11904 return randInt(60, 96); 11905 } 11906 11907 function applyFilters() { 11908 const m = $("#marketFilter").value; 11909 const minC = parseFloat($("#minConf").value || "0"); 11910 11911 return TOGEL_MARKETS 11912 .filter(x => (m === "ALL" || x.market === m)) 11913 .map(x => ({ ...x, conf: simulateConfidence() })) 11914 .filter(x => x.conf >= minC); 11915 } 11916 11917 function pickRandom(arr, n = 1) { 11918 const copy = [...arr]; 11919 const out = []; 11920 for (let i = 0; i < n && copy.length; i++) { 11921 const idx = randInt(0, copy.length - 1); 11922 out.push(copy.splice(idx, 1)[0]); 11923 } 11924 return out; 11925 } 11926 11927 function formatAngka(type) { 11928 if (type === "4D") return genDigits(4); 11929 if (type === "3D") return genDigits(3); 11930 return genDigits(2); 11931 } 11932 11933 function renderMain(item, type) { 11934 $("#rMarket").textContent = `${item.market} (${item.code}) ⢠Open ${item.open} ${item.zone}`; 11935 $("#rType").textContent = type; 11936 $("#rConf").textContent = `${item.conf}%`; 11937 11938 $("#rAngka").textContent = formatAngka(type); 11939 $("#rBBFS").textContent = genBBFS(); 11940 $("#rIkut").textContent = genIkut(); 11941 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 11942 $("#rTip").textContent = "Tips: Ini simulasi hiburan. Tetap bijak & disiplin."; 11943 } 11944 11945 function renderAlts(list, type) { 11946 const wrap = $("#togelAltList"); 11947 wrap.innerHTML = ""; 11948 list.forEach(x => { 11949 const el = document.createElement("div"); 11950 el.className = "togel-alt-item"; 11951 el.innerHTML = ` 11952 <div class="g">${x.market} (${x.code})</div> 11953 <div class="p">Tipe ${type} ⢠Conf <strong>${x.conf}%</strong></div> 11954 <div class="r">Angka: <strong>${formatAngka(type)}</strong></div> 11955 `; 11956 wrap.appendChild(el); 11957 }); 11958 } 11959 11960 function spinButton(btn, on) { 11961 if (on) { 11962 btn.disabled = true; 11963 btn.dataset.text = btn.innerHTML; 11964 btn.innerHTML = `<span class="icon">ð</span> PROCESS...`; 11965 } else { 11966 btn.disabled = false;
11967 btn.innerHTML = btn.dataset.text || `<span class="icon">ð²</span> GENERATE`; 11968 } 11969 } 11970 11971 /* =============================== 11972 INIT 11973 ================================ */ 11974 (function init() { 11975 const sel = $("#marketFilter"); 11976 uniqueMarkets().forEach(m => { 11977 const o = document.createElement("option"); 11978 o.value = m; o.textContent = m; 11979 sel.appendChild(o); 11980 }); 11981 11982 $("#btnTogelRandom").addEventListener("click", (e) => { 11983 const btn = e.currentTarget; 11984 const type = $("#typeFilter").value; 11985 const filtered = applyFilters(); 11986 const st = $("#togelStatus"); 11987 11988 if (!filtered.length) { 11989 st.textContent = "â NO MATCH â"; 11990 st.style.color = "white"; 11991 renderAlts([], type); 11992 $("#rMarket").textContent = "-"; 11993 $("#rType").textContent = type; 11994 $("#rConf").textContent = "-"; 11995 $("#rAngka").textContent = "-"; 11996 $("#rBBFS").textContent = "-"; 11997 $("#rIkut").textContent = "-"; 11998 $("#rShio").textContent = "-"; 11999 return; 12000 } 12001 12002 spinButton(btn, true); 12003 st.textContent = "â GENERATING â"; 12004 st.style.animation = "pulse .6s linear infinite"; 12005 st.style.color = "white"; 12006 12007 const t0 = setInterval(() => { 12008 const temp = pickRandom(filtered, 1)[0]; 12009 $("#rMarket").textContent = `${temp.market} (${temp.code}) ⢠Open ${temp.open} ${temp.zone}`; 12010 $("#rType").textContent = type; 12011 $("#rConf").textContent = `${temp.conf}%`; 12012 $("#rAngka").textContent = formatAngka(type); 12013 $("#rBBFS").textContent = genBBFS(); 12014 $("#rIkut").textContent = genIkut(); 12015 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 12016 }, 85); 12017 12018 setTimeout(() => { 12019 clearInterval(t0); 12020 12021 const picks = pickRandom(filtered, 4); 12022 renderMain(picks[0], type); 12023 renderAlts(picks.slice(1), type); 12024 12025 st.textContent = "â READY â"; 12026 st.style.animation = ""; 12027 st.style.color = "gold"; 12028 spinButton(btn, false); 12029 }, 1200); 12030 }); 12031 12032 renderAlts([], "4D"); 12033 })(); 12034 </script>
12034 12035
12036<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516" integrity="sha512-8DS7rgIrAmghBFwoOTujcf6D9rXvH8xm8JQ1Ja01h9QX8EzXldiszufYa4IFfKdLUKTTrnSFXLDkUEOTrZQ8Qg==" data-cf-beacon='{"version":"2024.11.0","token":"1909e908a10849b7af66d8986368283e","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12036
12036
12037<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"f591dd59004a48eeafbe4ab22ac1a9da","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12037
12037
12038<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"e160a7c7e3074d01bd34414603c53b23","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12038
12038
12039<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"7d1313fe7e8b4cc3ac480817ec77c2a1","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12039
12039
12040<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"7d1313fe7e8b4cc3ac480817ec77c2a1","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12040
12040
12041<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"57cb2e82efc64548a4a6bee0c0fb1166","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 12041
12041
12042<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v833ccba57c9e4d2798f2e76cebdd09a11778172276447" integrity="sha512-57MDmcccJXYtNnH+ZiBwzC4jb2rvgVCEokYN+L/nLlmO8rfYT/gIpW2A569iJ/3b+0UEasghjuZH/ma3wIs/EQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
12042 12043 12044<style> 12045body{background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); font-family:Poppins,sans-serif; color:white;} 12046.IWANTOGEL-container{ 12047max-width: 520px; 12048 margin: auto; 12049 padding: 15px; 12050 border-radius: 12px; 12051 background: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 12052 border: 2px solid #ca0000; 12053 box-shadow: 0 0 20px #d90000; 12054} 12055 12056.IWANTOGEL-header{ 12057 font-weight:900; 12058 text-align:center; 12059 color:#ff4c4c; 12060 text-shadow:0 0 12px #5f90d9; 12061 margin-bottom:12px; 12062} 12063 12064.IWANTOGEL-controls{ 12065 display:grid; 12066 grid-template-columns:1fr 1fr; 12067 gap:10px; 12068 margin-bottom:12px; 12069} 12070.IWANTOGEL-controls label{ 12071 display:flex; flex-direction:column; 12072 font-size:11px; color:#ff9999; 12073} 12074.IWANTOGEL-controls select, 12075.IWANTOGEL-controls input{ 12076 padding:8px; 12077 border-radius:8px; 12078 border:1px solid #ff4c4c; 12079 background:#2b0000; 12080 color:white; 12081} 12082#btnGenerate{ 12083 grid-column:span 2; 12084 padding:12px; 12085 background:#5f90d9; 12086 border:none; 12087 color:black; 12088 font-weight:900; 12089 cursor:pointer; 12090 margin-top:5px; 12091 box-shadow:0 0 15px #ff4c4c; 12092} 12093 12094.IWANTOGEL-result{ 12095 margin-top:12px; 12096 padding:12px; 12097 border-radius:10px; 12098 background:rgba(255,0,0,0.05); 12099 border:1px solid #ff4c4c; 12100} 12101
12102.IWANTOGEL-main div, .IWANTOGEL-details div{ 12103 margin:5px 0; 12104} 12105 12106#rPrediction{ 12107 font-size:24px; 12108 font-weight:900; 12109 color:#5f90d9; 12110} 12111 12112.IWANTOGEL-alts{ 12113 margin-top:10px; 12114} 12115.IWANTOGEL-alts h4{margin-bottom:6px;} 12116#IWANTOGELAltList div{ 12117 background:#2b0000; 12118 padding:6px 10px; 12119 margin:3px 0; 12120 border-radius:8px; 12121 border-left:3px solid #5f90d9; 12122} 12123 .bank-tab.active, .bank-tab:hover { 12124 background: #df0000; 12125 color: #fff; 12126 border-color: #df0000; 12127 box-shadow: 0 3px 10px -4px rgb(240 29 29 / 60%); 12128} 12129 12130.IWANTOGEL-disclaimer{ 12131 font-size:10px; 12132 margin-top:8px; 12133 text-align:center; 12134 color:#ff9999; 12135} 12136</style> 12137
12138<script> 12139const leagues = ["Premier League","La Liga","Serie A"]; 12140const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12141function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12142 12143var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12144 const type = document.getElementById("betTypeFilter").value; 12145 const league = rand(leagues); 12146 12147 let pred; 12148 if(type==="1X2") pred=rand(["1","X","2"]); 12149 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12150 else pred=rand(["Home -1","Away +1","Draw"]); 12151 12152 document.getElementById("rLeague").innerText=league; 12153 document.getElementById("rPrediction").innerText=pred; 12154 document.getElementById("rBetType").innerText=type; 12155 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12156 document.getElementById("rTip").innerText=rand(tips); 12157 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12158 12159 // alternatif 12160 const alt=document.getElementById("IWANTOGELAltList"); alt.innerHTML=""; 12161 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12162 12163 document.getElementById("IWANTOGELStatus").innerText="â READY â"; 12164} 12165</script>
12165 12166 12167 12168 12169 <div class="pwp-deals-section" id="pwp-deals-section" hidden data-product-id="325" data-variant-id="0" data-category-path="Komputer & Laptop/Laptop/Gaming"><div class="pwp-deals-header"><svg class="pwp-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg><div class="pwp-text"><h3 class="pwp-title">Penawaran Spesial PWP (Purchase with Purchase)</h3><p class="pwp-subtitle">Beli produk ini dan dapatkan produk pilihan dengan harga spesial</p></div></div><div class="pwp-deals-loading"><div class="pwp-spinner"></div><p>Memuat penawaran...</p></div><div class="pwp-deals-grid" id="pwp-deals-grid"></div><p class="pwp-cart-note">ð¡ Produk PWP dapat ditambahkan di halaman keranjang setelah produk ini dimasukkan</p></div> 12170 </div> 12171 </div> 12172 </section> 12173 12174 <aside class="pd-sidebar"> 12175 <div class="pd-side-card pd-buy-card"> 12176 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12177 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12178 <div> 12179 <label for="qty" class="qty-label">Qty</label> 12180 <input type="number" id="qty" name="qty" value="0" min="1" 12181 max="0" 12182 disabled> 12183 </div> 12184 <div class="pd-actions"> 12185 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12186 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12187 <button type="button" class="btn-chat-icon" id="btn-chat-icon" aria-label="Tanya Produk" title="Tanya Produk"><svg viewBox="0 0 640 640" fill="currentColor"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg></button> 12188 </div> 12189 </form> 12190 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12191 <span class="paylater-icon">ð³</span> 12192 <span class="paylater-text"> 12193 <strong>Autorized IWANTOGEL</strong> 12194 <small> IWANTOGEL OFFICIAL </small> 12195 </span> 12196</div> </div> 12197 12198 12199 <div class="pd-side-card qoala-banner-card"> 12200 <div class="qoala-banner"> 12201 <div class="qoala-banner-content"> 12202 <div class="qoala-icon"> 12203 <center><img src="https://area-terpercaya.b-cdn.net/newicon-iwantogel.png" alt="IWANTOGEL" class="qoala-shield-img"></center> 12204 </div> 12205 <div class="qoala-text"> 12206 <div class="qoala-title">Login IWANTOGEL</div> 12207 <div class="qoala-subtitle">Verified</div> 12208 </div> 12209 </div> 12210 <a href="https://slot-gacor.loklokpro.com/" class="qoala-cta" 12211 data-ga-event="qoala_banner_click" 12212 data-product-id="325" 12213 data-product-name="IWANTOGEL" 12214 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">LOGIN</a> 12215 </div> 12216<style> 12217.qoala-banner-card{padding:0;overflow:hidden} 12218 12219.qoala-banner{ 12220 background:linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 12221 padding:12px 16px; 12222 display:flex; 12223 align-items:center; 12224 justify-content:space-between; 12225 color:white; 12226 text-decoration:none; 12227 min-height:60px; 12228} 12229 12230.qoala-banner-content{ 12231 display:flex; 12232 align-items:center; 12233 gap:12px; 12234} 12235 12236.qoala-icon{ 12237 display:flex; 12238 align-items:center; 12239 justify-content:center; 12240} 12241 12242.qoala-shield-img{ 12243 width:36px; 12244 height:36px; 12245 object-fit:contain; 12246 opacity:0.9; 12247} 12248 12249.qoala-title{ 12250 font-weight:600; 12251 font-size:14px; 12252 line-height:1.2; 12253} 12254 12255.qoala-subtitle{ 12256 font-size:12px; 12257 opacity:0.9; 12258 line-height:1.2; 12259} 12260 12261.qoala-cta{ 12262 background:rgba(0,0,0,0.15); 12263 color:white; 12264 padding:6px 12px; 12265 border-radius:6px; 12266 font-size:12px; 12267 font-weight:600; 12268 text-decoration:none; 12269 transition:background 0.2s ease; 12270 border:1px solid rgba(255,80,80,0.4); 12271} 12272 12273.qoala-cta:hover{ 12274 background:rgba(255,60,60,0.25); 12275 color:white; 12276} 12277 12278@media(max-width:768px){ 12279 .qoala-banner{padding:10px 12px} 12280 .qoala-title{font-size:17px} 12281 .qoala-subtitle{font-size:11px} 12282} 12283</style> 12284 </div> 12285 12286 <div class="pd-side-card pd-installment-card2" 12287 id="inst-card" 12288 data-base-price="20789000" 12289 data-zero-smart="0"> 12290 <div class="inst-head"> 12291 <h3>Promo & Bonus IWANTOGEL</h3> 12292 </div> 12293 12294 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12295 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Spin</button> 12296 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12296nover" aria-selected="false">New Member</button> 12297 <button type="button" class="bank-tab" data-bank="Prediksi Jitu" aria-selected="false">Garansi kekalahan 70%</button> 12298 </div> 12299 12300 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12301 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12302 </div> 12303 12304 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12305<div class="tnt-review-mecha"> 12306 12307<div class="tnt-header"> 12308<h2>REVIEW PENGGUNA IWANTOGEL</h2> 12309 12310</div> 12311 12312<div class="tnt-slider" id="tntSlider"> 12313 12314<div class="tnt-slide active"> 12315 <div class="tnt-name">Rudiyanto â Batam</div> 12316 <div class="tnt-comment">Dari Dulu Saya Gabung Sama IWANTOGEL belum Perna Di kecewakan Oleh Csnya maupun Permainannya Karna itu IWANTOGEL Sudah menjadi Situs Favorit Buat Saya Dan Teman kampus Saya juga Sudah Yang Gabung Sama Situs ini.</div> 12317</div> 12318 12319<div class="tnt-slide"> 12320 <div class="tnt-name">Evi â Jakarta</div> 12321 <div class="tnt-comment">Sejak Saya Gabung Situs ini uang belanja Saya Bisa Buat Chekout make up mantap IWANTOGEL Situs terbaik Sangat membantu Saya Buat Update Diri.</div> 12322</div> 12323 12324<div class="tnt-slide"> 12325 <div class="tnt-name">Mardianto Syaputra - Pontianak</div> 12326 <div class="tnt-comment">Situsnya Rapi Dan Sangat Menjakan Mata Mulai dari permainan yang Sangat banyak Sehingga tidak Bakal Bosan kalau main di situs ini.</div> 12327</div> 12328 12329 12330 12331</div> 12332</div> 12333 12334</aside> 12335</div> 12336 12337<style> 12338.tnt-review-mecha{ 12339 background: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 12340 border: 2px solid #aa0000; 12341 border-radius: 14px; 12342 padding: 20px; 12343 color: #dbdbdb; 12344 font-family: 'Orbitron', sans-serif; 12345 position: relative; 12346 overflow: hidden; 12347 box-shadow: 0 0 25px #990000, inset 0 0 20px rgba(255, 0, 0, 0.2); 12348} 12349 12350/* scanline */ 12351.tnt-review-mecha::before{ 12352 content:""; 12353 position:absolute; 12354 inset:0; 12355 background: repeating-linear-gradient( 12356 to bottom, 12357 rgba(255,0,0,0.08), 12358 rgba(255,0,0,0.08) 1px, 12359 transparent 2px, 12360 transparent 4px 12361 ); 12362 pointer-events:none; 12363}.bank-tab.active, .bank-tab:hover 12364 12365 12366.tnt-header h2{ 12367 font-size: 18px; 12368 color:#000000; 12369 text-shadow:0 0 10px #d71414, 0 0 20px #d10000; 12370 margin-bottom:5px; 12371} 12372 12373.tnt-header p{ 12374 font-size:12px; 12375 opacity:0.85; 12376 margin-bottom:15px; 12377} 12378 12379.tnt-slider{ 12380 position:relative; 12381 height:90px; 12382} 12383 12384.tnt-slide{ 12385 position:absolute; 12386 width:100%; 12387 opacity:0; 12388 transform:translateX(40px); 12389 transition:all .6s ease; 12390} 12391 .tenor-badge { 12392 background: #df0000; 12393 color: #fff; 12394 font-weight: 700; 12395 min-width: 42px; 12396 text-align: center; 12397 padding: .38rem .4rem; 12398 font-size: .8rem; 12399 border-radius: 10px; 12400 box-shadow: 0 3px 10px -4px rgb(211 3 3); 12401} 12402 12403.tnt-slide.active{ 12404 opacity:1; 12405 transform:translateX(0); 12406} 12407 12408.tnt-name{ 12409font-size: 14px; 12410 color: #000000; 12411 margin-bottom: 5px; 12412 text-shadow: 0 0 6px #290000; 12413} 12414 12415.tnt-comment{ 12416 font-size:13px; 12417 line-height:1.5; 12418} 12419 12420/* glowing border animation */ 12421@keyframes pulseGlow{ 12422 0%{box-shadow:0 0 10px #d71414;} 12423 50%{box-shadow:0 0 25px #d71414;} 12424 100%{box-shadow:0 0 10px #d71414;} 12425} 12426 12427.tnt-review-mecha{ 12428 animation:pulseGlow 2.5s infinite; 12429} 12430</style> 12431
12432<script> 12433let slides = document.querySelectorAll('.tnt-slide'); 12434let index = 0; 12435 12436setInterval(()=>{ 12437 slides[index].classList.remove('active'); 12438 index = (index + 1) % slides.length; 12439 slides[index].classList.add('active'); 12440}, 3000); 12441</script>
12441 12442 12443 <div class="artikel-glow-KT"> 12444 12445 <div class="artikel-shine"></div> 12446 12447 <h1>LokLokpro.com | Tempat Menikmati Vidio game Online Dan Vidio secara Gratis Bersama IWANTOGEL </h1> 12448 <p style="text-align: justify;"><a href="https://loklokpro.com/">IWANTOGEL</a> Merupakan Platform toto Slot yang Paling Mudah menang Sehingga Platform Ini Sudah banyak membantu Banyak orang yang Sedang Dalam Imbang kehancuran.</p> 12449 12450<p style="text-align: justify;">tidak hanya Fokus Untuk Mengembalikan Ekonomi Orang lain IWANTOGEL juga Memfokuskan pelayanan Yang Sangat profesional Dengan layanan Transaksi Secepat Kilat Dan Layanan Costumer Servis yang Online Selama 24 jam Sehingga Dapat membantu Semua kendala Pengguna yang mengalami Kendala/Masalah.</p> 12451 12452 <p style="text-align: justify;">Selain itu IWANTOGEL Juga Memfokuskan pada Permainan yang Lengkap Beserta Dukungan RTP Slot Akurat yang Di susun rapi Sehingga pecinta Slot Online dapat Menemukan permainan yang ingin Di cek.IWANTOEGL juga Menyediakan Data Togel Terlengkap Untuk pecinta Togel Online Untuk Melakukan pengecekan data Togel yang Sudah Lewat Jadi tidak perlu lagi Bolak balik situs untuk melakukan Pengecekan.</p> 12453 12454</p> 12455</div> 12456 12457<style> 12458.artikel-glow-KT{ 12459 max-width: 820px; 12460 margin: 40px auto; 12461 padding: 36px; 12462 box-sizing: border-box; 12463 position: relative; 12464 overflow: hidden; 12465 border-radius: 30px; 12466 font-family: Arial, sans-serif; 12467 color: #ffffff; 12468 background: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 12469 border: 2px solid rgba(255, 255, 255, .55); 12470 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgb(225 0 0), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(145 0 0); 12471} 12472 12473.artikel-glow-KT:before{ 12474 content:""; 12475 position:absolute; 12476 inset:10px; 12477 border-radius:24px; 12478 border:1px solid rgba(0,0,0,.32); 12479 pointer-events:none; 12480} 12481 12482.artikel-shine{ 12483 position:absolute; 12484 top:-120px; 12485 left:-180px; 12486 width:280px; 12487 height:650px; 12488 background:linear-gradient(120deg,transparent,rgba(0,0,0,.55),transparent); 12489 transform:rotate(25deg); 12490 animation:artikelShine 4.5s infinite; 12491 pointer-events:none; 12492} 12493 12494@keyframes artikelShine{ 12495 0%{left:-220px;} 12496 55%{left:110%;} 12497 100%{left:110%;} 12498} 12499 12500.artikel-glow-KT h1, 12501.artikel-glow-KT h2{ 12502 position:relative; 12503 color:#000000; 12504 text-align:center; 12505 line-height:1.35; 12506 text-shadow: 12507 0 0 8px rgba(179 0 0 / 95%), 12508 0 0 18px rgba(197 0 0), 12509 0 2px 8px rgba(149 0 0); 12510} 12511 12512.artikel-glow-KT h1{ 12513 font-size:31px; 12514 margin:0 0 20px; 12515} 12516 12517.artikel-glow-KT h2{ 12518 font-size:24px; 12519 margin:28px 0 14px; 12520} 12521 12522.artikel-glow-KT p{ 12523 position:relative; 12524 margin:0 0 16px; 12525 color:#f8ffff; 12526 font-size:15px; 12527 line-height:1.85; 12528 text-align:justify; 12529 text-shadow:0 1px 4px rgba(0,0,0,.28); 12530} 12531 12532.artikel-glow-KT a{ 12533 color:#000000; 12534 font-weight:800; 12535 text-shadow:0 0 8px rgba(153 0 0); 12536} 12537 12538.artikel-glow-KT img{ 12539 position:relative; 12540 display:block; 12541 max-width:100%; 12542 margin:16px auto 24px; 12543 border-radius:16px; 12544 border:1px solid rgba(0,0,0,.55); 12545 box-shadow: 12546 0 0 18px rgba(0,0,0,.55), 12547 0 12px 28px rgba(0,0,0,.28); 12548} 12549 12550.artikel-highlight{ 12551 position:relative; 12552 margin:24px 0; 12553 padding:22px; 12554 border-radius:22px; 12555 background:rgba(0,0,0,.20); 12556 border:1px solid rgba(0,0,0,.45); 12557 box-shadow: 12558 inset 0 1px 0 rgba(0,0,0,.65), 12559 inset 0 0 22px rgba(0,0,0,.14), 12560 0 10px 25px rgba(0,0,0,.18); 12561 backdrop-filter:blur(8px); 12562} 12563 12564.artikel-highlight:before{ 12565 content:""; 12566 position:absolute; 12567 left:0; 12568 right:0; 12569 top:0; 12570 height:45%; 12571 border-radius:22px 22px 0 0; 12572 background:linear-gradient(rgba(0,0,0,.35),rgba(0,0,0,.04)); 12573 pointer-events:none; 12574} 12575 12576.artikel-glow-KT ul{ 12577 position:relative; 12578 margin:15px 0 0 20px; 12579 padding:0; 12580} 12581 12582.artikel-glow-KT li{ 12583 margin-bottom:11px; 12584 padding:10px 12px; 12585 border-radius:12px; 12586 background:rgba(0,0,0,.16); 12587 color:#ffffff; 12588 line-height:1.6; 12589 box-shadow:inset 0 0 10px rgba(0,0,0,.08); 12590} 12591 12592@media(max-width:600px){ 12593 .artikel-glow-KT{ 12594 margin:25px 12px; 12595 padding:25px; 12596 border-radius:24px; 12597 } 12598 12599 .artikel-glow-KT h1{ 12600 font-size:24px; 12601 } 12602 12603 .artikel-glow-KT h2{ 12604 font-size:21px; 12605 } 12606} 12607</style> 12608 12609<div class="KT-faq-rainbow"> 12610 <div class="KT-shine"></div> 12611 12612 <div class="KT-faq-top"> 12613 <span>FAQ</span> 12614 <h2>Frequently Asked Questions IWANTOGEL</h2> 12615 12616 </div> 12617 12618 <div class="KT-rainbow-item"> 12619 <button type="button">Apa itu IWANTOGEL?</button> 12620 <div class="KT-rainbow-answer"> 12621 <p>
12621Iwantogel merupakan Platform yang menjanjikan kemenangan kepada Semua member Sehingga Platform Ini di kenal Sebagai Platform Sedekah Dalam bentu Permainan berhadiah.</p> 12622 </div> 12623 </div> 12624 12625 <div class="KT-rainbow-item"> 12626 <button type="button">Apakah Disini Menyediakan Prediksi Angka Akurat?</button> 12627 <div class="KT-rainbow-answer"> 12628 <p>Tentu Saja , IWANTOGEL menyediakan semua kebutuhkan pemain Mulai dari Prediksi Angka togel Akurat , Data togel Terlengkap dan Detail, Dan RTP slot paling Jituu.</p> 12629 </div> 12630 </div> 12631 12632 <div class="KT-rainbow-item"> 12633 <button type="button">Berapa lama proses WIthdraw</button> 12634 <div class="KT-rainbow-answer"> 12635 <p>Sangatlah Instan Pemain hanaya perlu menunggu 2 menit Saja Dan Withdraw Sudah Masuk ke Rekening Pemain Jadi pemain tidak Akan merasa Bosan dalam hal tunggu menunggu.</p> 12636 </div> 12637 </div> 12638 12639 <div class="KT-rainbow-item"> 12640 <button type="button">Apa saya lakukan jika Situs ini Kendala?</button> 12641 <div class="KT-rainbow-answer"> 12642 <p>Pemain hanya perlu Konfirmasikan Kendala yang Anda Alami ke Layanan Costumer Servis kami Dan team prefosinal Bersedia Selama 24 jam Untuk Membantu Semua kendala yang Anda alami.</p> 12643 </div> 12644 </div> 12645 <div class="KT-rainbow-item"> 12646 <button type="button">Berapa Minimal Deposit dan WIthdraw Di Situs IWANTOGEL?</button> 12647 <div class="KT-rainbow-answer"> 12648 <p>Sangatlah terjangkau mulai Dari Deposit Minimal 5 Ribu Ruppiah Saja dan Minimal Withdraw Mulai Dari 25 Rb saja.Dengan Jumlah Transaksi yang Sangat Terjangkau IWANTOGEL makin menjadi Platform Favorit bagi Semua Pecinta Judi online.</p> 12649 </div> 12650 </div> 12651</div> 12652 12653<style> 12654.KT-faq-rainbow{ 12655 max-width: 760px; 12656 margin: 40px auto; 12657 padding: 30px; 12658 box-sizing: border-box; 12659 position: relative; 12660 overflow: hidden; 12661 border-radius: 32px; 12662 font-family: Arial, sans-serif; 12663 background: linear-gradient(135deg, rgba(0,0,0,0.95) 0%, rgba(130, 22, 3,0.85) 100%); 12664 border: 2px solid rgba(177 2 2); 12665 box-shadow: 0 0 25px rgb(207 3 3), 0 0 55px rgba(223 0 0), inset 0 0 35px rgba(145 0 0); 12666} 12667 12668.KT-shine{ 12669 position:absolute; 12670 top:-90px; 12671 left:-120px; 12672 width:260px; 12673 height:520px; 12674 background:linear-gradient(120deg,transparent,rgba(0,0,0,.55),transparent); 12675 transform:rotate(25deg); 12676 animation:KTShine 4s infinite; 12677 pointer-events:none; 12678} 12679 12680@keyframes KTShine{ 12681 0%{left:-180px;} 12682 55%{left:110%;} 12683 100%{left:110%;} 12684} 12685 12686.KT-faq-top{ 12687 position:relative; 12688 text-align:center; 12689 margin-bottom:24px; 12690} 12691 12692.KT-faq-top span{ 12693 display:inline-block; 12694 padding:8px 22px; 12695 border-radius:999px; 12696 background:rgba(185 0 0 / 0%); 12697 color:#dbdbdb; 12698 font-size:12px; 12699 font-weight:900; 12700 letter-spacing:2px; 12701 border:1px solid rgba(215 215 215 / 62%); 12702 box-shadow:inset 0 0 14px rgba(0,0,0,.25); 12703} 12704 12705.KT-faq-top h2{ 12706 margin:16px 0 8px; 12707 color:#000000; 12708 font-size:34px; 12709 line-height:1.15; 12710 text-shadow: 12711 0 0 10px rgba(177 0 0), 12712 0 0 22px rgba(173 0 0); 12713} 12714 12715.KT-faq-top p{ 12716 margin:0; 12717 color:#f3ffff; 12718 font-size:14px; 12719} 12720 12721.KT-rainbow-item{ 12722 position:relative; 12723 overflow:hidden; 12724 margin-bottom:14px; 12725 border-radius:20px; 12726 background:rgba(0,0,0,.20); 12727 border:1px solid rgba(0,0,0,.48); 12728 box-shadow: 12729 inset 0 1px 0 rgba(0,0,0,.6), 12730 0 10px 25px rgba(0,0,0,.18); 12731 backdrop-filter:blur(10px); 12732} 12733 12734.KT-rainbow-item:before{ 12735 content:""; 12736 position:absolute; 12737 top:0; 12738 left:0; 12739 right:0; 12740 height:45%; 12741 background:linear-gradient(rgba(0,0,0,.32),rgba(0,0,0,.04)); 12742 pointer-events:none; 12743} 12744 12745.KT-rainbow-item button{ 12746 width:100%; 12747 padding:18px 58px 18px 20px; 12748 border:0; 12749 background: #af0000; 12750 color:#dbdbdb; 12751 text-align:left; 12752 cursor:pointer; 12753 font-size:15px; 12754 font-weight:900; 12755 position:relative; 12756 z-index:2; 12757 text-shadow:0 2px 6px rgba(0,0,0,.35); 12758} 12759 12760.KT-rainbow-item button:after{ 12761 content:"+"; 12762 position:absolute; 12763 right:18px; 12764 top:50%; 12765 transform:translateY(-50%); 12766 width:34px; 12767 height:34px; 12768 border-radius:50%; 12769 background:linear-gradient(135deg,#b10000, #9b0000); 12770 color:#d1d1d1; 12771 text-align:center; 12772 line-height:34px; 12773 font-size:24px; 12774 font-weight:900; 12775 box-shadow: 12776 0 0 14px rgba(0,0,0,.8), 12777 inset 0 -4px 8px rgba(0,0,0,.18); 12778} 12779 12780.KT-rainbow-item.active button:after{ 12781 content:"â"; 12782 background:linear-gradient(135deg,#ff0000, #5b0000); 12783} 12784 12785.KT-rainbow-answer{ 12786 max-height:0; 12787 overflow:hidden; 12788 transition:max-height .35s ease; 12789 position:relative; 12790 z-index:2; 12791} 12792 12793.KT-rainbow-item.active .KT-rainbow-answer{ 12794 max-height:170px; 12795} 12796 12797.KT-rainbow-answer p{ 12798 margin:0; 12799 padding:0 20px 18px; 12800 color:#dbdbdb; 12801 font-size:14px; 12802 line-height:1.7; 12803 text-shadow:0 2px 5px rgba(0 0 0); 12804} 12805 12806@media(max-width:600px){ 12807 .KT-faq-rainbow{ 12808 margin:25px 12px; 12809 padding:22px; 12810 border-radius:24px; 12811 } 12812 12813 .KT-faq-top h2{ 12814 font-size:26px; 12815 } 12816} 12817</style> 12818
12819<script> 12820document.querySelectorAll(".KT-rainbow-item button").forEach(function(btn){ 12821 btn.addEventListener("click", function(){ 12822 this.parentElement.classList.toggle("active"); 12823 }); 12824}); 12825</script>
12825 12826<style> 12827@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap'); 12828 12829.cyber-mecha { 12830 max-width: 800px; 12831 margin: 40px auto; 12832 12833 /* deep luxury red */ 12834 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12835 12836 border-radius: 16px; 12837 border: 1px solid rgba(255, 90, 90, 0.35); 12838 12839 box-shadow: 12840 0 0 25px rgba(140, 30, 30, 0.35), 12841 inset 0 0 20px rgba(255, 60, 60, 0.10); 12842 12843 padding: 25px; 12844 font-family: 'Orbitron', sans-serif; 12845 color: #f7e6e6; 12846 position: relative; 12847 overflow: hidden; 12848} 12849 12850/* scanline overlay */ 12851.cyber-mecha::before { 12852 content: ""; 12853 position: absolute; 12854 inset: 0; 12855 background: repeating-linear-gradient( 12856 to bottom, 12857 rgba(255,90,90,0.03), 12858 rgba(255,90,90,0.03) 1px, 12859 transparent 1px, 12860 transparent 3px 12861 ); 12862 pointer-events: none; 12863} 12864 12865.pd-full-desc-title { 12866 text-align: center; 12867 font-size: 22px; 12868 12869 /* elegant muted red */ 12870 color: #e08b8b; 12871 12872 letter-spacing: 2px; 12873 text-shadow: 0 0 10px rgba(224, 139, 139, 0.4); 12874 margin-bottom: 20px; 12875} 12876 12877.mecha-header h3 { 12878 text-align: center; 12879 font-size: 18px; 12880 color: #ffb3b3; 12881 text-shadow: 0 0 8px rgba(255, 90, 90, 0.5); 12882 margin-bottom: 15px; 12883} 12884 12885.mecha-image img { 12886 width: 100%; 12887 border-radius: 10px; 12888 border: 1px solid rgba(255, 90, 90, 0.4); 12889 box-shadow: 0 0 20px rgba(255, 60, 60, 0.25); 12890 margin-bottom: 20px; 12891} 12892 12893.mecha-content p { 12894 font-size: 14px; 12895 line-height: 1.8; 12896 margin-bottom: 14px; 12897 color: #f3dede; 12898} 12899 12900.glow { 12901 color: #e08b8b; 12902 text-shadow: 0 0 6px rgba(224, 139, 139, 0.5); 12903 font-weight: 600; 12904} 12905 12906.glow-strong { 12907 color: #ff6b6b; 12908 text-shadow: 12909 0 0 10px rgba(255, 90, 90, 0.6), 12910 0 0 20px rgba(255, 60, 60, 0.4); 12911 font-weight: 700; 12912} 12913 12914.final-line { 12915 margin-top: 18px; 12916 padding: 10px; 12917 border: 1px solid rgba(255, 90, 90, 0.35); 12918 background: rgba(60, 0, 0, 0.25); 12919 border-radius: 8px; 12920 text-align: center; 12921 color: #ffd0d0; 12922} 12923</style> 12924 12925 12926 12927 <section class="pd-related-full"> 12928 <h2 class="pd-related-title">Produk Terkait</h2> 12929 <div class="pd-related-grid pd-related-grid--full"> 12930 <a class="pd-related-card" href="https://loklokpro.com/"> 12931 <div class="thumb"> 12932 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="IWANTOGEL"> 12933 <span class="mini-disc">-2%</span> </div> 12934 <div class="info"> 12935 <h3>IWANTOGEL</h3> 12936 <div class="spec-chips">I5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX3050</div> <span class="price"> 12937 <span class="final">Rp 11.500.000</span> 12938 <span class="ori">Rp 12.799.000</span> 12939 </span> 12940 <span class="sold">Terjual 200+</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12941 </div> 12942 </a> 12943 <a class="pd-related-card" href="https://loklokpro.com/"> 12944 <div class="thumb"> 12945 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOTO TOGEL"> 12946 <span class="mini-disc">-1%</span> </div> 12947 <div class="info"> 12948 <h3>TOTO SLOT</h3> 12949 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12950 <span class="final">Rp 22.887.000</span> 12951 <span class="ori">Rp 21.999.000</span> 12952 </span> 12953 <span class="sold">Terjual 20+</span> <span class="stock-dot out" title="Habis"></span> 12954 </div> 12955 </a> 12956 <a class="pd-related-card" href="https://loklokpro.com/"> 12957 <div class="thumb"> 12958 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE">
12959 <span class="mini-disc">-1%</span> </div> 12960 <div class="info"> 12961 <h3>TOTO TOGEL</h3> 12962 <div class="spec-chips">R7AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12963 <span class="final">Rp 23.600.000</span> 12964 <span class="ori">Rp 21.399.000</span> 12965 </span> 12966 <span class="sold">Terjual 6</span> <span class="stock-dot ready" title="Ready"></span> 12967 </div> 12968 </a> 12969 <a class="pd-related-card" href="https://loklokpro.com/"> 12970 <div class="thumb"> 12971 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL 4D"> 12972 <span class="mini-disc">-1%</span> </div> 12973 <div class="info"> 12974 <h3>SITUS SLOT GACOR</h3> 12975 <div class="spec-chips">R5AI <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 1TB <span class="spec-divider">|</span> RTX5060</div> <span class="price"> 12976 <span class="final">Rp 18.800.000</span> 12977 <span class="ori">Rp 19.799.000</span> 12978 </span> 12979 <span class="sold">Terjual 5</span> <span class="stock-dot ready" title="Ready"></span> 12980 </div> 12981 </a> 12982 <a class="pd-related-card" href="https://loklokpro.com/"> 12983 <div class="thumb"> 12984 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOTO TOGEL 4D"> 12985 <span class="mini-disc">-1%</span> </div> 12986 <div class="info"> 12987 <h3>SLOT MUDAH MENANG</h3> 12988 <div class="spec-chips">I7 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX5050</div> <span class="price"> 12989 <span class="final">Rp 28.600.080</span> 12990 <span class="ori">Rp 27.999.000</span> 12991 </span> 12992 <span class="sold">Terjual 6</span> <span class="stock-dot low" title="Stok Terbatas"></span> 12993 </div> 12994 </a> 12995 <a class="pd-related-card" href="https://loklokpro.com/"> 12996 <div class="thumb"> 12997 <img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="TOGEL ONLINE"> 12998 <span class="mini-disc">-1%</span> </div> 12999 <div class="info"> 13000 <h3>SLOT GACOR</h3> 13001 <div class="spec-chips">CORE5 <span class="spec-divider">|</span> 16GB <span class="spec-divider">|</span> 512GB <span class="spec-divider">|</span> RTX4050</div> <span class="price"> 13002 <span class="final">Rp 17.888.000</span> 13003 <span class="ori">Rp 18.499.000</span> 13004 </span> 13005 <span class="stock-dot low" title="Stok Terbatas"></span> 13006 </div> 13007 </a> 13008 13009 </div> 13010 </section> 13011 13012 <div class="pd-mobile-fab" id="pd-mobile-fab" data-pid="325" data-price="20789000" data-voucher-code="" data-voucher-value="0"><div class="mf-actions"><button type="button" class="mf-btn mf-share" id="mf-share" aria-label="Bagikan"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3
13012-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z"/></svg></button><button type="button" class="mf-btn mf-chat" id="mf-chat" aria-label="Tanya Produk"><svg viewBox="0 0 640 640" fill="currentColor"><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg></button><button type="button" class="mf-btn mf-cart" id="mf-cart" disabled aria-label="Tambah ke keranjang"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg></button><button type="button" class="mf-btn mf-buy" id="mf-buy" disabled><span class="mf-buy-text">Beli Sekarang</span><span class="mf-buy-price">Rp 20.789.000</span></button></div></div> 13013 13014 <button type="button" 13015 class="pd-scroll-top" 13016 id="pd-scroll-top" 13017 aria-label="Kembali ke atas" 13018 title="Kembali ke atas"> 13019 <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"> 13020 <path fill="currentColor" d="M7.41 14.59 12 10l4.59 4.59L18 13.17l-6-6-6 6z"/> 13021 </svg> 13022 </button> 13023</main> 13024 13025<div class="pd-share-modal-overlay" id="pd-share-modal-overlay"><div class="pd-share-modal"><div class="pd-share-header"><h2 class="pd-share-title">Bagikan Produk</h2><button type="button" class="pd-share-close" id="pd-share-close" aria-label="Tutup"><svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button></div><div class="pd-share-body"><div class="pd-share-preview"><img src="https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png" alt="IWANTOGEL" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">IWANTOGEL</h3><p class="pd-share-preview-price">Rp 20.789.000</p></div></div><p class="pd-share-label">Bagikan ke</p><div class="pd-share-grid"><a href="#" class="pd-share-option" data-share="whatsapp" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-whatsapp"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.029-.967-.273-.099-.471-.148-.669.149-.198.297-.767.966-.94 1.164-.173.198-.347.223-.644.074-.297-.149-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.206-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.262.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.411.248-.694.248-1.29.173-1.411-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.002a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.263c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.887 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.158 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.305-1.654a11.86 11.86 0 005.67 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.485-8.413"/></svg></span><span class="pd-share-name">WhatsApp</span></a><a href="#" class="pd-share-option" data-share="facebook" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-facebook"><svg viewBox="0 0 24 24">
13025<path fill="currentColor" d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></span><span class="pd-share-name">Facebook</span></a><a href="#" class="pd-share-option" data-share="twitter" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-twitter"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></span><span class="pd-share-name">Twitter</span></a><a href="#" class="pd-share-option" data-share="telegram" target="_blank" rel="noopener"><span class="pd-share-icon pd-share-icon-telegram"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg></span><span class="pd-share-name">Telegram</span></a><a href="#" class="pd-share-option" data-share="email"><span class="pd-share-icon pd-share-icon-email"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></span><span class="pd-share-name">Email</span></a><button type="button" class="pd-share-option" data-share="copy"><span class="pd-share-icon pd-share-icon-copy"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg></span><span class="pd-share-name">Salin Link</span></button><button type="button" class="pd-share-option" data-share="pdf"><span class="pd-share-icon pd-share-icon-pdf"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zM8 15.5h1.5c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5H8v4zm6 0h1c1.1 0 2-.9 2-2v-2c0-1.1-.9-2-2-2h-1v6zm-4.5-1.5H8v-1h1.5v1zM14 13h1v1h-1v-1z"/></svg></span><span class="pd-share-name">Export PDF</span></button></div><div class="pd-share-copy-section"><div class="pd-share-copy-input-wrap"><input type="text" class="pd-share-copy-input" id="pd-share-url-input" value="https://loklokpro.com/" readonly><button type="button" class="pd-share-copy-btn" id="pd-share-copy-url-btn"><svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg><span class="pd-share-copy-text">Salin</span></button></div></div></div></div></div> 13026 13027<style>.toast-root{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);display:flex;flex-direction:column;gap:10px;z-index:99999;pointer-events:none;max-width:min(92vw,540px)}.toast{display:grid;grid-template-columns:28px 1fr auto;gap:12px;background:#000000;color:#eef3ff;border:1px solid rgba(0,0,0,.08);padding:12px 14px;border-radius:12px;box-shadow:0 10px 30px rgba(10,10,20,.35);pointer-events:auto;overflow:hidden}.toast .ico{align-self:start;margin-top:2px}.toast .ttl{font-weight:700;line-height:1.25;font-size:14px;margin-bottom:4px}.toast .msg{opacity:.95;line-height:1.45;font-size:13px}.toast .actions{display:flex;gap:8px;align-self:center}.toast .btn{background:#0ea5e9;color:#fff;border:0;border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;cursor:pointer}.toast .btn.alt{background:transparent;color:#8fb5ff;border:1px solid rgba(143,181,255,.35)}.toast .close{background:transparent;border:0;color:#98a2b3;cursor:pointer;padding:4px;margin-left:6px}.toast.success{border-left:4px solid #22c55e}.toast.info{border-left:4px solid #0ea5e9}.toast.warn{border-left:4px solid #f59e0b}.toast .progress{position:absolute;
13027left:0;bottom:0;height:3px;background:linear-gradient(90deg,#60a5fa,#22c55e);width:100%;transform-origin:left;animation:toastBar linear forwards}@keyframes toastBar{from{transform:scaleX(1)}to{transform:scaleX(0)}}@media(max-width:640px){.toast{grid-template-columns:24px 1fr auto;padding:12px}.toast .btn{padding:7px 10px}}</style> 13028<style> 13029.toast .msg { word-break: break-word; overflow-wrap: anywhere; } 13030 13031.toast-root { bottom: max(12px, env(safe-area-inset-bottom, 12px)); } 13032 13033@media (max-width: 540px) { 13034 .toast-root { 13035 left: 0; right: 0; transform: none; 13036 padding: 0 12px; 13037 bottom: calc(max(12px, env(safe-area-inset-bottom, 12px)) + var(--fab-offset, 72px)); 13038 max-width: none; 13039 } 13040 .toast { 13041 grid-template-columns: 1fr; /* 1 kolom */ 13042 position: relative; 13043 padding: 14px 14px 12px 14px; 13044 border-radius: 16px; 13045 } 13046 .toast .ico { 13047 position: absolute; left: 14px; top: 14px; 13048 margin: 0; transform: translateY(1px); 13049 } 13050 .toast .cnt { padding-left: 34px; } 13051 .toast .ttl { font-size: 15px; margin-bottom: 6px; } 13052 .toast .msg { font-size: 13px; opacity: .96; } 13053 .toast .actions { 13054 margin-top: 12px; gap: 10px; width: 100%; 13055 display: grid; grid-template-columns: 1fr 1fr; 13056 } 13057 .toast .btn { width: 100%; min-width: 0; padding: 10px 0; } 13058 .toast .close { position: absolute; right: 6px; top: 6px; } 13059} 13060</style> 13061
13062<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13063(function adjustToastFabOffset(){ 13064 const root = document.getElementById('toastRoot'); 13065 if (!root) return; 13066 13067 function isVisible(el){ 13068 if (!el) return false; 13069 const cs = getComputedStyle(el); 13070 if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 13071 const r = el.getBoundingClientRect(); 13072 return r.width > 0 && r.height > 0; 13073 } 13074 13075 function update(){ 13076 const fab = document.getElementById('pd-mobile-fab'); 13077 let extra = 16; 13078 if (isVisible(fab)) { 13079 const h = Math.round(fab.getBoundingClientRect().height) || 56; 13080 extra = h + 16; // FAB height + margin 13081 } 13082 root.style.setProperty('--fab-offset', extra + 'px'); 13083 } 13084 13085 window.addEventListener('resize', update, { passive: true }); 13086 const fab = document.getElementById('pd-mobile-fab'); 13087 if (window.ResizeObserver && fab) { 13088 const ro = new ResizeObserver(update); 13089 ro.observe(fab); 13090 } 13091 13092 update(); 13093})(); 13094</script>
13094 13095<div class="toast-root" id="toastRoot" aria-live="polite" aria-atomic="true"></div> 13096
13097<script nonce="se3iyzEHMc9FQEB4wSyd0w==">(function(){const root=document.getElementById('toastRoot');function svgIcon(kind){if(kind==='success')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#22c55e"><path d="M9 16.2l-3.5-3.5 1.4-1.4L9 13.4l7.7-7.7 1.4 1.4z"/></svg>';if(kind==='warn')return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#f59e0b"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>';return '<svg width="24" height="24" viewBox="0 0 24 24" fill="#0ea5e9"><path d="M11 7h2v2h-2zm0 4h2v6h-2z"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z" fill="none" stroke="#0ea5e9"/></svg>'} 13098window.showToast=function(opts){const o=Object.assign({title:'',message:'',type:'info',duration:4000,actions:[]},opts||{});const el=document.createElement('div');el.className='toast '+(o.type||'info');el.setAttribute('role','status');el.style.position='relative';const acts=(o.actions||[]).map(a=>{const lab=a.label||'OK';const cls='btn'+(a.variant==='alt'?' alt':'');return `<button type="button" class="${cls}" data-act="${lab}">${lab}</button>`}).join('');el.innerHTML=`<div class="ico" aria-hidden="true">${svgIcon(o.type)}</div><div class="cnt">${o.title?`<div class="ttl">${o.title}</div>`:''}${o.message?`<div class="msg">${o.message}</div>`:''}</div><div class="actions">${acts}</div><button type="button" class="close" aria-label="Tutup" title="Tutup"><svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M18.3 5.71L12 12.01l-6.3-6.3-1.4 1.41 6.3 6.3-6.3 6.3 1.4 1.41 6.3-6.3 6.3 6.3 1.41-1.41-6.3-6.3 6.3-6.3z"/></svg></button><div class="progress" style="animation-duration:${Math.max(1500,o.duration)}ms"></div>`; 13099 13100 const close = ()=> { 13101 if (!el.parentNode) return; 13102 el.style.transition = 'transform .2s ease, opacity .2s ease'; 13103 el.style.opacity = '0'; el.style.transform = 'translateY(10px)'; 13104 setTimeout(()=>{ if (el.parentNode) el.parentNode.removeChild(el); }, 220); 13105 }; 13106 13107 el.querySelector('.close').addEventListener('click', close); 13108 el.querySelectorAll('.actions .btn').forEach(btn=>{ 13109 btn.addEventListener('click', ()=>{ 13110 const lab = btn.getAttribute('data-act'); 13111 try { 13112 (o.actions||[]).forEach(a=>{ if (a.label===lab && typeof a.onClick==='function') a.onClick(); }); 13113 } catch(_){} 13114 close(); 13115 }); 13116 }); 13117 13118 root.appendChild(el); 13119 setTimeout(close, o.duration); 13120 return el; 13121 }; 13122})(); 13123 13124const _0x1a2b=[{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y","slug":""},{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png"},{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-223.webp"},{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-776.webp"},{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8asp2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-634.webp"},{"url":"https://all-in-iwantogel.b-cdn.net/bannerloklokpro.png","alt":"Lenovo Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Wi...","slug":"lenovo-legion-go-8as
13124p2-83n0000rid-ryzen-z2-extreme-amd-radeon-graphics-eclipse-b-1761796941-892.webp"}],_0x3c4d="Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",_0x5e6f="SB0B3T";function getVariantSummaryText(){const a=document.getElementById('pd-variant-summary');if(!a)return '';const b=Array.from(a.querySelectorAll('.vs-item strong')).map(c=>c.textContent.trim()).filter(Boolean);return b.length?b.join(' / '):''} 13125function encodeVariantHash(id){if(!id||id<=0)return '';const xor=id^0x4A5D;return xor.toString(36)} 13126function decodeVariantHash(hash){if(!hash)return 0;const xor=parseInt(hash,36);return isNaN(xor)?0:xor^0x4A5D} 13127 13128function showSuccessToast(productId,qty,variantId){const variantText=getVariantSummaryText();const skuText=(function(){const el=document.getElementById('pd-sku');return el?el.textContent.trim():PD_SKU})();showToast({type:'warn',title:'Fitur Segera Hadir',message:`<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: ${qty}`,duration:5000,actions:[{label:'Lihat Keranjang',onClick:()=>{}},{label:'Lanjut Belanja',variant:'alt',onClick:()=>{}}]});try{if(window.ga4Ecomm){const qtySend=qty||1;window.ga4Ecomm.addToCart({currency:'IDR',price:20789000,quantity:qtySend,item_id:"SB0B3T",item_name:"Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",item_brand:"Lenovo ",item_category:(["Komputer & Laptop","Laptop","Gaming"]||[]).join(' / ')})}}catch(_){}return false}function showErrorToast(){showToast({type:'error',title:'Gagal Menambah ke Keranjang',message:'Terjadi kesalahan saat memproses permintaan. Silakan coba lagi.',duration:5000})} 13129 13130function executeBuyNowLegacy(id,variantId){const a=getVariantSummaryText(),b=(function(){const c=document.getElementById('pd-sku');return c?c.textContent.trim():_0x5e6f})(); 13131 13132 let willRedirect = false; 13133 try { 13134 if (window.addToCart) { 13135 window.addToCart(id, 1, variantId); 13136 willRedirect = false; 13137 } 13138 } catch(_){} 13139 13140 if (willRedirect) { 13141 showToast({ 13142 type: 'info', 13143 title: 'Menuju Keranjang', 13144 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13145 duration: 1800 13146 }); 13147 setTimeout(()=>{ window.location.href='/keranjang'; }, 1200); 13148 } else { 13149 showToast({ 13150 type: 'warn', 13151 title: 'Fitur Beli Langsung Segera Hadir', 13152 message: `Sementara ini Anda bisa menambahkan produk ke keranjang terlebih dahulu.<br><strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}`, 13153 duration: 6000, 13154 actions: [ 13155 { label: '+ Keranjang', onClick: ()=> { try{ if (window.addToCart) window.addToCart(id,1,variantId); }catch(_){} } }, 13156 { label: 'Hubungi CS', variant: 'alt', onClick: ()=> { window.open('https://wa.me/6281380009898','_blank'); } } 13157 ] 13158 }); 13159 } 13160} 13161 13162document.addEventListener('DOMContentLoaded',function(){var a=document.getElementById('pd-buy-form');if(a){a.addEventListener('submit',function(e){var b=parseInt(a.getAttribute('data-pid'),10)||0;if(window.addToCartSubmit){return window.addToCartSubmit(e,b)}})}});document.addEventListener('DOMContentLoaded',function(){var a=document.getElementById('btn-buy');if(a){a.addEventListener('click',function(){var b=a.getAttribute('data-id');buyNow(b)})}}); 13163 13164function _0x2m3n(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))} 13165 13166(function initGallery(){const mainImg=document.getElementById('pd-main-img');const gallery=document.getElementById('pd-thumb-gallery');if(!mainImg||!gallery)return;gallery.addEventListener('click',function(ev){const btn=ev.target.closest('.pd-thumb-btn');if(!btn)return;const full=btn.getAttribute('data-full');if(full){mainImg.src=full;const alt=btn.getAttribute('data-alt')||'';if(alt)mainImg.alt=alt;const slug=btn.getAttribute('data-slug')||'';if(slug)mainImg.dataset.slug=slug;gallery.querySelectorAll('.pd-thumb-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active')}})})(); 13167 13168(function initMobileSliderCounter(){const slider=document.getElementById('pd-mobile-slider');const counter=document.getElementById('pd-gallery-counter');if(!slider||!counter)return;let ticking=false;slider.addEventListener('scroll',function(){if(!ticking){window.requestAnimationFrame(function(){const slides=slider.querySelectorAll('.pd-mslide');if(!slides.length)return;const containerRect=slider.getBoundingClientRect();const containerCenter=containerRect.left+containerRect.width/2;let currentIdx=0;let minDist=Infinity;
13168slides.forEach((slide,idx)=>{const slideRect=slide.getBoundingClientRect();const slideCenter=slideRect.left+slideRect.width/2;const dist=Math.abs(slideCenter-containerCenter);if(dist<minDist){minDist=dist;currentIdx=idx}});counter.textContent=(currentIdx+1)+'/'+slides.length;ticking=false});ticking=true}},{passive:true})})(); 13169 13170/* ================= Simulasi Cicilan ================= */ 13171(function initInstallmentList(){const card=document.getElementById('inst-card');if(!card)return;function getBasePrice(){return parseInt(card.getAttribute('data-base-price')||'0',10)||0}function setBasePrice(v){card.setAttribute('data-base-price',String(parseInt(v||'0',10)||0))}const zeroEligible=card.getAttribute('data-zero-smart')==='1';window.initMobile=zeroEligible;const listEl=document.getElementById('inst-month-list');const zeroNote=document.getElementById('inst-zero-note');const tenors=[3,6,12];const rates={BRI:{3:0.975,6:0.975,12:0.975},Mandiri:{3:0.984,6:0.9725,12:0.96},BCA:{3:0.97,6:0.94,12:0.91}};function formatRupiah(num){return 'Rp '+(Math.round(num)).toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.')}function activeBank(){const tab=card.querySelector('.bank-tab.active');return tab?tab.dataset.bank:'BRI'} 13172 13173function _0x4o5p(bank){const a=getBasePrice();listEl.innerHTML='';const b=rates[bank]||{};let c=false;tenors.forEach(m=>{let d=b[m]??1;if(zeroEligible&&(bank==='BRI'||bank==='Mandiri')){d=1;c=true}const e=a/d;const f=m>0?Math.ceil((e/m)/100)*100:0;const g=document.createElement('li');g.className='inst-month-item';g.innerHTML=`<span class="tenor-badge">${m}x</span><span class="amount">${formatRupiah(f)} / bln</span>${zeroEligible&&(bank==='BRI'||bank==='Mandiri')?'<span class="tag tag-zero">0%</span>':''}`;listEl.appendChild(g)});zeroNote.style.display=c?'block':'none'} 13174 13175card.addEventListener('click',function(ev){const a=ev.target.closest('.bank-tab');if(!a)return;card.querySelectorAll('.bank-tab').forEach(b=>{b.classList.remove('active');b.setAttribute('aria-selected','false')});a.classList.add('active');a.setAttribute('aria-selected','true');_0x4o5p(a.dataset.bank)}); 13176 13177window.instSetBasePrice=function(newPrice){setBasePrice(newPrice);_0x4o5p(activeBank())};_0x4o5p(activeBank()); 13178})(); 13179 13180/* ====== Fokus gambar varian ====== */ 13181function _0x6q7r(s){return String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'')}function _0x8s9t(u){try{return new URL(u,window.location.origin).pathname.split('/').pop().toLowerCase()}catch(e){try{return(u||'').split('/').pop().toLowerCase()}catch(_){return ''}}} 13182function _0xau1v(url,altOpt){const a=document.getElementById('pd-main-img'),b=document.getElementById('pd-thumb-gallery'),c=document.getElementById('pd-mobile-slider');if(a&&url){a.src=url;if(altOpt)a.alt=altOpt}if(b){const d=_0x8s9t(url);let e=null;b.querySelectorAll('.pd-thumb-btn').forEach(btn=>{const f=btn.getAttribute('data-full')||'',g=_0x8s9t(f)===d;if(g)e=btn;btn.classList.toggle('active',g)});if(e){const h=e.getAttribute('data-alt')||'',i=e.getAttribute('data-slug')||'';if(h&&a)a.alt=h;if(i&&a)a.dataset.slug=i}} 13183 if (c){ 13184 const targetName = _0x8s9t(url); 13185 const slideImg = Array.from(c.querySelectorAll('img')).find(im => _0x8s9t(im.src) === targetName); 13186 if (slideImg){ 13187 13188 try { 13189 const container = c; 13190 const imgRect = slideImg.getBoundingClientRect(); 13191 const contRect = container.getBoundingClientRect(); 13192 const offsetLeft = slideImg.offsetLeft - container.offsetLeft; 13193 const center = offsetLeft - (container.clientWidth - slideImg.clientWidth) / 2; 13194 container.scrollTo({ left: Math.max(0, center), behavior: 'smooth' }); 13195 } catch(_){ 13196 13197 slideImg.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); 13198 } 13199 } 13200 } 13201} 13202function findImageForVariant(variant){ 13203 if (!variant || !variant.attrs) return null; 13204 const tokens = []; 13205 try{ 13206 Object.values(variant.attrs).forEach(v=>{ 13207 if (!v) return; 13208 const raw = String(v); 13209 tokens.push(raw); 13210 tokens.push(normalize(raw)); 13211 }); 13212 }catch(_){} 13213 if (variant.sku){ 13214 tokens.push(String(variant.sku)); 13215 tokens.push(normalize(String(variant.sku))); 13216 } 13217 if (variant.image_url) { 13218 tokens.push(String(variant.image_url)); 13219 } 13220 let best = null; 13221 let bestScore = -1; 13222 13223 for (const g of (galleryData||[])){ 13224 const slugN = normalize(g.slug||''); 13225 const altN = normalize(g.alt||''); 13226 const file = _0x8s9t(g.url||''); 13227 const fileN = normalize(file); 13228 const imgU = g.url; 13229 13230 for (const t of tokens){ 13231 const tN = normalize(t); 13232 if (!tN) continue; 13233 13234 let score = -1; 13235 if (imgU === tN) score = 300; 13236 if (slugN && slugN === tN) score = 300; 13237 else if (fileN && fileN.includes(tN)) score = 200; 13238 else if (altN && altN.includes(tN)) score = 120; 13239 13240 if (score > bestScore){ 13241 bestScore = score; 13242 best = g; 13243 } 13244 if (bestScore >= 300) break; 13245 } 13246 if (bestScore >= 300) break; 13247 } 13248 return bestScore >= 120 ? best : null; 13249} 13250function _0xey3z(variant){const a=_0xcw2x(variant);if(a&&a.url){_0xau1v(a.url,a.alt||'')}} 13251
13252/* ====== Layout mobile ====== */ 13253(function _0xg04h(){const a=document.querySelector('.pd-media-card'),b=document.getElementById('pd-price-block'),c=document.getElementById('pd-price-anchor'),d=document.querySelector('.pd-info-card'),e=document.getElementById('pd-title'),f=document.getElementById('pd-variant-combined'),g=document.getElementById('pd-variants'),h=document.getElementById('pd-variants-anchor'),i=document.getElementById('pd-variant-summary'),j=document.getElementById('pd-summary-anchor'),titleRow=document.querySelector('.pd-title-row'),promoBanner=document.querySelector('.promo-endyear-mini-banner'),compareSection=document.querySelector('.pd-compare-section');if(!a||!b||!c||!d||!e)return; 13254 13255 let pricePlaced = false; 13256 let varPlaced = false; 13257 let titlePlaced = false; 13258 let promoPlaced = false; 13259 const mq = window.matchMedia('(max-width: 640px)'); 13260 13261 function apply(){ 13262 if (mq.matches) { 13263 if (!titlePlaced && a && titleRow && d.contains(titleRow)) { try{a.appendChild(titleRow);titlePlaced=true;}catch(_){} } 13264 if (!pricePlaced && a && b) { try{a.appendChild(b);pricePlaced=true;}catch(_){} } 13265 if (!varPlaced && f && d && e) { 13266 try{ 13267 f.hidden = false; 13268 if (i && !f.contains(i)) f.appendChild(i); 13269 if (g && !f.contains(g)) f.appendChild(g); 13270 if(d.contains(e))d.insertBefore(f, e); 13271 varPlaced = true; 13272 }catch(_){} 13273 } 13274 if (!promoPlaced && promoBanner && compareSection && compareSection.parentNode) { 13275 try{ 13276 compareSection.parentNode.insertBefore(promoBanner, compareSection); 13277 promoPlaced = true; 13278 }catch(_){} 13279 } 13280 } else { 13281 if (titlePlaced && titleRow && d) { 13282 try{ 13283 const vcombined = document.getElementById('pd-variant-combined'); 13284 if (vcombined && d.contains(vcombined)) { 13285 d.insertBefore(titleRow, vcombined); 13286 } else { 13287 d.insertBefore(titleRow, d.firstChild); 13288 } 13289 titlePlaced=false; 13290 }catch(_){} 13291 } 13292 if (pricePlaced && b && c && c.parentNode) { 13293 try{c.parentNode.insertBefore(b, c.nextSibling);pricePlaced=false;}catch(_){} 13294 } 13295 if (varPlaced) { 13296 try{ 13297 if (i && j && j.parentNode && !j.parentNode.contains(i)) j.parentNode.insertBefore(i, j.nextSibling); 13298 if (g && h && h.parentNode && !h.parentNode.contains(g)) h.parentNode.insertBefore(g, h.nextSibling); 13299 if (f) { f.hidden = true; f.innerHTML = ''; } 13300 varPlaced = false; 13301 }catch(_){} 13302 } 13303 if (promoPlaced && promoBanner && b) { 13304 try{ 13305 b.appendChild(promoBanner); 13306 promoPlaced = false; 13307 }catch(_){} 13308 } 13309 } 13310 } 13311 13312 mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply); 13313 apply(); 13314 13315 const fab = document.getElementById('pd-mobile-fab'); 13316 if (fab) { 13317 const pid = parseInt(fab.getAttribute('data-pid')||'0',10) || 325; 13318 const btnCart = document.getElementById('mf-cart'); 13319 13320 if (btnCart) btnCart.addEventListener('click', function(){ 13321 const varInput = document.getElementById('pd-variant-id'); 13322 const variantId = varInput ? parseInt(varInput.value||'0',10) : 0; 13323 try { if (window.addToCart) addToCart(pid, 1, variantId); } catch(_){} 13324 const variantText = getVariantSummaryText(); 13325 const skuText = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim():PD_SKU; })(); 13326 showToast({ 13327 type: 'success', 13328 title: 'Berhasil ditambahkan ke Keranjang', 13329 message: `<strong>${escapeHtml(PD_NAME)}</strong>${variantText?` · <em>${escapeHtml(variantText)}</em>`:''}<br>SKU: ${escapeHtml(skuText)} · Qty: 1`, 13330 duration: 5000, 13331 actions: [ 13332 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 13333 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 13334 ] 13335 }); 13336 }); 13337 } 13338})(); 13339 13340/* ====== Auto-hide installment banner if sidebar too tall (Desktop only) ====== */ 13341(function(){ 13342 function adjustSidebarBanner() { 13343 if (window.innerWidth <= 768) return; 13344 const sidebar = document.querySelector('.pd-sidebar'); 13345 const infoCard = document.querySelector('.pd-info-card'); 13346 const banner = document.querySelector('.pd-side-card.inst-alt-banner.inst-alt-banner--compact'); 13347 if (!sidebar || !infoCard || !banner) return; 13348 const sidebarHeight = sidebar.offsetHeight; 13349 const infoCardHeight = infoCard.offsetHeight; 13350 if (sidebarHeight > infoCardHeight) { 13351 banner.style.display = 'none'; 13352 } else { 13353 banner.style.display = ''; 13354 } 13355 } 13356 if (document.readyState === 'loading') { 13357 document.addEventListener('DOMContentLoaded', adjustSidebarBanner); 13358 } else { 13359 adjustSidebarBanner(); 13360 } 13361 let resizeTimer; 13362 window.addEventListener('resize', function() { 13363 clearTimeout(resizeTimer); 13364 resizeTimer = setTimeout(adjustSidebarBanner, 250); 13365 }); 13366 const pwpSection = document.getElementById('pwp-deals-section'); 13367 if (pwpSection) { 13368 const observer = new MutationObserver(function(mutations) {
13369 let shouldRecheck = false; 13370 mutations.forEach(function(mutation) { 13371 if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') { 13372 shouldRecheck = true; 13373 } else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 13374 shouldRecheck = true; 13375 } 13376 }); 13377 if (shouldRecheck) { 13378 setTimeout(adjustSidebarBanner, 100); 13379 } 13380 }); 13381 observer.observe(pwpSection, { 13382 attributes: true, 13383 childList: true, 13384 subtree: true 13385 }); 13386 } 13387 window.adjustSidebarBanner = adjustSidebarBanner; 13388})(); 13389 13390/* ====== Global constants ====== */ 13391const IS_DIGITAL=false; 13392const IS_PREORDER=false; 13393 13394/* ====== Variants state (REVISED) ====== */ 13395(function initVariants(){ 13396const _0x7g8h=[];window.variants=_0x7g8h; 13397window.isPreorderProduct=false; 13398 const attrOrder = []; 13399 13400 if (!variants.length || !attrOrder.length) return; 13401 13402 function comboKey(sel){ 13403 return attrOrder.map(a => a + '=' + (sel[a] ?? '')).join('|'); 13404 } 13405 13406 const variantMap = {};
13407 variants.forEach(v=>{ 13408 const key = comboKey(v.attrs); 13409 variantMap[key] = v; 13410 }); 13411 13412 function readSelection(){ 13413 const sel = {}; 13414 attrOrder.forEach(a=>{ 13415 const r = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio]:checked`); 13416 sel[a] = r ? r.value : ''; 13417 }); 13418 return sel; 13419 } 13420 13421 function findVariant(sel){ 13422 return variantMap[comboKey(sel)] || null; 13423 } 13424 13425 function autoFillMissing(sel){ 13426 let changed = false; 13427 for (const a of attrOrder) { 13428 if (sel[a]) continue; 13429 const possible = []; 13430 const seen = new Set(); 13431 variants.forEach(v=>{ 13432 let ok = true; 13433 for (const a2 of attrOrder) { 13434 if (a2 === a) continue; 13435 if (sel[a2] && v.attrs[a2] !== sel[a2]) { ok = false; break; } 13436 } 13437 if (ok) { 13438 const val = v.attrs[a]; 13439 if (!seen.has(val)) { seen.add(val); possible.push(val); } 13440 } 13441 }); 13442 if (possible.length === 1) { 13443 const val = possible[0]; 13444 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13445 if (input && !input.checked) { input.checked = true; sel[a] = val; changed = true; } 13446 } 13447 } 13448 return changed; 13449 } 13450 13451 function rupiah(n){ 13452 n = parseInt(n||0,10)||0; 13453 return 'Rp ' + n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.'); 13454 } 13455 13456 function focusGalleryByUrl(imageUrl) { 13457 const isMobile = window.matchMedia && window.matchMedia('(max-width: 640px)').matches; 13458 if (isMobile) { 13459 _0xau1v(imageUrl); 13460 return; 13461 } 13462 13463 const gallery = document.getElementById('pd-thumb-gallery'); 13464 const targetName = _0x8s9t(imageUrl); 13465 13466 if (gallery) { 13467 const btns = Array.from(gallery.querySelectorAll('.pd-thumb-btn')); 13468 const btn = btns.find(b => _0x8s9t(b.getAttribute('data-full') || '') === targetName); 13469 if (btn) { btn.click(); return; } 13470 } 13471 13472 _0xau1v(imageUrl); 13473 } 13474 13475 13476function formatStockLabel(n){n=parseInt(n||0,10)||0;return(n>10)?'10+':String(n)} 13477function updateBranchPopover(v){try{if(!v)return;const a=document.getElementById('sp-qty-meg'),b=document.getElementById('sp-qty-mdf'),c=document.getElementById('sp-qty-bda');if(a)a.textContent=formatStockLabel(Math.max(0,v.smm|0));if(b)b.textContent=formatStockLabel(Math.max(0,v.smf|0));if(c)c.textContent=formatStockLabel(Math.max(0,v.sba|0))}catch(_){}} 13478 13479 function calcVoucherDisc(type,value,price,maxDisc){if(type==='PERCENTAGE'){let d=(price*value)/100;if(maxDisc>0&&d>maxDisc)d=maxDisc;return Math.round(d)}return Math.round(value)} 13480 13481 function applyVariant(v, focusImage = true){ 13482 const elCur = document.getElementById('pd-price-current'); 13483 const elOri = document.getElementById('pd-price-original'); 13484 const elDisc = document.getElementById('pd-discount-badge'); 13485 const elSku = document.getElementById('pd-sku'); 13486 const elStock= document.getElementById('pd-stock-badge'); 13487 const elQty = document.getElementById('qty'); 13488 const btnAdd = document.getElementById('btn-add-cart'); 13489 const btnBuy = document.getElementById('btn-buy'); 13490 const hidVar = document.getElementById('pd-variant-id'); 13491 13492 const mfCart= document.getElementById('mf-cart'); 13493 const mfBuy = document.getElementById('mf-buy'); 13494 const fab = document.getElementById('pd-mobile-fab'); 13495 13496 if (hidVar) hidVar.value = v ? v.id : 0; 13497 if (elCur) elCur.textContent = rupiah(v ? v.final : 0); 13498 if (elSku && v && v.sku) elSku.textContent = v.sku; 13499 13500 if (v && v.ori > v.final) { 13501 if (elOri) { elOri.style.display='inline'; elOri.textContent = rupiah(v.ori); } 13502 if (elDisc) { elDisc.style.display='inline'; elDisc.textContent = '-' + (v.disc||0) + '%'; } 13503 } else { 13504 if (elOri) elOri.style.display='none'; 13505 if (elDisc) elDisc.style.display='none'; 13506 } 13507 13508 const stock = v ? (parseInt(v.stock||0,10)||0) : 0; 13509 let effectiveStock = stock; 13510 let isActivePreorder = false;
13511 if (window.isPreorderProduct && v && 'preorder_remaining' in v && v.preorder_remaining !== null) { 13512 effectiveStock = parseInt(v.preorder_remaining, 10); 13513 if (isNaN(effectiveStock)) effectiveStock = 0; 13514 isActivePreorder = true; 13515 } 13516 if (elStock){ 13517 elStock.classList.remove('in','out'); 13518 if (effectiveStock>0){ 13519 elStock.classList.add('in'); 13520 elStock.textContent = IS_DIGITAL ? 'Tersedia' : (isActivePreorder ? 'Sisa Slot: ' + effectiveStock : 'Stok: ' + formatStockLabel(effectiveStock)); 13521 } else { 13522 elStock.classList.add('out'); 13523 elStock.textContent = 'Habis'; 13524 } 13525 } 13526 if (elQty){ 13527 elQty.max = String(effectiveStock>0?effectiveStock:0); 13528 if (effectiveStock<=0) { elQty.value = '0'; elQty.disabled = true; } 13529 else { if (parseInt(elQty.value||'1',10) < 1) elQty.value='1'; elQty.disabled=false; } 13530 } 13531 13532 const shouldDisable = !(v && effectiveStock>0); 13533 const updateButtons = () => { 13534 const btnAddCart = document.getElementById('btn-add-cart'); 13535 const btnBuyNow = document.getElementById('btn-buy'); 13536 const mobileCart = document.getElementById('mf-cart'); 13537 const mobileBuy = document.getElementById('mf-buy'); 13538 [btnAddCart, btnBuyNow, mobileCart, mobileBuy].forEach(btn => { 13539 if (!btn) return; 13540 if (shouldDisable) { 13541 btn.disabled = true; 13542 btn.setAttribute('disabled', 'disabled'); 13543 btn.classList.add('stock-disabled'); 13544 btn.setAttribute('data-stock-blocked', 'true'); 13545 btn.setAttribute('data-stock-reason', window.isPreorderProduct ? 'Pre-order habis' : 'Stok habis'); 13546 } else { 13547 btn.disabled = false; 13548 btn.removeAttribute('disabled'); 13549 btn.classList.remove('stock-disabled'); 13550 btn.removeAttribute('data-stock-blocked'); 13551 btn.removeAttribute('data-stock-reason'); 13552 } 13553 }); 13554 }; 13555 updateButtons(); 13556 setTimeout(updateButtons, 10); 13557 setTimeout(updateButtons, 50); 13558 setTimeout(updateButtons, 100); 13559 if (mfBuy && v) { 13560 const voucherBtn = document.getElementById('btn-buy-voucher'); 13561 const voucherCode = fab ? fab.dataset.voucherCode : ''; 13562 let voucherDiscount = 0; 13563 13564 if (voucherBtn && voucherCode) { 13565 const vType = voucherBtn.dataset.voucherType; 13566 const vValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 13567 const vMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 13568 const finalPrice = v.final || 0; 13569 if (vType && vValue > 0) { 13570 voucherDiscount = calcVoucherDisc(vType, vValue, finalPrice, vMax); 13571 if (fab) fab.dataset.voucherValue = voucherDiscount.toString(); 13572 } 13573 } 13574 13575 const finalPrice = v.final || 0; 13576 const textEl = mfBuy.querySelector('.mf-buy-text'); 13577 const priceEl = mfBuy.querySelector('.mf-buy-price'); 13578 13579 if (effectiveStock > 0) { 13580 if (IS_PREORDER) { 13581 if (textEl) textEl.textContent = 'Pre Order Sekarang'; 13582 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13583 } else if (voucherCode && voucherDiscount > 0) { 13584 if (textEl) textEl.textContent = 'Beli dengan Voucher'; 13585 if (priceEl) priceEl.textContent = rupiah(Math.max(0, finalPrice - voucherDiscount)); 13586 } else { 13587 if (textEl) textEl.textContent = 'Beli Sekarang'; 13588 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13589 } 13590 } else { 13591 if (textEl) textEl.textContent = 'Stok Habis'; 13592 if (priceEl) priceEl.textContent = rupiah(finalPrice); 13593 } 13594 } 13595 13596 const isMobile = window.initMobile ?? 0; 13597 if (window.instSetBasePrice) window.instSetBasePrice(v ? (isMobile == '1' ? v.ori : v.final) : 0); 13598 if (window.updateSoldBySku) window.updateSoldBySku(v.sku); 13599 updateBranchPopover(v); 13600 if (v && v.image_url && focusImage) focusGalleryByUrl(v.image_url); 13601 if (v) { 13602 window.dispatchEvent(new CustomEvent('pd-variant-changed', { 13603 detail: { 13604 price: v.final,
13605 originalPrice: v.ori, 13606 discount: v.disc, 13607 stock: effectiveStock, 13608 sku: v.sku, 13609 variant_id: v.id 13610 } 13611 })); 13612 13613 const encodedHash = encodeVariantHash(v.id); 13614 if (encodedHash && window.history && window.history.replaceState) { 13615 const newHash = '#v=' + encodedHash; 13616 const newUrl = window.location.pathname + window.location.search + newHash; 13617 window.history.replaceState(null, '', newUrl); 13618 } 13619 } 13620 } 13621 13622 let sel = readSelection(); 13623 if (autoFillMissing(sel)) sel = readSelection(); 13624 13625 let v = findVariant(sel); 13626 if (!v) { 13627 const candidates = variants.filter(variant=>{ 13628 return attrOrder.every(a=>{ 13629 if (!sel[a]) return true; 13630 return variant.attrs[a] === sel[a]; 13631 }); 13632 }); 13633 if (candidates.length >= 1) { 13634 v = candidates[0]; 13635 attrOrder.forEach(a=> sel[a] = v.attrs[a]); 13636 attrOrder.forEach(a=>{ 13637 const val = sel[a]; 13638 const input = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"] input[type=radio][value="${CSS.escape(val)}"]`); 13639 if (input) input.checked = true; 13640 }); 13641 } 13642 } 13643 13644 const initSel = readSelection(); 13645 const initVar = findVariant(initSel) || variants[0] || null; 13646 if (document.readyState === 'loading') { 13647 document.addEventListener('DOMContentLoaded', () => applyVariant(initVar, false)); 13648 } else { 13649 applyVariant(initVar, false); 13650 } 13651 13652function updateSummary(sel){Object.entries(sel).forEach(([a,val])=>{const el=document.getElementById('vs-'+a);if(el)el.textContent=val||'-'})} 13653 function refreshOptions(sel){ 13654 attrOrder.forEach(a=>{ 13655 const group = document.querySelector(`.v-group[data-attr="${CSS.escape(a)}"]`); 13656 if (!group) return; 13657 const chips = group.querySelectorAll('.v-chip'); 13658 chips.forEach(ch=>{ 13659 const val = ch.getAttribute('data-value'); 13660 const possible = variants.some(v=>{ 13661 if (v.attrs[a] !== val) return false; 13662 for (const a2 of attrOrder) { 13663 if (a2 === a) continue; 13664 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13665 } 13666 return true; 13667 }); 13668 const input = ch.querySelector('input[type=radio]'); 13669 const disabled = !possible; 13670 if (input) input.disabled = disabled; 13671 ch.classList.toggle('disabled', disabled); 13672 13673 let candidateVariant = null; 13674 if (possible) { 13675 candidateVariant = variants.find(v=>{ 13676 if (v.attrs[a] !== val) return false; 13677 for (const a2 of attrOrder) { 13678 if (a2 === a) continue; 13679 if (sel[a2] && v.attrs[a2] !== sel[a2]) return false; 13680 } 13681 return true; 13682 }); 13683 } 13684 const outOfStock = candidateVariant ? (window.isPreorderProduct && candidateVariant.preorder_remaining !== null ? candidateVariant.preorder_remaining <= 0 : candidateVariant.stock <= 0) : false; 13685 ch.classList.toggle('out', !disabled && outOfStock); 13686 const badge = ch.querySelector('.chip-badge'); 13687 if (badge) badge.hidden = !(candidateVariant && outOfStock); 13688 }); 13689 group.querySelectorAll('.v-chip').forEach(ch=>{ 13690 const input = ch.querySelector('input[type=radio]'); 13691 ch.classList.toggle('selected', input && input.checked); 13692 }); 13693 }); 13694 } 13695 updateSummary(sel); 13696 refreshOptions(sel); 13697 document.querySelectorAll('.v-chip').forEach(chip=>{const input=chip.querySelector('input[type=radio]');if(input&&input.checked){chip.classList.add('selected');}}); 13698 13699 const container = document.getElementById('pd-variants'); 13700 container && container.addEventListener('change', function(ev){ 13701 const radio = ev.target.closest('input[type=radio]'); 13702 if (!radio) return; 13703 sel = readSelection(); 13704 if (autoFillMissing(sel)) sel = readSelection(); 13705 const chosen = findVariant(sel); 13706 applyVariant(chosen || null, true); 13707 13708 updateSummary(sel); 13709 refreshOptions(sel); 13710 }); 13711 13712window.applyVariant=applyVariant; 13713})(); 13714 13715(function cleanBrokenDescImages(){ 13716 const containers = [ 13717 document.querySelector('.pd-full-desc-body'), 13718 document.querySelector('.pd-short-desc')
13719 ].filter(Boolean); 13720 13721 containers.forEach(root => { 13722 const imgs = root.querySelectorAll('img'); 13723 imgs.forEach(img => { 13724 if (img.complete && img.naturalWidth === 0) { 13725 img.remove(); 13726 return; 13727 } 13728 img.addEventListener('error', () => { 13729 img.remove(); 13730 }, { once:true }); 13731 }); 13732 }); 13733})(); 13734 13735/* ====== Scroll To Top ====== */ 13736(function _0xi25j(){const a=document.getElementById('pd-scroll-top');if(!a)return;function b(){if(window.matchMedia('(max-width:640px)').matches){const d=document.querySelector('.bottom-nav'),e=document.querySelector('.pd-mobile-fab');if(!d&&!e){setTimeout(b,100);return}const f=d&&window.getComputedStyle(d).display!=='none'&&window.getComputedStyle(d).visibility!=='hidden',g=e&&window.getComputedStyle(e).display!=='none'&&window.getComputedStyle(e).visibility!=='hidden';let h;if(f&&g){h='calc(68px + 54px + env(safe-area-inset-bottom))'}else if(g){h='calc(68px + 8px + env(safe-area-inset-bottom))'}else if(f){h='calc(60px + env(safe-area-inset-bottom))'}else{h='calc(20px + env(safe-area-inset-bottom))'}a.style.setProperty('--scroll-top-bottom',h)}else{a.style.removeProperty('--scroll-top-bottom')}}function c(){const d=window.pageYOffset||document.documentElement.scrollTop||0,e=d>300;a.classList.toggle('visible',e);a.setAttribute('aria-hidden',e?'false':'true');b()}window.addEventListener('scroll',c,{passive:!0});window.addEventListener('resize',b,{passive:!0});setTimeout(b,100);c();a.addEventListener('click',function(){window.scrollTo({top:0,behavior:'smooth'})})})(); 13737 13738/* Format stok 10+ */ 13739function _0xk67l(n){n=parseInt(n||0,10)||0;return n>10?'10+':String(n)} 13740 13741/* Toggle popover stok per cabang */ 13742(function _0xm89n(){const a=document.getElementById('pd-stock-info-btn'),b=document.getElementById('pd-stock-popover'),c=document.getElementById('pd-stock-popover-close');if(!a||!b)return;function d(){if(window.matchMedia('(min-width: 641px)').matches){const e=document.getElementById('pd-stock-badge');if(!e)return;const f=e.getBoundingClientRect(),g=window.pageXOffset||document.documentElement.scrollLeft||0,h=window.pageYOffset||document.documentElement.scrollTop||0;b.style.left=(f.left+g-10)+'px';b.style.top=(f.bottom+h+10)+'px'}else{b.style.left='';b.style.top=''}} 13743function e(){d();b.classList.add('show');a.setAttribute('aria-expanded','true');b.setAttribute('aria-hidden','false');document.addEventListener('click',g,{capture:true});window.addEventListener('resize',d,{passive:true});window.addEventListener('scroll',d,{passive:true})}function f(){b.classList.remove('show');a.setAttribute('aria-expanded','false');b.setAttribute('aria-hidden','true');document.removeEventListener('click',g,{capture:true});window.removeEventListener('resize',d);window.removeEventListener('scroll',d)}function g(h){if(b.contains(h.target)||a.contains(h.target))return;f()} 13744a.addEventListener('click',function(h){h.preventDefault();b.classList.contains('show')?f():e()});if(c)c.addEventListener('click',f)})(); 13745 13746/* ===== Share Product Modal ===== */ 13747(function(){const a=document.getElementById('pd-share-btn'),z=document.getElementById('mf-share'),b=document.getElementById('pd-share-modal-overlay'),c=document.getElementById('pd-share-close'),d=document.getElementById('pd-share-copy-url-btn'),e=document.getElementById('pd-share-url-input');if((!a&&!z)||!b)return;const f="https://loklokpro.com/",g="Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y",h="Rp 20.789.000";const discountPercent=1;let i='';if(discountPercent>0){i=`ð¥ Lagi Promo Nih! Diskon ${discountPercent}%\n\n⨠${g}\nð° Cuma ${h} aja!\n\nð Buruan cek sebelum kehabisan!\nð `}else{i=`⨠Ada barang bagus nih!\n\nð¦ ${g}\nð¸ Harga: ${h}\n\nðï¸ Yuk, langsung cek!\nð `}function j(){b.classList.add('show');document.body.style.overflow='hidden'}function k(){b.classList.remove('show');document.body.style.overflow=''}if(a)a.addEventListener('click',j);if(z)z.addEventListener('click',j);if(c)c.addEventListener('click',k);b.addEventListener('click',function(l){if(l.target===b)k()});document.addEventListener('keydown',function(l){if(l.key==='Escape'&&b.classList.contains('show'))k()});b.addEventListener('click',function(l){const m=l.target.closest('[data-share]');if(!m)return;const n=m.getAttribute('data-share'),o=encodeURIComponent(f),p=e
13747ncodeURIComponent(i),q=encodeURIComponent(g);let r='';switch(n){case 'whatsapp':r=`https://wa.me/?text=${p}${o}`;break;case 'facebook':r=`https://www.facebook.com/sharer/sharer.php?u=${o}`;break;case 'twitter':r=`https://twitter.com/intent/tweet?text=${p}${o}`;break;case 'telegram':r=`https://t.me/share/url?url=${o}&text=${p}`;break;case 'email':r=`mailto:?subject=${encodeURIComponent('ð¥ Cek Produk Keren Ini!')}&body=${p}${o}`;window.location.href=r;k();return;case 'copy':s();return;case 'pdf':const ps=document.querySelector('.product-single');if(ps){ps.setAttribute('data-print-date',new Date().toLocaleDateString('id-ID',{year:'numeric',month:'long',day:'numeric'}))}k();setTimeout(function(){window.print();try{if(window.ga4Ecomm&&window.ga4Ecomm.trackEvent)window.ga4Ecomm.trackEvent('share',{method:'pdf_export',content_type:'product',item_id:"SB0B3T"})}catch(_){}},300);return}if(r){window.open(r,'_blank','noopener,noreferrer');try{if(window.ga4Ecomm&&window.ga4Ecomm.trackEvent)window.ga4Ecomm.trackEvent('share',{method:n,content_type:'product',item_id:"SB0B3T"})}catch(_){}}});function s(){if(!e)return;e.select();e.setSelectionRange(0,99999);try{if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(f).then(function(){t()}).catch(function(){document.execCommand('copy');t()})}else{document.execCommand('copy');t()}}catch(u){console.error('Failed to copy:',u)}}function t(){if(!d)return;const v=d.innerHTML;d.classList.add('copied');d.innerHTML='<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg><span class="pd-share-copy-text">Tersalin!</span>';if(window.showToast)showToast({type:'success',title:'⨠Link Tersalin!',message:'Link produk berhasil disalin. Yuk bagikan ke teman!',duration:3000});setTimeout(function(){d.classList.remove('copied');d.innerHTML=v},2000)}if(d)d.addEventListener('click',s);if(navigator.share){const shareHandler=function(w){w.preventDefault();navigator.share({title:g,text:i+f,url:f}).catch(function(x){console.log('Native share cancelled or failed:',x)})};if(a)a.addEventListener('dblclick',shareHandler);if(z)z.addEventListener('dblclick',shareHandler)}})(); 13748 13749/* ===== Embedding variants (server â client) ===== */ 13750(function(){ 13751 window.__PD_VARIANTS_WITH_BRANCH__ = []; 13752})(); 13753 13754/* ===== Helper: update angka popover ===== */ 13755function updateStockPopoverForVariant(v){ 13756 try{ 13757 const elMeg = document.getElementById('sp-qty-meg'); 13758 const elMdf = document.getElementById('sp-qty-mdf'); 13759 const elBda = document.getElementById('sp-qty-bda'); 13760 13761 if (typeof v?.smm === 'number' && elMeg) elMeg.textContent = formatStockLabel(Math.max(0, v.smm|0)); 13762 if (typeof v?.smf === 'number' && elMdf) elMdf.textContent = formatStockLabel(Math.max(0, v.smf|0)); 13763 if (typeof v?.sba === 'number' && elBda) elBda.textContent = formatStockLabel(Math.max(0, v.sba|0)); 13764 }catch(_){} 13765} 13766 13767/* ===== applyVariant: 10+ dan update popover ===== */ 13768(function patchApplyVariant(){ 13769 if (typeof applyVariant !== 'function') return; 13770 const original = applyVariant; 13771 window.applyVariant = function(v){ 13772 original(v); 13773 13774 try { 13775 const elStock = document.getElementById('pd-stock-badge'); 13776 if (elStock){ 13777 let txt = elStock.textContent || ''; 13778 let m = txt.match(/\d+/); 13779 if (m) { 13780 const n = parseInt(m[0]||'0',10)||0; 13781 elStock.textContent = (n>0) ? ('Stok: ' + formatStockLabel(n)) : 'Habis'; 13782 } 13783 } 13784 } catch(_){} 13785if(window.__PD_VARIANTS_WITH_BRANCH__&&v){const vv=window.__PD_VARIANTS_WITH_BRANCH__.find(x=>x.id===v.id)||v;updateStockPopoverForVariant(vv)} 13786 }; 13787})(); 13788 13789(function initCurrentVariantBranch(){ 13790 const list = window.__PD_VARIANTS_WITH_BRANCH__ || []; 13791 if (!list.length) return; 13792 const hid = document.getElementById('pd-variant-id'); 13793 const vid = hid ? parseInt(hid.value||'0',10) : 0; 13794 const current = list.find(x => x.id === vid) || list[0] || null; 13795 if (current) updateStockPopoverForVariant(current); 13796})(); 13797 13798(function(){ 13799 if (!window.ga4Ecomm) return; 13800 const catSegs = ["Komputer & Laptop","Laptop","Gaming"]; 13801 window.ga4Ecomm.viewItem({ 13802 currency: 'IDR', 13803 value: 20789000, 13804 item_id: "SB0B3T", 13805 item_name: "Lenovo Legion Go 8ASP2 - 83N0000RID /AMD Ryzen Z2 Extreme/32GB/1TB SSD/AMD Radeon Graphics/8.8\" WUXGA OLED Touch/Win 11 Home/Eclipse Black/3Y", 13806 item_brand: "Lenovo ",
13807 item_category: catSegs && catSegs.length ? catSegs.join(' / ') : '' 13808 }); 13809 })(); 13810 13811(function patchApplyVariantFocus(){ 13812 if (typeof applyVariant !== 'function') return; 13813 const original = applyVariant; 13814 window.applyVariant = function(v, focusImage = true){ 13815 13816 original(v, focusImage); 13817 13818 if (focusImage && v && v.image_url) { 13819 _0xau1v(v.image_url); 13820 } 13821 }; 13822})(); 13823 13824(function() { 13825 var soldMap = []; 13826 var soldTotalLabel = "Terjual 2"; 13827 13828 function formatSoldLabel(qty) { 13829 if (qty >= 1000000) { 13830 var v = Math.floor((qty / 100000) ) / 10; 13831 return 'Terjual ' + (String(v).replace('.', ',')) + 'jt+'; 13832 } 13833 if (qty >= 1000) { 13834 var v2 = Math.floor((qty / 100) ) / 10; 13835 return 'Terjual ' + (String(v2).replace('.', ',')) + 'rb+'; 13836 } 13837 if (qty >= 100) { 13838 var v2 = Math.floor((qty / 100) ) * 100; 13839 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13840 } 13841 if (qty >= 10) { 13842 var v2 = Math.floor((qty / 10) ) * 10; 13843 return 'Terjual ' + (String(v2).replace('.', ',')) + '+'; 13844 } 13845 return 'Terjual ' + (qty.toLocaleString('id-ID')); 13846 } 13847 13848 function updateSoldLabel(labelText) { 13849 var el = document.querySelector('#pd-sold-inline, [data-role="sold-chip"]'); 13850 if (el) el.textContent = labelText; 13851 } 13852 13853 window.updateSoldBySku = function(sku) { 13854 if (!sku || typeof sku !== 'string') { 13855 updateSoldLabel(soldTotalLabel); 13856 return; 13857 } 13858 var qty = soldMap[sku] || 0; 13859 updateSoldLabel(formatSoldLabel(qty)); 13860 }; 13861 13862 updateSoldLabel(soldTotalLabel); 13863 })(); 13864 13865(function(){const h=document.getElementById('pd-sticky-header');if(!h)return;const p=document.getElementById('pd-price-anchor');const ac=document.getElementById('pd-sticky-add-cart');const by=document.getElementById('pd-sticky-buy');let lt=window.scrollY;let tk=false;function u(){if(!p)return;const r=p.getBoundingClientRect();const s=window.scrollY;if(r.top<0){h.classList.add('visible')}else{h.classList.remove('visible')}lt=s;tk=false}function rt(){if(!tk){window.requestAnimationFrame(u);tk=true}}window.addEventListener('scroll',rt,{passive:true});u();function sy(){const ma=document.getElementById('btn-add-cart');const mb=document.getElementById('btn-buy');if(ac&&ma){ac.disabled=ma.disabled}if(by&&mb){by.disabled=mb.disabled}}sy();const ob=new MutationObserver(sy);const ma=document.getElementById('btn-add-cart');const mb=document.getElementById('btn-buy');if(ma)ob.observe(ma,{attributes:true,attributeFilter:['disabled']});if(mb)ob.observe(mb,{attributes:true,attributeFilter:['disabled']});const ss=document.getElementById('pd-sticky-share-btn');const ms=document.getElementById('pd-share-btn');if(ss&&ms){ss.addEventListener('click',function(e){e.preventDefault();ms.click()})}if(ac){ac.addEventListener('click',function(e){e.preventDefault();if(ac.disabled)return;const m=document.getElementById('btn-add-cart');if(m&&!m.disabled){m.click()}})}if(by){by.addEventListener('click',function(e){e.preventDefault();if(by.disabled)return;const m=document.getElementById('btn-buy');if(m&&!m.disabled&&window.buyNow){window.buyNow(e)}})} 13866function getChatProductData(){const varInput=document.getElementById('pd-variant-id');const variantId=varInput?parseInt(varInput.value||'0',10):0;const variantSummary=getVariantSummaryText();const priceEl=document.getElementById('pd-price-current');const currentPrice=priceEl?priceEl.textContent.trim():'Rp 20.789.000';const pd={id:325,name:"Lenovo Legion Go 8ASP2 - 83N0000RID \/AMD Ryzen Z2 Extreme\/32GB\/1TB SSD\/AMD Radeon Graphics\/8.8\" WUXGA OLED Touch\/Win 11 Home\/Eclipse Black\/3Y",price:currentPrice,image:"\/uploads\/products\/lenovo-legion-go-8asp2-83n0000rid.webp",url:window.location.href};if(variantId>0){pd.variant_id=variantId}if(variantSummary){pd.variant_summary=variantSummary}return pd} 13867const ch=document.getElementById('pd-sticky-chat');if(ch){ch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}const dch=document.getElementById('btn-chat-icon');if(dch){dch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}const mch=document.getElementById('mf-chat');if(mch){mch.addEventListener('click',function(e){e.preventDefault();if(!window.plazaChat){console.warn('Chat widget not loaded');return}window.plazaChat.attachProduct(getChatProductData());window.plazaChat.openWidget()})}window.addEventListener('pd-variant-changed',function(e){const d=e.detail||{};if(d.price!==undefined){const sp=document.getElementById('pd-sticky-price');if(sp){sp.textContent=formatRupiah(d.price)}}if(d.originalPrice!==undefined&&d.originalPrice>d.price){const so=document.getElementById('pd-sticky-price-original');if(so){so.textContent=formatRupiah(d.originalPrice);so.style.display=''}}else{const so=document.getElementById('pd-sticky-price-original');if(so){so.style.display='none'}}if(d.discount!==undefined&&d.discount>0){const sd=document.getElementById('pd-sticky-discount');if(sd){sd.textContent='-'+d.discount+'%';sd.style.display=''}}else{const sd=document.getElementById('pd-sticky-discount');if(sd){sd.style.display='none'}}const hs=d.stock>0;if(ac){ac.disabled=!hs}if(by){by.disabled=!hs}});
13867function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}})(); 13868 13869(function(){const t=document.getElementById('pdv-toggle-btn');const v=document.getElementById('pd-voucher-section');if(t&&v){const h=v.querySelectorAll('.pdv-min-hidden');const n=h.length;t.addEventListener('click',function(){const e=t.classList.toggle('expanded');h.forEach(function(i){if(e){i.classList.remove('pdv-min-hidden')}else{i.classList.add('pdv-min-hidden')}});t.textContent=e?'Tutup':'+'+n})}const m=document.getElementById('voucher-detail-modal');const mc=document.getElementById('voucher-modal-close');const ma=document.getElementById('voucher-modal-cancel');const mcp=document.getElementById('voucher-modal-copy');if(m){const bb=document.querySelectorAll('.pdv-badge-btn');bb.forEach(function(btn){btn.addEventListener('click',function(){if(btn.disabled)return;const c=btn.getAttribute('data-voucher-code');const nm=btn.getAttribute('data-voucher-name');const dl=btn.getAttribute('data-discount-label');const bl=btn.getAttribute('data-badge-label');const mp=parseInt(btn.getAttribute('data-min-purchase'));const md=parseInt(btn.getAttribute('data-max-discount'));const vu=btn.getAttribute('data-valid-until');document.getElementById('voucher-modal-badge').innerHTML='<span class="vmb-value">'+dl+'</span><span class="vmb-label">'+bl+'</span>';document.getElementById('voucher-modal-code').textContent=c;document.getElementById('voucher-modal-name').textContent=nm;const me=document.getElementById('voucher-modal-min');if(mp>0){me.style.display='';document.getElementById('voucher-modal-min-val').textContent=fr(mp)}else{me.style.display='none'}const xe=document.getElementById('voucher-modal-max');if(md>0){xe.style.display='';document.getElementById('voucher-modal-max-val').textContent=fr(md)}else{xe.style.display='none'}if(vu){const d=new Date(vu);const ft=d.toLocaleDateString('id-ID',{day:'numeric',month:'long',year:'numeric',hour:'2-digit',minute:'2-digit'});document.getElementById('voucher-modal-valid-val').textContent=ft}mcp.setAttribute('data-code',c);m.classList.add('show');document.body.style.overflow='hidden'})});function cm(){m.classList.remove('show');document.body.style.overflow=''}if(mc)mc.addEventListener('click',cm);if(ma)ma.addEventListener('click',cm);m.addEventListener('click',function(e){if(e.target===m)cm()});if(mcp){mcp.addEventListener('click',function(){const c=mcp.getAttribute('data-code');if(!c)return;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(c).then(function(){smc()}).catch(function(err){fc(c)})}else{fc(c)}})}}const b=document.querySelectorAll('.pdv-min-copy');b.forEach(function(btn){btn.addEventListener('click',function(){if(btn.disabled)return;const code=btn.getAttribute('data-code');if(!code)return;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(code).then(function(){sc(btn)}).catch(function(err){fc(code,btn)})}else{fc(code,btn)}})});function fc(text,btn){const tx=document.createElement('textarea');tx.value=text;tx.style.position='fixed';tx.style.opacity='0';document.body.appendChild(tx);tx.select();try{document.execCommand('copy');if(btn)sc(btn);else smc()}catch(err){}document.body.removeChild(tx)}function sc(btn){const o=btn.textContent;btn.classList.add('copied');btn.textContent='â Tersalin';setTimeout(function(){btn.classList.remove('copied');btn.textContent=o},2000)}function smc(){const o=mcp.textContent;mcp.textContent='â Kode Tersalin';mcp.style.background='#22c55e';setTimeout(function(){mcp.textContent=o;mcp.style.background=''},2000)}function fr(a){return'Rp'+Math.floor(a).toLocaleString('id-ID')}})(); 13870</script>
vendor: 1 bytes, line 13870
13870
13871<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 13872window.PD_ID=325;window.PD_NAME=_0x3c4d;window.PD_SKU=_0x5e6f;window.PD_CATEGORY_PATH="Komputer & Laptop/Laptop/Gaming";window.PD_BEST_VOUCHER=null; 13873window.IS_DIGITAL=IS_DIGITAL; 13874window.isPreorderProduct=false; 13875window.PD_PREORDER_DATA=null; 13876</script>
13876 13877
13878<script data-inlined="pd-lightbox.js"> 13879// PlazaIT Product Lightbox (vanilla, no CDN) â v4 (swipe-down to close) 13880(function(){ 13881 const state = { 13882 items: [], 13883 open: false, 13884 idx: 0, 13885 scale: 1, 13886 minScale: 1, 13887 maxScale: 4, 13888 tx: 0, ty: 0, 13889 isDragging: false, 13890 dragStartX: 0, dragStartY: 0, 13891 baseTx: 0, baseTy: 0, 13892 touchStartX: 0, touchStartY: 0, 13893 lastTap: 0, 13894 swipeMode: 'none', // 'none' | 'h' | 'v' 13895 totalDx: 0, totalDy: 0 13896 }; 13897 13898 let root, imgEl, counterEl, btnPrev, btnNext, btnClose, canvas; 13899 13900 const ORIGIN = (location.origin || (location.protocol + '//' + location.host)); 13901 13902 function absUrl(u){ 13903 if (!u) return ''; 13904 try { 13905 if (/^https?:\/\//i.test(u)) return u; 13906 return new URL(u, ORIGIN).href; 13907 } catch(_) { return u; } 13908 } 13909 function fileName(u){ 13910 try { return new URL(u, ORIGIN).pathname.split('/').pop().toLowerCase(); } 13911 catch(_){ return String(u||'').split('/').pop().toLowerCase(); } 13912 } 13913 13914 function buildItems(){ 13915 const gd = (window.galleryData || []).filter(Boolean); 13916 if (gd.length){ 13917 state.items = gd.map(g => ({ 13918 src: absUrl(g.url), alt: g.alt || '', slug: g.slug || '', w: g.w || 1200, h: g.h || 1200 13919 })).filter(it => !!it.src); 13920 return; 13921 } 13922 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 13923 state.items = Array.from(thumbs).map(btn => { 13924 const raw = btn.getAttribute('data-full') || ''; 13925 return { src: absUrl(raw), alt: btn.getAttribute('data-alt') || '', slug: btn.getAttribute('data-slug') || '', w: 1200, h: 1200 }; 13926 }).filter(it => !!it.src); 13927 } 13928 13929 function ensureUI(){ 13930 if (root) return; 13931 root = document.createElement('div'); 13932 root.className = 'pd-lb'; 13933 root.id = 'pd-lightbox'; 13934 root.setAttribute('role','dialog'); 13935 root.setAttribute('aria-modal','true'); 13936 root.setAttribute('aria-hidden','true'); 13937 root.setAttribute('tabindex','-1'); 13938 13939 // Canvas terlebih dulu (z-index di CSS membuat tombol di atas) 13940 root.innerHTML = ` 13941 <div class="pd-lb-canvas"><img class="pd-lb-img" id="pd-lb-img" alt=""></div> 13942 <button class="pd-lb-close" aria-label="Tutup" title="Tutup">×</button> 13943 <button class="pd-lb-prev" aria-label="Sebelumnya" title="Sebelumnya">❮</button> 13944 <button class="pd-lb-next" aria-label="Berikutnya" title="Berikutnya">❯</button> 13945 <div class="pd-lb-counter" id="pd-lb-counter">1 / 1</div> 13946 `; 13947 document.body.appendChild(root); 13948 13949 imgEl = root.querySelector('#pd-lb-img'); 13950 counterEl = root.querySelector('#pd-lb-counter'); 13951 btnPrev = root.querySelector('.pd-lb-prev'); 13952 btnNext = root.querySelector('.pd-lb-next'); 13953 btnClose = root.querySelector('.pd-lb-close'); 13954 canvas = root.querySelector('.pd-lb-canvas'); 13955 13956 // Button clicks 13957 btnPrev.addEventListener('click', (e)=>{ e.stopPropagation(); prev(); }); 13958 btnNext.addEventListener('click', (e)=>{ e.stopPropagation(); next(); }); 13959 btnClose.addEventListener('click', (e)=>{ e.stopPropagation(); close(); }); 13960 13961 // Klik backdrop untuk tutup 13962 root.addEventListener('click', (e)=>{ if (e.target === root) close(); }); 13963 13964 // Keyboard global 13965 document.addEventListener('keydown', (e)=>{ 13966 if (!state.open) return; 13967 if (e.key === 'Escape') { e.preventDefault(); close(); } 13968 if (e.key === 'ArrowRight') { e.preventDefault(); next(); } 13969 if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } 13970 }); 13971 13972 // Wheel zoom (desktop) 13973 root.addEventListener('wheel', (e)=>{ 13974 if (!state.open) return; 13975 e.preventDefault(); 13976 const delta = -Math.sign(e.deltaY) * 0.2; 13977 zoomAt(delta, e.clientX, e.clientY); 13978 }, { passive: false }); 13979 13980 // Drag pan (mouse) 13981 canvas.addEventListener('mousedown', (e)=>{ 13982 if (state.scale <= 1) return; 13983 state.isDragging = true; 13984 state.dragStartX = e.clientX; 13985 state.dragStartY = e.clientY; 13986 state.baseTx = state.tx; 13987 state.baseTy = state.ty; 13988 e.preventDefault(); 13989 }); 13990 window.addEventListener('mousemove', (e)=>{ 13991 if (!state.isDragging) return; 13992 const dx = e.clientX - state.dragStartX; 13993 const dy = e.clientY - state.dragStartY; 13994 setTranslate(state.baseTx + dx, state.baseTy + dy); 13995 }); 13996 window.addEventListener('mouseup', ()=>{ state.isDragging = false; }); 13997 13998 // Touch: swipe navigate (H), swipe-down close (V), double-tap zoom, drag pan 13999 canvas.addEventListener('touchstart', (e)=>{
14000 if (!state.open) return; 14001 if (e.touches.length === 1) { 14002 const t = e.touches[0]; 14003 state.touchStartX = t.clientX; 14004 state.touchStartY = t.clientY; 14005 state.totalDx = 0; 14006 state.totalDy = 0; 14007 state.swipeMode = 'none'; 14008 14009 state.isDragging = state.scale > 1; 14010 state.dragStartX = t.clientX; 14011 state.dragStartY = t.clientY; 14012 state.baseTx = state.tx; 14013 state.baseTy = state.ty; 14014 14015 // Double-tap toggle zoom 14016 const now = Date.now(); 14017 if (now - state.lastTap < 300) { 14018 if (state.scale > 1) resetTransform(); else zoomAt(+1, t.clientX, t.clientY, 2); 14019 state.lastTap = 0; 14020 e.preventDefault(); 14021 return; 14022 } 14023 state.lastTap = now; 14024 } 14025 }, { passive: true }); 14026 14027 canvas.addEventListener('touchmove', (e)=>{ 14028 if (!state.open) return; 14029 14030 // Drag pan saat zoom 14031 if (state.isDragging && e.touches.length === 1 && state.scale > 1) { 14032 e.preventDefault(); 14033 const t = e.touches[0]; 14034 const dx = t.clientX - state.dragStartX; 14035 const dy = t.clientY - state.dragStartY; 14036 setTranslate(state.baseTx + dx, state.baseTy + dy); 14037 return; 14038 } 14039 14040 // Gesture saat scale <= 1 14041 if (state.scale <= 1 && e.touches.length === 1) { 14042 const t = e.touches[0]; 14043 const dx = t.clientX - state.touchStartX; 14044 const dy = t.clientY - state.touchStartY; 14045 state.totalDx = dx; state.totalDy = dy; 14046 14047 // Tentukan mode swipe di awal gerakan 14048 if (state.swipeMode === 'none') { 14049 const absX = Math.abs(dx), absY = Math.abs(dy); 14050 if (absY > 12 && absY > absX * 1.3) { 14051 state.swipeMode = 'v'; // swipe vertical (untuk close) 14052 } else if (absX > 12 && absX > absY * 1.3) { 14053 state.swipeMode = 'h'; // swipe horizontal (ganti slide) 14054 } 14055 } 14056 14057 if (state.swipeMode === 'v') { 14058 // Ikuti jari, kurangi opacity untuk feedback 14059 e.preventDefault(); 14060 setRootDrag(dy); 14061 } else if (state.swipeMode === 'h') { 14062 // Biarkan sampai touchend untuk menentukan next/prev (tidak perlu preventDefault di sini) 14063 } 14064 } 14065 }, { passive: false }); 14066 14067 canvas.addEventListener('touchend', (e)=>{ 14068 if (!state.open) return; 14069 14070 if (state.scale <= 1) { 14071 const dx = state.totalDx; 14072 const dy = state.totalDy; 14073 14074 if (state.swipeMode === 'v') { 14075 const TH = 80; // threshold jarak piksel untuk menutup 14076 if (Math.abs(dy) > TH) { 14077 // Animasi keluar arah swipe 14078 animateClose(dy); 14079 } else { 14080 // Snap back 14081 resetRootDrag(true); 14082 } 14083 } else if (state.swipeMode === 'h') { 14084 if (Math.abs(dx) > 50) { dx < 0 ? next() : prev(); } 14085 } 14086 } 14087 14088 state.isDragging = false; 14089 state.swipeMode = 'none'; 14090 state.totalDx = 0; 14091 state.totalDy = 0; 14092 }); 14093 } 14094 14095 function setImage(idx){ 14096 if (!state.items.length) return; 14097 state.idx = (idx + state.items.length) % state.items.length; 14098 const it = state.items[state.idx]; 14099 imgEl.src = it.src; 14100 imgEl.alt = it.alt || ''; 14101 counterEl.textContent = (state.idx + 1) + ' / ' + state.items.length; 14102 resetTransform(); 14103 preloadNeighbors(); 14104 } 14105 14106 function resetTransform(){ 14107 state.scale = 1; 14108 state.tx = 0; state.ty = 0; 14109 applyTransform(); 14110 } 14111 14112 function setTranslate(tx, ty){ 14113 state.tx = tx; state.ty = ty; 14114 applyTransform(); 14115 } 14116 14117 function applyTransform(){ 14118 imgEl.style.transform = `translate3d(${state.tx}px, ${state.ty}px, 0) scale(${state.scale})`; 14119 } 14120 14121 function zoomAt(delta, clientX, clientY, snapTo){ 14122 const old = state.scale; 14123 let next = (typeof snapTo === 'number') ? snapTo : (old + delta); 14124 next = Math.max(state.minScale, Math.min(state.maxScale, next)); 14125 14126 if (next !== old) { 14127 const rect = imgEl.getBoundingClientRect(); 14128 const cx = clientX - rect.left - rect.width/2; 14129 const cy = clientY - rect.top - rect.height/2; 14130 state.tx -= cx * (next/old - 1); 14131 state.ty -= cy * (next/old - 1); 14132 state.scale = next; 14133 applyTransform(); 14134 } 14135 } 14136 14137 function preloadNeighbors(){ 14138 if (state.items.length < 2) return; 14139 const nextIdx = (state.idx + 1) % state.items.length; 14140 const prevIdx = (state.idx - 1 + state.items.length) % state.items.length;
14141 [nextIdx, prevIdx].forEach(i=>{ 14142 const img = new Image(); img.src = state.items[i].src; 14143 }); 14144 } 14145 14146 // ----- Swipe-down visual helpers ----- 14147 function setRootDrag(dy){ 14148 const damped = dy * 0.6; // redam gerakan agar lembut 14149 const abs = Math.abs(dy); 14150 const fade = Math.max(0, Math.min(1, 1 - abs / 300)); // transparansi sesuai jarak 14151 root.style.transform = `translate3d(0, ${damped}px, 0)`; 14152 root.style.opacity = String(0.95 * fade + 0.05); // jangan jadi 0 total sebelum close 14153 } 14154 function resetRootDrag(withAnim){ 14155 if (withAnim) { 14156 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14157 root.style.transform = 'translate3d(0,0,0)'; 14158 root.style.opacity = '1'; 14159 setTimeout(()=>{ root.style.transition = ''; }, 190); 14160 } else { 14161 root.style.transition = ''; 14162 root.style.transform = ''; 14163 root.style.opacity = ''; 14164 } 14165 } 14166 function animateClose(dy){ 14167 root.style.transition = 'transform .18s ease, opacity .18s ease'; 14168 const off = (dy > 0 ? window.innerHeight : -window.innerHeight); 14169 root.style.transform = `translate3d(0, ${off}px, 0)`; 14170 root.style.opacity = '0'; 14171 setTimeout(()=>{ root.style.transition = ''; close(); }, 180); 14172 } 14173 14174 function setSiblingsInert(makeInert){ 14175 // Inert semua saudara root (elemen lain di body) agar fokus tidak lari 14176 const nodes = Array.from(document.body.children).filter(el => el !== root); 14177 nodes.forEach(el=>{ 14178 if (makeInert) { 14179 if (!el.hasAttribute('inert')) el.setAttribute('inert',''); 14180 } else { 14181 if (el.hasAttribute('inert')) el.removeAttribute('inert'); 14182 } 14183 }); 14184 } 14185 14186 function open(idx){ 14187 if (!state.items.length) buildItems(); 14188 if (!state.items.length) return; 14189 ensureUI(); 14190 14191 // Reset efek drag visual 14192 resetRootDrag(false); 14193 14194 document.body.style.overflow = 'hidden'; 14195 root.classList.add('show'); 14196 root.removeAttribute('aria-hidden'); // hindari aria-hidden saat fokus child 14197 setSiblingsInert(true); 14198 14199 state.open = true; 14200 setImage(typeof idx === 'number' ? idx : 0); 14201 14202 // Fokuskan dialog agar keyboard pasti tertangkap 14203 try { root.focus(); } catch(_){} 14204 } 14205 14206 function close(){ 14207 state.open = false; 14208 root.classList.remove('show'); 14209 root.setAttribute('aria-hidden','true'); 14210 setSiblingsInert(false); 14211 document.body.style.overflow = ''; 14212 14213 // Bersihkan transform/opacity 14214 resetRootDrag(false); 14215 } 14216 14217 function next(){ setImage(state.idx + 1); } 14218 function prev(){ setImage(state.idx - 1); } 14219 14220 function findIndexByUrl(url){ 14221 const target = fileName(absUrl(url)); 14222 const idx = state.items.findIndex(it => fileName(it.src) === target); 14223 return idx >= 0 ? idx : 0; 14224 } 14225 14226 function initPdLightbox(){ 14227 buildItems(); 14228 ensureUI(); 14229 14230 // Klik gambar utama â buka LB di index yang sama 14231 const mainImg = document.getElementById('pd-main-img'); 14232 if (mainImg) { 14233 mainImg.style.cursor = 'zoom-in'; 14234 mainImg.addEventListener('click', ()=>{ 14235 open(findIndexByUrl(mainImg.src)); 14236 }); 14237 } 14238 14239 // Klik gambar slider mobile â buka LB di gambar yang diketuk 14240 const slider = document.getElementById('pd-mobile-slider'); 14241 if (slider) { 14242 slider.addEventListener('click', (e)=>{ 14243 const im = e.target && e.target.tagName === 'IMG' ? e.target : e.target.closest('img'); 14244 if (!im) return; 14245 open(findIndexByUrl(im.src)); 14246 }); 14247 } 14248 14249 // (Opsional) double-click pada thumbnail untuk buka langsung 14250 const thumbs = document.querySelectorAll('.pd-thumb-btn'); 14251 thumbs.forEach((btn)=>{ 14252 let last = 0; 14253 btn.addEventListener('click', ()=>{ 14254 const now = Date.now(); 14255 if (now - last < 300) { 14256 const src = btn.getAttribute('data-full') || ''; 14257 open(findIndexByUrl(absUrl(src))); 14258 last = 0; 14259 } else { 14260 last = now; 14261 } 14262 }); 14263 }); 14264 14265 // Ekspos API 14266 window.PDLightbox = { open, close, next, prev, findIndexByUrl }; 14267 } 14268 14269 if (document.readyState === 'loading') { 14270 document.addEventListener('DOMContentLoaded', initPdLightbox); 14271 } else { 14272 initPdLightbox(); 14273 } 14274})(); 14275</script>
vendor: 1 bytes, line 14275
14275
14276<script data-inlined="cart.js"> 14277(function(){ 14278 function $(s,ctx=document){ return ctx.querySelector(s); } 14279 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 14280 14281 // CSRF token helper 14282 function getCsrfToken() { 14283 const metaTag = document.querySelector('meta[name="csrf-token"]'); 14284 return metaTag ? metaTag.getAttribute('content') : ''; 14285 } 14286 14287 function guessPdMeta(){ 14288 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 14289 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 14290 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 14291 const imgEl = $('#pd-main-img'); 14292 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 14293 return { name, sku, image_url }; // price removed for security 14294 } 14295 14296 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 14297 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 14298 const payload = { 14299 product_id: parseInt(productId||0,10) || 0, 14300 qty: parseInt(qty||1,10) || 1, 14301 variant_id: parseInt(variantId||0,10) || 0, 14302 meta 14303 }; 14304 14305 // Check if user is logged in 14306 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 14307 document.body.classList.contains('logged-in') || 14308 window.userLoggedIn === true || 14309 document.body.dataset.userLoggedIn === 'true'; 14310 14311 // Skip localStorage untuk logged user - mereka pakai database 14312 if (!isLoggedIn && window.guestCart) { 14313 try { 14314 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 14315 } catch (e) { 14316 console.warn('Failed to add to localStorage cart:', e); 14317 } 14318 } 14319 14320 const res = await fetch('/api/cart/add.php', { 14321 method: 'POST', 14322 headers: { 14323 'Content-Type':'application/json', 14324 'X-CSRF-Token': getCsrfToken() 14325 }, 14326 credentials: 'same-origin', 14327 body: JSON.stringify({ 14328 ...payload, 14329 csrf_token: getCsrfToken() 14330 }), 14331 }); 14332 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 14333 if (!res.ok || !data.ok) { 14334 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 14335 throw new Error(msg); 14336 } 14337 14338 // Update badge di navbar dengan data dari response 14339 try { 14340 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14341 if (el && data.cart && typeof data.cart.items_count === 'number') { 14342 el.textContent = String(data.cart.items_count); 14343 el.classList.toggle('is-empty', data.cart.items_count <= 0); 14344 } 14345 } catch(_){} 14346 14347 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 14348 try { 14349 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 14350 } catch(_){} 14351 14352 // Update CartBadge dengan data dari response (lebih reliable) 14353 try { 14354 if (window.CartBadge && window.CartBadge.set) { 14355 window.CartBadge.set(data.cart.items_count||0); 14356 } 14357 } catch(_){} 14358 14359 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 14360 setTimeout(() => { 14361 if (window.updateCartCounter) { 14362 window.updateCartCounter(); 14363 } 14364 }, 100); 14365 14366 return data; 14367 } 14368 14369 // Function to update cart counter from server 14370 async function updateCartCounter(){ 14371 try { 14372 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 14373 const data = await res.json().catch(()=>({ok:false})); 14374 if (data.ok && typeof data.items_count === 'number') { 14375 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 14376 if (el) { 14377 el.textContent = String(data.items_count); 14378 el.classList.toggle('is-empty', data.items_count <= 0); 14379 } 14380 // Dispatch event 14381 try { 14382 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 14383 } catch(_){} 14384 } 14385 } catch (err) { 14386 console.log('Could not update cart counter:', err); 14387 } 14388 } 14389 14390 // Function to manually merge guest cart (if needed) 14391 async function mergeGuestCart(){ 14392 try { 14393 const res = await fetch('/api/cart/merge-guest.php', { 14394 method: 'POST', 14395 headers: { 14396 'Content-Type': 'application/json', 14397 'X-CSRF-Token': getCsrfToken() 14398 }, 14399 credentials: 'same-origin', 14400 body: JSON.stringify({ csrf_token: getCsrfToken() }) 14401 }); 14402 const data = await res.json().catch(()=>({ok:false})); 14403 14404 if (data.ok && data.merged_items > 0) { 14405 console.log(`Guest cart merged: ${data.merged_items} items`); 14406 // Update cart counter after merge 14407 await updateCartCounter(); 14408 return data; 14409 } 14410 return data; 14411 } catch (err) { 14412 console.log('Could not merge guest cart:', err); 14413 return { ok: false, error: err.message }; 14414 } 14415 } 14416 14417 window.addToCart = addToCart; 14418 window.updateCartCounter = updateCartCounter; 14419 window.mergeGuestCart = mergeGuestCart; 14420 14421 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 14422 document.addEventListener('click', async (e)=>{ 14423 const btn = e.target.closest('[data-addtocart]'); 14424 if (!btn) return; 14425 e.preventDefault(); 14426 if (btn.disabled) return; 14427 14428 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 14429 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
14430 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 14431 14432 try { 14433 const data = await addToCart(pid, qty, vid); 14434 if (window.showToast) { 14435 const meta = guessPdMeta(); 14436 window.showToast({ 14437 type:'success', 14438 title:'Berhasil ditambahkan', 14439 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 14440 duration: 3800, 14441 actions: [ 14442 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 14443 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 14444 ] 14445 }); 14446 } 14447 } catch (err) { 14448 if (window.showToast) { 14449 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 14450 } else { 14451 console.error(err); 14452 } 14453 } 14454 }); 14455})(); 14456</script>
vendor: 1 bytes, line 14456
14456
14457<script data-inlined="stock-validator-vanilla.js"> 14458/** 14459 * Stock Validation - Vanilla JS Version 14460 * 14461 * This script provides client-side stock validation without jQuery dependency 14462 * - Works with native JavaScript and fetch API 14463 * - Graceful fallback for older browsers 14464 * - Same functionality as jQuery version 14465 */ 14466 14467window.StockValidator = (function() { 14468 'use strict'; 14469 14470 const config = { 14471 apiEndpoint: '/api/stock/check.php', 14472 modalSelector: '#stock-modal-overlay', // We create our own modal 14473 loadingClass: 'loading-stock', 14474 disabledClass: 'stock-disabled', 14475 debug: true // Enable debug logging 14476 }; 14477 14478 /** 14479 * Cross-browser event listener helper 14480 */ 14481 function addEventListener(element, event, handler) { 14482 if (element.addEventListener) { 14483 element.addEventListener(event, handler); 14484 } else if (element.attachEvent) { 14485 element.attachEvent('on' + event, handler); 14486 } 14487 } 14488 14489 /** 14490 * Get stock information from embedded variant data (instant, no AJAX needed) 14491 */ 14492 function getStockFromEmbeddedData(variantId) { 14493 // Check if embedded variant data exists 14494 if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) { 14495 return null; 14496 } 14497 14498 const variants = window.__PD_VARIANTS_WITH_BRANCH__; 14499 14500 // SPECIAL CASE: Simple product (no variants) - empty array means simple product 14501 if (variants.length === 0) { 14502 // For simple products, we cannot determine stock from embedded data 14503 // Return null to force API validation 14504 return null; 14505 } 14506 14507 // If no variant specified, check base product stock (all variants) 14508 if (!variantId || variantId <= 0) { 14509 // For base product, check if ANY variant has stock 14510 const isPreorder = window.isPreorderProduct || false; 14511 let hasAnyStock = false; 14512 let totalStock = 0; 14513 14514 if (isPreorder) { 14515 // For pre-order, check preorder_remaining 14516 hasAnyStock = variants.some(v => { 14517 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14518 return v.preorder_remaining > 0; 14519 } 14520 return v.stock > 0; 14521 }); 14522 totalStock = variants.reduce((sum, v) => { 14523 if ('preorder_remaining' in v && v.preorder_remaining !== null) { 14524 return sum + (v.preorder_remaining || 0); 14525 } 14526 return sum + (v.stock || 0); 14527 }, 0); 14528 } else { 14529 hasAnyStock = variants.some(v => v.stock > 0); 14530 totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0); 14531 } 14532 14533 return { 14534 hasStock: hasAnyStock, 14535 stock: totalStock, 14536 sku: 'Base Product', 14537 variant: null 14538 }; 14539 } 14540 14541 // Find specific variant 14542 const variant = variants.find(v => v.id === variantId); 14543 14544 if (!variant) { 14545 return { 14546 hasStock: false, 14547 stock: 0, 14548 sku: 'Unknown Variant', 14549 variant: null 14550 }; 14551 } 14552 14553 // For pre-order products, check preorder_remaining instead of stock 14554 const isPreorder = window.isPreorderProduct || false; 14555 let hasStock = false; 14556 let availableStock = 0; 14557 14558 if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) { 14559 availableStock = parseInt(variant.preorder_remaining, 10); 14560 if (isNaN(availableStock)) availableStock = 0; 14561 hasStock = availableStock > 0; 14562 } else { 14563 availableStock = variant.stock || 0; 14564 hasStock = availableStock > 0; 14565 } 14566 14567 return { 14568 hasStock: hasStock, 14569 stock: availableStock, 14570 sku: variant.sku, 14571 variant: variant 14572 }; 14573 } 14574 14575 /** 14576 * Cross-browser AJAX helper 14577 */
14578 function makeRequest(url, options) { 14579 return new Promise((resolve, reject) => { 14580 // Try fetch first (modern browsers) 14581 if (typeof fetch !== 'undefined') { 14582 const fetchOptions = { 14583 method: options.method || 'POST', 14584 headers: { 14585 'Content-Type': 'application/x-www-form-urlencoded', 14586 }, 14587 body: options.data ? new URLSearchParams(options.data).toString() : null 14588 }; 14589 14590 fetch(url, fetchOptions) 14591 .then(response => { 14592 if (!response.ok) { 14593 throw new Error(`HTTP ${response.status}`); 14594 } 14595 return response.json(); 14596 }) 14597 .then(resolve) 14598 .catch(reject); 14599 } else { 14600 // Fallback to XMLHttpRequest 14601 const xhr = new XMLHttpRequest(); 14602 xhr.open(options.method || 'POST', url); 14603 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 14604 14605 xhr.onload = function() { 14606 if (xhr.status >= 200 && xhr.status < 300) { 14607 try { 14608 resolve(JSON.parse(xhr.responseText)); 14609 } catch (e) { 14610 reject(new Error('Invalid JSON response')); 14611 } 14612 } else { 14613 reject(new Error(`HTTP ${xhr.status}`)); 14614 } 14615 }; 14616 14617 xhr.onerror = function() { 14618 reject(new Error('Network error')); 14619 }; 14620 14621 const data = options.data ? new URLSearchParams(options.data).toString() : null; 14622 xhr.send(data); 14623 } 14624 }); 14625 } 14626 14627 /** 14628 * Check stock via AJAX 14629 */ 14630 function checkStock(productId, variantId, qty) { 14631 return makeRequest(config.apiEndpoint, { 14632 method: 'POST', 14633 data: { 14634 product_id: productId, 14635 variant_id: variantId || 0, 14636 qty: qty || 1 14637 } 14638 }); 14639 } 14640 14641 /** 14642 * Create and show stock alert modal using modal.css structure 14643 */ 14644 function showStockModal(stockInfo, action = 'add to cart') { 14645 let title, message, type, iconClass; 14646 14647 // Handle both string and object formats 14648 if (typeof stockInfo === 'string') { 14649 title = 'Stok Tidak Tersedia'; 14650 message = stockInfo; 14651 type = 'warning'; 14652 iconClass = 'warning'; 14653 } else if (stockInfo && typeof stockInfo === 'object') { 14654 if (!stockInfo.is_active) { 14655 title = 'Produk Tidak Tersedia'; 14656 message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.'; 14657 type = 'danger'; 14658 iconClass = 'danger'; 14659 } else if (!stockInfo.is_in_stock) { 14660 title = 'Stok Habis'; 14661 message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`; 14662 if (stockInfo.sku) { 14663 message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`; 14664 } 14665 type = 'warning'; 14666 iconClass = 'warning'; 14667 } else if (!stockInfo.can_add_to_cart) { 14668 title = 'Stok Tidak Mencukupi'; 14669 message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`; 14670 message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`; 14671 message += `Silakan kurangi jumlah atau pilih varian lain.`; 14672 type = 'warning'; 14673 iconClass = 'warning'; 14674 } else { 14675 title = 'Stok Tidak Tersedia'; 14676 message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14677 type = 'warning'; 14678 iconClass = 'warning'; 14679 } 14680 } else { 14681 title = 'Stok Tidak Tersedia'; 14682 message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.'; 14683 type = 'warning'; 14684 iconClass = 'warning'; 14685 } 14686 14687 // Create modal HTML using modal.css structure 14688 const modalHTML = ` 14689 <div class="modal-overlay" id="stock-modal-overlay"> 14690 <div class="modal-dialog"> 14691 <div class="modal-header"> 14692 <div class="modal-icon ${iconClass}"> 14693 ${getIconSVG(iconClass)} 14694 </div> 14695 <h3 class="modal-title">${title}</h3> 14696 </div> 14697 <div class="modal-body"> 14698 <p class="modal-message">${message}</p> 14699 <div class="modal-actions"> 14700 <button type="button" class="modal-btn modal-btn-secondary" data-action="close"> 14701 Tutup 14702 </button> 14703 <button type="button" class="modal-btn modal-btn-primary" data-action="reload"> 14704 Muat Ulang 14705 </button> 14706 </div> 14707 </div> 14708 </div> 14709 </div> 14710 `; 14711 14712 // Remove existing modal if any 14713 const existingModal = document.getElementById('stock-modal-overlay'); 14714 if (existingModal) { 14715 existingModal.remove(); 14716 } 14717 14718 // Add modal to body 14719 document.body.insertAdjacentHTML('beforeend', modalHTML); 14720 14721 // Show modal with animation 14722 const modal = document.getElementById('stock-modal-overlay'); 14723 setTimeout(() => { 14724 modal.classList.add('show'); 14725 }, 10); 14726 14727 // Add event listeners to modal buttons 14728 const closeBtn = modal.querySelector('[data-action="close"]'); 14729 const reloadBtn = modal.querySelector('[data-action="reload"]'); 14730 14731 if (closeBtn) { 14732 addEventListener(closeBtn, 'click', closeStockModal); 14733 } 14734 14735 if (reloadBtn) { 14736 addEventListener(reloadBtn, 'click', function() { 14737 closeStockModal(); 14738 window.location.reload(); 14739 }); 14740 } 14741 14742 // Close modal when clicking overlay 14743 addEventListener(modal, 'click', function(event) { 14744 if (event.target === modal) { 14745 closeStockModal(); 14746 } 14747 }); 14748 14749 // Auto-close after 8 seconds 14750 setTimeout(() => { 14751 closeStockModal(); 14752 }, 8000); 14753 } 14754 14755 /** 14756 * Get icon SVG based on type 14757 */ 14758 function getIconSVG(type) { 14759 const icons = { 14760 danger: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 14761 warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
14761<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>', 14762 success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>', 14763 info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>' 14764 }; 14765 return icons[type] || icons.info; 14766 } 14767 14768 /** 14769 * Close stock modal 14770 */ 14771 function closeStockModal() { 14772 const modal = document.getElementById('stock-modal-overlay'); 14773 if (modal) { 14774 modal.classList.remove('show'); 14775 setTimeout(() => { 14776 modal.remove(); 14777 }, 200); 14778 } 14779 } 14780 14781 // Make closeStockModal globally available 14782 if (typeof window !== 'undefined') { 14783 window.closeStockModal = closeStockModal; 14784 } 14785 14786 /** 14787 * Get element data attribute with fallback 14788 */ 14789 function getDataAttribute(element, attr) { 14790 if (element.dataset) { 14791 return element.dataset[attr]; 14792 } else { 14793 return element.getAttribute('data-' + attr); 14794 } 14795 } 14796 14797 /** 14798 * Validate stock before cart action 14799 */ 14800 async function validateBeforeCart(productId, variantId, qty, action = 'add') { 14801 try { 14802 // SMART VALIDATION: For simple products, check if we already have cached stock info 14803 // If buttons are enabled and cached info shows stock available, skip API validation 14804 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) { 14805 14806 return true; 14807 } 14808 14809 // For variant products or when cached info is not available, do API validation 14810 const stockInfo = await checkStock(productId, variantId, qty); 14811 14812 if (!stockInfo.ok) { 14813 showStockModal({ 14814 message: stockInfo.error || 'Gagal mengecek stok', 14815 is_active: false 14816 }, action); 14817 return false; 14818 } 14819 14820 if (!stockInfo.can_add_to_cart) { 14821 showStockModal(stockInfo, action); 14822 return false; 14823 } 14824 14825 return true; // Stock available, proceed 14826 14827 } catch (error) { 14828 console.error('Stock validation error:', error); 14829 showStockModal({ 14830 message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.', 14831 is_active: false 14832 }, action); 14833 return false; 14834 } 14835 } 14836 14837 /** 14838 * Handle button click with stock validation 14839 */ 14840 function handleButtonClick(event, action) { 14841 const button = event.target; 14842 14843 14844 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14845 if (button.hasAttribute('data-stock-blocked')) { 14846 event.preventDefault(); 14847 event.stopImmediatePropagation(); 14848 14849 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14850 showStockModal(reason); 14851 14852 // Show security warning 14853 if (window.showToast) { 14854 window.showToast({ 14855 type: 'warning', 14856 title: 'Aksi Tidak Diizinkan', 14857 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14858 duration: 3000 14859 }); 14860 } 14861 return false; 14862 } 14863 14864 // SECOND CHECK: Real-time stock validation against cached stock info 14865 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14866 event.preventDefault(); 14867 event.stopImmediatePropagation(); 14868 14869 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14870 14871 // Re-enforce button disabling 14872 button.disabled = true; 14873 button.style.opacity = '0.5'; 14874 button.style.cursor = 'not-allowed'; 14875 button.setAttribute('data-stock-blocked', 'true'); 14876 14877 return false;
14878 } 14879 14880 // Skip if already processing 14881 if (button.classList.contains(config.loadingClass)) { 14882 event.preventDefault(); 14883 return false; 14884 } 14885 14886 // Get product data from button attributes or form 14887 let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0); 14888 let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0); 14889 let qty = parseInt(getDataAttribute(button, 'qty') || 1); 14890 14891 // Try to get from global variables (set by product detail page) 14892 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 14893 productId = parseInt(window.PD_ID || 0); 14894 } 14895 14896 // Try to get from parent form if not in button 14897 if (productId <= 0) { 14898 const form = button.closest ? button.closest('form') : null; 14899 if (form) { 14900 const pidAttr = form.getAttribute('data-pid'); 14901 const pidInput = form.querySelector('[name="product_id"]'); 14902 const vidInput = form.querySelector('[name="variant_id"]'); 14903 const qtyInput = form.querySelector('[name="qty"]'); 14904 14905 if (pidAttr) productId = parseInt(pidAttr); 14906 if (pidInput) productId = parseInt(pidInput.value || 0); 14907 if (vidInput) variantId = parseInt(vidInput.value || 0); 14908 if (qtyInput) qty = parseInt(qtyInput.value || 1); 14909 } 14910 } 14911 14912 // CRITICAL: Always get current variant from selector (overrides button attributes) 14913 // This ensures we validate against the currently selected variant, not cached button data 14914 const variantSelector = document.getElementById('pd-variant-id'); 14915 if (variantSelector) { 14916 const currentVariantId = parseInt(variantSelector.value || 0); 14917 if (currentVariantId > 0) { 14918 variantId = currentVariantId; 14919 } 14920 } 14921 14922 14923 if (productId <= 0) { 14924 return true; // Let default handler deal with it 14925 } 14926 14927 // Prevent default and validate stock 14928 event.preventDefault(); 14929 event.stopPropagation(); 14930 14931 // Show loading state 14932 button.classList.add(config.loadingClass); 14933 const originalDisabled = button.disabled; 14934 button.disabled = true; 14935 14936 14937 validateBeforeCart(productId, variantId, qty, action) 14938 .then(isValid => { 14939 if (isValid) { 14940 // Stock OK, proceed with original action 14941 if (action === 'cart' && typeof window.addToCart === 'function') { 14942 window.addToCart(productId, qty, variantId); 14943 } else if (action === 'buy' && typeof window.buyNow === 'function') { 14944 // Call buyNow from pd-buynow.js without parameters (it gets data internally) 14945 window.buyNow(); 14946 } else { 14947 // Add fallback cart/buy logic here if needed 14948 } 14949 } 14950 }) 14951 .catch(error => { 14952 }) 14953 .finally(() => { 14954 // Remove loading state 14955 button.classList.remove(config.loadingClass); 14956 button.disabled = originalDisabled; 14957 }); 14958 14959 return false; 14960 } 14961 14962 /** 14963 * Handle mobile button click with stock validation 14964 */ 14965 function handleMobileButtonClick(event, action) { 14966 const button = event.target; 14967 14968 14969 // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering) 14970 if (button.hasAttribute('data-stock-blocked')) { 14971 event.preventDefault(); 14972 event.stopImmediatePropagation(); 14973 14974 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 14975 showStockModal(reason); 14976 14977 // Show security warning 14978 if (window.showToast) { 14979 window.showToast({ 14980 type: 'warning', 14981 title: 'Aksi Tidak Diizinkan', 14982 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 14983 duration: 3000 14984 }); 14985 } 14986 return false;
14987 } 14988 14989 // SECOND CHECK: Real-time stock validation against cached stock info 14990 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 14991 event.preventDefault(); 14992 event.stopImmediatePropagation(); 14993 14994 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 14995 14996 // Re-enforce button disabling 14997 button.disabled = true; 14998 button.style.opacity = '0.5'; 14999 button.style.cursor = 'not-allowed'; 15000 button.setAttribute('data-stock-blocked', 'true'); 15001 15002 return false; 15003 } 15004 15005 // Skip if already processing 15006 if (button.classList.contains(config.loadingClass)) { 15007 event.preventDefault(); 15008 return false; 15009 } 15010 15011 // Get product data from mobile FAB container 15012 let productId = 0; 15013 let variantId = 0; 15014 let qty = 1; 15015 15016 // Try to get from mobile FAB data-pid 15017 const mobileFab = document.getElementById('pd-mobile-fab'); 15018 if (mobileFab) { 15019 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15020 if (pidAttr) productId = parseInt(pidAttr); 15021 } 15022 15023 // Try to get from global variables (set by product detail page) 15024 if (productId <= 0 && typeof window.PD_ID !== 'undefined') { 15025 productId = parseInt(window.PD_ID || 0); 15026 } 15027 15028 // CRITICAL: Always get current variant from selector (real-time) 15029 // This ensures we validate against the currently selected variant 15030 const variantSelector = document.getElementById('pd-variant-id'); 15031 if (variantSelector) { 15032 variantId = parseInt(variantSelector.value || 0); 15033 } 15034 15035 // Try to get quantity from quantity input 15036 const qtySelector = document.querySelector('[name="qty"]'); 15037 if (qtySelector) { 15038 qty = parseInt(qtySelector.value || 1); 15039 } 15040 15041 // Ensure minimum quantity is 1 15042 qty = Math.max(1, qty); 15043 15044 15045 if (productId <= 0) { 15046 return true; // Let default handler deal with it 15047 } 15048 15049 // Event already prevented at interceptor level, just proceed with validation 15050 15051 // Show loading state 15052 button.classList.add(config.loadingClass); 15053 const originalDisabled = button.disabled; 15054 const originalText = button.textContent; 15055 button.disabled = true; 15056 button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...'; 15057 15058 15059 // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions 15060 if (window.addToCart && typeof window.addToCart === 'function') { 15061 window.addToCart._stockValidatorBlocked = true; 15062 } 15063 15064 // Also preemptively block other cart functions 15065 const originalAddToCartSubmit = window.addToCartSubmit; 15066 const originalPdAddToCartSubmit = window._pdAddToCartSubmit; 15067 15068 // Temporarily block ALL cart functions during validation 15069 window.addToCartSubmit = (...args) => { 15070 return Promise.resolve(false); 15071 }; 15072 15073 if (originalPdAddToCartSubmit) { 15074 window._pdAddToCartSubmit = (...args) => { 15075 return Promise.resolve(false); 15076 }; 15077 } 15078 15079 validateBeforeCart(productId, variantId, qty, action) 15080 .then(isValid => { 15081 if (isValid) { 15082 // Stock OK, proceed with original action 15083 15084 if (action === 'cart') { 15085 // For mobile cart, we need to recreate the original behavior 15086 // Strategy: Try multiple methods to ensure cart add + toast success 15087 15088 15089 let cartAddSuccess = false; 15090 let errorMessage = ''; 15091 15092 // Method 1: Try original addToCart function 15093 const originalAddToCart = window.addToCart && window.addToCart._original; 15094 if (originalAddToCart && !cartAddSuccess) { 15095 try { 15096 const result = originalAddToCart(productId, qty, variantId); 15097 15098 // Handle both sync and async returns 15099 if (result && typeof result.then === 'function') { 15100 result.then(() => { 15101 cartAddSuccess = true; 15102 }).catch(err => { 15103 errorMessage = err.message || 'addToCart failed'; 15104 }); 15105 } else { 15106 cartAddSuccess = true; 15107 } 15108 } catch (error) { 15109 errorMessage = error.message || 'addToCart exception'; 15110 } 15111 } 15112 15113 // Method 2: Try pd-addtocart.js function (backup method) 15114 if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) { 15115 try { 15116 15117 // Create fake form for pd-addtocart.js 15118 const fakeForm = document.createElement('form');
15119 const qtyInput = document.createElement('input'); 15120 qtyInput.name = 'qty'; 15121 qtyInput.value = qty; 15122 fakeForm.appendChild(qtyInput); 15123 15124 // Set variant selector value for pd-addtocart to read 15125 const variantSelector = document.getElementById('pd-variant-id'); 15126 if (variantSelector) { 15127 variantSelector.value = variantId; 15128 } 15129 15130 const fakeEvent = { 15131 preventDefault: () => {}, 15132 target: fakeForm 15133 }; 15134 15135 // Call pd-addtocart.js function (this should handle toast internally) 15136 const result = window._pdAddToCartSubmit(fakeEvent, productId); 15137 if (result && typeof result.then === 'function') { 15138 result.then(() => { 15139 cartAddSuccess = true; 15140 }); 15141 } else { 15142 cartAddSuccess = true; 15143 } 15144 } catch (error) { 15145 } 15146 } 15147 15148 // Method 3: Direct addToCart call (fallback) 15149 if (typeof window.addToCart === 'function' && !cartAddSuccess) { 15150 try { 15151 const result = window.addToCart(productId, qty, variantId); 15152 if (result && typeof result.then === 'function') { 15153 result.then(() => { 15154 cartAddSuccess = true; 15155 }); 15156 } else { 15157 cartAddSuccess = true; 15158 } 15159 } catch (error) { 15160 } 15161 } 15162 15163 // Always show success toast for mobile (regardless of which method worked) 15164 // This ensures user gets feedback for successful cart addition 15165 setTimeout(() => { 15166 if (window.showToast) { 15167 15168 // Get product details for toast 15169 const getVariantSummaryText = () => { 15170 const vs = document.getElementById('pd-variant-summary'); 15171 if (!vs) return ''; 15172 const items = Array.from(vs.querySelectorAll('.vs-item strong')) 15173 .map(el => el.textContent.trim()) 15174 .filter(Boolean); 15175 return items.length ? items.join(' / ') : ''; 15176 }; 15177 15178 const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({ 15179 '&':'&','<':'<','>':'>','"':'"',"'":''' 15180 }[m])); 15181 15182 const variantText = getVariantSummaryText(); 15183 const skuText = (function(){ 15184 const el = document.getElementById('pd-sku'); 15185 return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 15186 })(); 15187 const productName = window.PD_NAME || 'Product'; 15188 15189 window.showToast({ 15190 type: 'success', 15191 title: 'Berhasil ditambahkan ke Keranjang', 15192 message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` ÷ <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
15192 ÷ Qty: ${qty}`, 15193 duration: 5000, 15194 actions: [ 15195 { 15196 label: 'Lihat Keranjang', 15197 onClick: () => { window.location.href = '/keranjang'; } 15198 }, 15199 { 15200 label: 'Lanjut Belanja', 15201 variant: 'alt', 15202 onClick: () => {} 15203 } 15204 ] 15205 }); 15206 } else { 15207 } 15208 }, 100); // Small delay to ensure cart operation completes first 15209 15210 } else if (action === 'buy') { 15211 // For mobile buy, call buyNow 15212 if (typeof window.buyNow === 'function') { 15213 window.buyNow(); 15214 } 15215 } 15216 } else { 15217 // Stock validation FAILED - absolutely NO cart function calls allowed 15218 15219 // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart) 15220 } 15221 15222 // Always restore functions after validation (success or failure) 15223 setTimeout(() => { 15224 // Remove blocking flag 15225 if (window.addToCart && typeof window.addToCart === 'function') { 15226 window.addToCart._stockValidatorBlocked = false; 15227 } 15228 15229 // Restore other functions 15230 if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') { 15231 window.addToCartSubmit = originalAddToCartSubmit; 15232 } 15233 if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') { 15234 window._pdAddToCartSubmit = originalPdAddToCartSubmit; 15235 } 15236 15237 }, 1000); // Shorter timeout since we want to restore quickly after success 15238 }) 15239 .catch(error => { 15240 // On error, also block the action 15241 }) 15242 .finally(() => { 15243 // Remove loading state 15244 button.classList.remove(config.loadingClass); 15245 button.disabled = originalDisabled; 15246 button.textContent = originalText; 15247 }); 15248 15249 return false; 15250 } 15251 15252 /** 15253 * Intercept cart buttons 15254 */ 15255 function interceptCartButtons() { 15256 15257 15258 // CRITICAL: Remove existing mobile cart handlers from product-detail.php 15259 setTimeout(() => { 15260 const mobileCartBtn = document.getElementById('mf-cart'); 15261 if (mobileCartBtn) { 15262 // Clone the button to remove ALL existing event listeners 15263 const newBtn = mobileCartBtn.cloneNode(true); 15264 mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn); 15265 15266 15267 // Add our own handler to the new button 15268 newBtn.addEventListener('click', function(event) { 15269 event.preventDefault(); 15270 event.stopPropagation(); 15271 event.stopImmediatePropagation(); 15272 handleMobileButtonClick(event, 'cart'); 15273 return false; 15274 }, true); 15275 } 15276 }, 50); 15277 15278 // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit) 15279 // This provides better integration with existing pd-addtocart.js toast system 15280 15281 // Buy now buttons - updated selectors (desktop + mobile) 15282 addEventListener(document, 'click', function(event) { 15283 const target = event.target; 15284 15285 // Desktop buy buttons 15286 if (target.classList.contains('btn-buy') || // Product detail buy button 15287 target.classList.contains('btn-buy-now') || 15288 target.classList.contains('buy-now-btn') || 15289 target.id === 'btn-buy' || // Specific ID match 15290 getDataAttribute(target, 'action') === 'buy-now' || 15291 getDataAttribute(target, 'action') === 'buy') { 15292 15293 handleButtonClick(event, 'buy'); 15294 } 15295 15296 // Mobile buy buttons 15297 else if (target.classList.contains('mf-buy') || // Mobile fab buy button 15298 target.id === 'mf-buy') { // Mobile buy button ID 15299 15300 handleMobileButtonClick(event, 'buy'); 15301 } 15302 15303 // Mobile cart buttons - ENHANCED BLOCKING 15304 else if (target.classList.contains('mf-cart') || // Mobile fab cart button 15305 target.id === 'mf-cart') { // Mobile cart button ID 15306 15307 15308 // Immediately prevent all event propagation 15309 event.preventDefault(); 15310 event.stopPropagation(); 15311 event.stopImmediatePropagation(); 15312 15313 handleMobileButtonClick(event, 'cart'); 15314 15315 // Return false to ensure no other handlers run 15316 return false;
15317 } 15318 }, true); // Use capturing phase to ensure we intercept before other handlers 15319 15320 // Additional protection: Override addToCart function to add mobile button state checking 15321 const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null; 15322 if (originalAddToCart) { 15323 window.addToCart = async function(...args) { 15324 // Check if this call is coming from a mobile button that's currently being validated 15325 const mobileCartBtn = document.getElementById('mf-cart'); 15326 if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) { 15327 return Promise.reject(new Error('Mobile validation in progress - cart action blocked')); 15328 } 15329 15330 // Check if functions are temporarily blocked (during stock failure) 15331 if (window.addToCart._stockValidatorBlocked) { 15332 return Promise.reject(new Error('Stock validation failed - cart action blocked')); 15333 } 15334 15335 // Allow normal calls 15336 return originalAddToCart.apply(this, args); 15337 }; 15338 15339 // Store reference to original for restoration 15340 window.addToCart._original = originalAddToCart; 15341 } 15342 } 15343 15344 /** 15345 * Setup global click interceptor for security 15346 * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler 15347 */ 15348 function setupGlobalClickInterceptor() { 15349 // Add capture-phase listener to catch clicks before other handlers 15350 document.addEventListener('click', function(event) { 15351 const target = event.target; 15352 15353 // Check if this is a cart/buy button 15354 const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') || 15355 target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart'); 15356 const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') || 15357 target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'); 15358 15359 if (!isCartButton && !isBuyButton) { 15360 return; // Not our target, allow normal processing 15361 } 15362 15363 const button = isCartButton ? 15364 (target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') ? target : target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart')) : 15365 (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy')); 15366 15367 // IMMEDIATE BLOCK: Prevent any processing first 15368 event.preventDefault(); 15369 event.stopImmediatePropagation(); 15370 15371 // SECURITY CHECK 1: Is button marked as stock-blocked? 15372 if (button && button.hasAttribute('data-stock-blocked')) { 15373 15374 const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 15375 showStockModal(reason); 15376 15377 // Show security alert 15378 if (window.showToast) { 15379 window.showToast({ 15380 type: 'error', 15381 title: 'Aksi Diblokir', 15382 message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.', 15383 duration: 4000 15384 }); 15385 } 15386 15387 return false; 15388 } 15389 15390 // SECURITY CHECK 2: Real-time validation against cached stock info 15391 if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) { 15392 15393 showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia'); 15394 15395 // Re-enforce button disabling 15396 if (button) { 15397 button.disabled = true; 15398 button.style.opacity = '0.5'; 15399 button.style.cursor = 'not-allowed'; 15400 button.setAttribute('data-stock-blocked', 'true'); 15401 button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia'); 15402 } 15403 15404 return false;
15405 } 15406 15407 // SECURITY CHECK 3: Smart client-side validation 15408 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15409 const variantSelector = document.getElementById('pd-variant-id'); 15410 const variantId = variantSelector ? parseInt(variantSelector.value || 0) : 0; 15411 15412 if (productId > 0) { 15413 // SMART VALIDATION: If we already have cached stock info showing stock is available, 15414 // and buttons are enabled, proceed immediately without re-validation 15415 if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 15416 button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15417 15418 // Allow the action to proceed immediately with toast 15419 if (isCartButton) { 15420 // Trigger cart action with proper toast handling 15421 if (typeof window._pdAddToCartSubmit === 'function') { 15422 // Use pd-addtocart.js function for proper toast handling 15423 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15424 if (form) { 15425 const fakeEvent = { 15426 target: form, 15427 preventDefault: () => {} 15428 }; 15429 window._pdAddToCartSubmit(fakeEvent, productId); 15430 } 15431 } else if (typeof window.addToCartSubmit === 'function') { 15432 // Fallback to addToCartSubmit 15433 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15434 if (form) { 15435 const fakeEvent = { 15436 target: form, 15437 preventDefault: () => {} 15438 }; 15439 window.addToCartSubmit(fakeEvent, productId); 15440 } 15441 } else if (typeof window.addToCart === 'function') { 15442 // Direct addToCart call 15443 window.addToCart(productId, 1, variantId); 15444 } 15445 } else if (isBuyButton) { 15446 // Trigger buy action 15447 if (typeof window.buyNow === 'function') { 15448 window.buyNow(); 15449 } 15450 } 15451 15452 return false; // Action completed 15453 } 15454 15455 // Get stock info from embedded variant data (for variant products) 15456 const stockInfo = getStockFromEmbeddedData(variantId); 15457 15458 if (stockInfo && stockInfo.hasStock) { 15459 15460 // Update global stock cache 15461 window._currentStockInfo = { 15462 can_add_to_cart: true, 15463 is_in_stock: true, 15464 available_stock: stockInfo.stock, 15465 message: 'Stok tersedia' 15466 }; 15467 15468 // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted) 15469 // If data-stock-blocked is set, respect it and don't enable the button 15470 const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true'; 15471 15472 if (!isExplicitlyBlocked) { 15473 // Remove stock blocking if it was set and enable button 15474 button.removeAttribute('data-stock-blocked'); 15475 button.removeAttribute('data-stock-reason'); 15476 button.disabled = false; 15477 button.style.opacity = ''; 15478 button.style.cursor = ''; 15479 button.classList.remove(config.disabledClass); 15480 } else { 15481 // Button is blocked by variant logic, show reason 15482 const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia'; 15483 console.log('Button blocked by variant logic:', reason); 15484 e.preventDefault(); 15485 e.stopPropagation(); 15486 return false;
15487 } 15488 15489 // Allow the action to proceed immediately with toast 15490 if (isCartButton) { 15491 // Trigger cart action with proper toast handling 15492 if (typeof window._pdAddToCartSubmit === 'function') { 15493 // Use pd-addtocart.js function for proper toast handling 15494 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15495 if (form) { 15496 const fakeEvent = { 15497 target: form, 15498 preventDefault: () => {} 15499 }; 15500 window._pdAddToCartSubmit(fakeEvent, productId); 15501 } 15502 } else if (typeof window.addToCartSubmit === 'function') { 15503 // Fallback to addToCartSubmit 15504 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15505 if (form) { 15506 const fakeEvent = { 15507 target: form, 15508 preventDefault: () => {} 15509 }; 15510 window.addToCartSubmit(fakeEvent, productId); 15511 } 15512 } else if (typeof window.addToCart === 'function') { 15513 // Direct addToCart call 15514 window.addToCart(productId, 1, variantId); 15515 } 15516 } else if (isBuyButton) { 15517 // Trigger buy action 15518 if (typeof window.buyNow === 'function') { 15519 window.buyNow(); 15520 } 15521 } 15522 } else if (stockInfo && !stockInfo.hasStock) { 15523 15524 // Update global stock cache 15525 window._currentStockInfo = { 15526 can_add_to_cart: false, 15527 is_in_stock: false, 15528 available_stock: stockInfo.stock, 15529 message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}` 15530 }; 15531 15532 // Force block button 15533 button.disabled = true; 15534 button.style.opacity = '0.5'; 15535 button.style.cursor = 'not-allowed'; 15536 button.setAttribute('data-stock-blocked', 'true'); 15537 button.setAttribute('data-stock-reason', window._currentStockInfo.message); 15538 15539 // Show modal with variant info 15540 const modalMessage = stockInfo ? 15541 `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` : 15542 'Produk ini sedang habis stok. Silakan pilih varian lain.'; 15543 15544 showStockModal(modalMessage); 15545 15546 // Show security warning 15547 if (window.showToast) { 15548 window.showToast({ 15549 type: 'warning', 15550 title: 'Stok Tidak Tersedia', 15551 message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.', 15552 duration: 3000 15553 }); 15554 } 15555 } else { 15556 // stockInfo is null (simple product) - this means embedded data is not available 15557 // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay) 15558 if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) { 15559 15560 // Allow the action to proceed immediately with toast 15561 if (isCartButton) { 15562 // Trigger cart action with proper toast handling 15563 if (typeof window._pdAddToCartSubmit === 'function') { 15564 // Use pd-addtocart.js function for proper toast handling 15565 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15566 if (form) { 15567 const fakeEvent = { 15568 target: form, 15569 preventDefault: () => {} 15570 }; 15571 window._pdAddToCartSubmit(fakeEvent, productId); 15572 } 15573 } else if (typeof window.addToCartSubmit === 'function') { 15574 // Fallback to addToCartSubmit 15575 const form = document.getElementById('pd-buy-form') || button.closest('form'); 15576 if (form) { 15577 const fakeEvent = { 15578 target: form, 15579 preventDefault: () => {} 15580 }; 15581 window.addToCartSubmit(fakeEvent, productId); 15582 } 15583 } else if (typeof window.addToCart === 'function') { 15584 // Direct addToCart call
15585 window.addToCart(productId, 1, variantId); 15586 } 15587 } else if (isBuyButton) { 15588 // Trigger buy action 15589 if (typeof window.buyNow === 'function') { 15590 window.buyNow(); 15591 } 15592 } 15593 } else { 15594 // Button is disabled, don't allow action 15595 showStockModal('Produk ini sedang tidak tersedia.'); 15596 } 15597 } 15598 } else { 15599 // No product ID, block action 15600 showStockModal('Data produk tidak valid.'); 15601 } 15602 15603 return false; // Always block at capture phase 15604 15605 }, true); // Use capture phase to run BEFORE other listeners 15606 15607 15608 } 15609 15610 /** 15611 * Initialize button data attributes from current page 15612 */ 15613 function initializeButtonAttributes() { 15614 // Get product data from page globals or form 15615 let productId = 0, variantId = 0; 15616 15617 // Try to get from global variables (set by PHP) 15618 if (window.PD_ID) productId = parseInt(window.PD_ID); 15619 15620 // Try to get from form 15621 const form = document.getElementById('pd-buy-form'); 15622 if (form) { 15623 const pidAttr = form.getAttribute('data-pid'); 15624 if (pidAttr) productId = parseInt(pidAttr); 15625 15626 const varInput = document.getElementById('pd-variant-id'); 15627 if (varInput) variantId = parseInt(varInput.value || 0); 15628 } 15629 15630 // Try to get from mobile FAB 15631 const mobileFab = document.getElementById('pd-mobile-fab'); 15632 if (mobileFab && productId <= 0) { 15633 const pidAttr = getDataAttribute(mobileFab, 'pid'); 15634 if (pidAttr) productId = parseInt(pidAttr); 15635 } 15636 15637 // Set attributes on desktop buttons 15638 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15639 desktopButtons.forEach(button => { 15640 if (productId > 0) { 15641 button.setAttribute('data-product-id', productId); 15642 if (variantId > 0) { 15643 button.setAttribute('data-variant-id', variantId); 15644 } 15645 } 15646 }); 15647 15648 // Set attributes on mobile buttons 15649 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15650 mobileButtons.forEach(button => { 15651 if (productId > 0) { 15652 button.setAttribute('data-product-id', productId); 15653 if (variantId > 0) { 15654 button.setAttribute('data-variant-id', variantId); 15655 } 15656 } 15657 }); 15658 15659 15660 15661 // Initial stock display update on page load 15662 if (productId > 0) { 15663 setTimeout(() => { 15664 updateStockDisplay(productId, variantId); 15665 15666 }, 100); 15667 } 15668 } 15669 15670 /** 15671 * Update button attributes when variant changes 15672 */ 15673 function updateButtonAttributesForVariant(variantId) { 15674 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15675 15676 if (productId <= 0) return; 15677 15678 // Update desktop buttons 15679 const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher'); 15680 desktopButtons.forEach(button => { 15681 button.setAttribute('data-product-id', productId); 15682 if (variantId > 0) { 15683 button.setAttribute('data-variant-id', variantId); 15684 } else { 15685 button.removeAttribute('data-variant-id'); 15686 } 15687 }); 15688 15689 // Update mobile buttons 15690 const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy'); 15691 mobileButtons.forEach(button => { 15692 button.setAttribute('data-product-id', productId); 15693 if (variantId > 0) { 15694 button.setAttribute('data-variant-id', variantId); 15695 } else { 15696 button.removeAttribute('data-variant-id'); 15697 } 15698 }); 15699 15700 15701 } 15702 15703 /** 15704 * Setup variant change integration with existing product-detail.php handlers 15705 */ 15706 function setupVariantChangeIntegration() { 15707 // Strategy: Hook into existing applyVariant function instead of adding new listener 15708 // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.) 15709 15710 // Method 1: Hook into existing applyVariant function 15711 setTimeout(() => { 15712 if (typeof window.applyVariant === 'function') { 15713 const originalApplyVariant = window.applyVariant; 15714 window.applyVariant = function(v, focusImage) { 15715 // Call original function first 15716 const result = originalApplyVariant.call(this, v, focusImage); 15717 15718 // Then add our stock validator logic 15719 const variantId = v ? (v.id || v.ID || 0) : 0; 15720 15721 // Update button attributes for stock validator 15722 updateButtonAttributesForVariant(variantId); 15723 15724 // Update stock display using embedded data (instant) 15725 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15726 if (productId > 0) { 15727 updateStockDisplay(productId, variantId); 15728 } return result; 15729 }; 15730 15731 15732 } else { 15733 15734 setupVariantObserver(); 15735 } 15736 }, 500); 15737 15738 // Method 2: Direct observation of hidden input value changes (fallback) 15739 setTimeout(() => { 15740 setupVariantObserver(); 15741 }, 1000); 15742 } 15743 15744 /** 15745 * Setup variant observer as fallback when applyVariant hook fail
15745s 15746 */ 15747 function setupVariantObserver() { 15748 const variantInput = document.getElementById('pd-variant-id'); 15749 if (variantInput) { 15750 let lastVariantId = parseInt(variantInput.value || '0'); 15751 15752 // Use MutationObserver to watch for value changes 15753 if (window.MutationObserver) { 15754 const observer = new MutationObserver((mutations) => { 15755 mutations.forEach((mutation) => { 15756 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 15757 const newVariantId = parseInt(variantInput.value || '0'); 15758 if (newVariantId !== lastVariantId) { 15759 15760 updateButtonAttributesForVariant(newVariantId); 15761 15762 // Update stock display with embedded data 15763 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15764 if (productId > 0) { 15765 updateStockDisplay(productId, newVariantId); 15766 } 15767 15768 lastVariantId = newVariantId; 15769 } 15770 } 15771 }); 15772 }); 15773 15774 observer.observe(variantInput, { 15775 attributes: true, 15776 attributeFilter: ['value'] 15777 }); 15778 15779 15780 } 15781 15782 // Additional polling as ultimate fallback 15783 setInterval(() => { 15784 const currentVariantId = parseInt(variantInput.value || '0'); 15785 if (currentVariantId !== lastVariantId) { 15786 15787 updateButtonAttributesForVariant(currentVariantId); 15788 15789 // Update stock display with embedded data 15790 const productId = window.PD_ID ? parseInt(window.PD_ID) : 0; 15791 if (productId > 0) { 15792 updateStockDisplay(productId, currentVariantId); 15793 } 15794 15795 lastVariantId = currentVariantId; 15796 } 15797 }, 1000); 15798 15799 15800 } 15801 } 15802 15803 /** 15804 * Update stock display and enforce stock-based button blocking using embedded data 15805 */ 15806 function updateStockDisplay(productId, variantId = 0) { 15807 15808 15809 // Get stock info from embedded data (instant) 15810 const embeddedStockInfo = getStockFromEmbeddedData(variantId); 15811 15812 let stockInfo; 15813 if (embeddedStockInfo) { 15814 // Convert embedded data to standard format 15815 stockInfo = { 15816 can_add_to_cart: embeddedStockInfo.hasStock, 15817 is_in_stock: embeddedStockInfo.hasStock, 15818 available_stock: embeddedStockInfo.stock, 15819 message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`, 15820 product_name: window.PD_NAME || 'Produk', 15821 sku: embeddedStockInfo.sku 15822 }; 15823 15824 15825 } else { 15826 // Fallback to API call if embedded data not available 15827 15828 checkStock(productId, variantId, 1) 15829 .then(apiStockInfo => { 15830 updateStockDisplayWithInfo(apiStockInfo, productId, variantId); 15831 }) 15832 .catch(error => { 15833 console.error('Failed to update stock display via API:', error); 15834 }); 15835 return; 15836 } 15837 15838 updateStockDisplayWithInfo(stockInfo, productId, variantId); 15839 } 15840 15841 /** 15842 * Update stock display with provided stock info 15843 */ 15844 function updateStockDisplayWithInfo(stockInfo, productId, variantId) { 15845 // Store current stock status globally for blocking checks 15846 window._currentStockInfo = stockInfo; 15847 15848 // Update stock indicators 15849 const stockIndicators = document.querySelectorAll('.stock-indicator');
15850 stockIndicators.forEach(indicator => { 15851 if (stockInfo.is_in_stock) { 15852 indicator.classList.add('in-stock'); 15853 indicator.classList.remove('out-of-stock'); 15854 } else { 15855 indicator.classList.add('out-of-stock'); 15856 indicator.classList.remove('in-stock'); 15857 } 15858 }); 15859 15860 // Update stock count 15861 const stockCounts = document.querySelectorAll('.stock-count'); 15862 stockCounts.forEach(count => { 15863 count.textContent = stockInfo.available_stock || 0; 15864 }); 15865 15866 // CRITICAL: For digital products, DO NOT modify button states here 15867 // PHP already handles button disable/enable based on LICENSE_POOL count 15868 // Only physical products need JavaScript stock validation 15869 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15870 15871 if (isDigitalProduct) { 15872 // For digital products: ONLY update global stock info, don't touch buttons 15873 // Let PHP-rendered disabled state remain authoritative 15874 console.log('[Stock Validator] Digital product detected - skipping button state modification'); 15875 return; // Exit early, preserve PHP-set button states 15876 } 15877 15878 // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY) 15879 const allButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15880 allButtons.forEach(button => { 15881 if (stockInfo.can_add_to_cart) { 15882 // STOCK AVAILABLE - ENABLE BUTTONS 15883 button.classList.remove(config.disabledClass); 15884 button.disabled = false; 15885 button.style.opacity = ''; 15886 button.style.cursor = ''; 15887 15888 // CRITICAL: Remove stock blocking attributes when stock becomes available 15889 button.removeAttribute('data-stock-blocked'); 15890 button.removeAttribute('data-stock-reason'); 15891 15892 15893 } else { 15894 // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING 15895 button.classList.add(config.disabledClass); 15896 button.disabled = true; 15897 button.style.opacity = '0.5'; 15898 button.style.cursor = 'not-allowed'; 15899 15900 // Mark as stock-blocked to prevent re-enabling 15901 button.setAttribute('data-stock-blocked', 'true'); 15902 button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia'); 15903 15904 15905 } 15906 }); 15907 15908 // Start aggressive button monitoring for out-of-stock variants 15909 if (!stockInfo.can_add_to_cart) { 15910 enforceStockDisabling(productId, variantId); 15911 } 15912 15913 15914 } 15915 15916 /** 15917 * Aggressively enforce button disabling for out-of-stock items 15918 * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative) 15919 */ 15920 function enforceStockDisabling(productId, variantId) { 15921 // CRITICAL: Skip enforcement for digital products - PHP handles button states 15922 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15923 if (isDigitalProduct) { 15924 console.log('[Stock Validator] Digital product - skipping button enforcement'); 15925 return; // Exit early, let PHP-rendered disabled state remain 15926 } 15927 15928 const monitorInterval = setInterval(() => { 15929 // Check if variant changed 15930 const currentVariantSelector = document.getElementById('pd-variant-id'); 15931 const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0; 15932 15933 if (currentVariantId !== variantId) { 15934 // Variant changed, stop monitoring this variant 15935 clearInterval(monitorInterval); 15936 15937 return; 15938 } 15939 15940 // Check current stock status using embedded data (real-time check) 15941 const currentStockInfo = getStockFromEmbeddedData(currentVariantId); 15942 15943 if (currentStockInfo && !currentStockInfo.hasStock) { 15944 // Still out of stock - continue monitoring for tampering 15945 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15946 buttons.forEach(button => { 15947 // Check if button was manually enabled (inspect attack) 15948 if (!button.disabled || button.style.opacity !== '0.5') { 15949 15950 // Force re-disable 15951 button.disabled = true; 15952 button.style.opacity = '0.5'; 15953 button.style.cursor = 'not-allowed'; 15954 button.setAttribute('data-stock-blocked', 'true'); 15955 button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`); 15956 15957 // Show warning toast 15958 if (window.showToast) { 15959 window.showToast({ 15960 type: 'warning', 15961 title: 'Aksi Tidak Diizinkan', 15962 message: 'Produk ini sedang habis stok. Silakan pilih varian lain.', 15963 duration: 3000 15964 }); 15965 } 15966 } 15967 }); 15968 } else { 15969 // Stock available OR embedded data shows stock - stop monitoring 15970 clearInterval(monitorInterval); 15971 15972 // For PHYSICAL products only, enable buttons 15973 // For DIGITAL products, skip (PHP is authoritative) 15974 const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true'; 15975 15976 if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) { 15977 const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy'); 15978 buttons.forEach(button => { 15979 button.disabled = false;
15980 button.style.opacity = ''; 15981 button.style.cursor = ''; 15982 button.removeAttribute('data-stock-blocked'); 15983 button.removeAttribute('data-stock-reason'); 15984 button.classList.remove(config.disabledClass); 15985 }); 15986 15987 } 15988 } 15989 }, 500); // Check every 500ms for tampering attempts 15990 15991 // Auto-stop monitoring after 30 seconds to prevent memory leaks 15992 setTimeout(() => { 15993 clearInterval(monitorInterval); 15994 15995 }, 30000); 15996 } 15997 15998 /** 15999 * Override addToCartSubmit function to add stock validation 16000 * This function now works with pd-addtocart.js integration 16001 */ 16002 function overrideAddToCartSubmitFunction() { 16003 // Check if pd-addtocart.js is already loaded 16004 const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 16005 window.addToCartSubmit.toString().includes('showToast'); 16006 16007 if (pdAddToCartExists) { 16008 16009 16010 // Store the pd-addtocart.js function 16011 const pdAddToCartSubmit = window.addToCartSubmit; 16012 window._pdAddToCartSubmit = pdAddToCartSubmit; 16013 16014 // Override with stock validation that calls pd-addtocart.js 16015 window.addToCartSubmit = async function(e, productId) { 16016 16017 // Don't prevent default here - let pd-addtocart handle it if stock is valid 16018 16019 try { 16020 // Extract product data from form (same way as pd-addtocart.js) 16021 const form = e.target; 16022 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16023 16024 // CRITICAL: Always get current variant from selector (real-time) 16025 let variantId = 0; 16026 const variantSelector = document.getElementById('pd-variant-id'); 16027 if (variantSelector) { 16028 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16029 } 16030 16031 if (!productId || productId <= 0) { 16032 return pdAddToCartSubmit(e, productId); 16033 } 16034 16035 16036 // Validate stock before proceeding 16037 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16038 16039 if (isValid) { 16040 // Stock OK, call pd-addtocart.js function (it will handle toast) 16041 return pdAddToCartSubmit(e, productId); 16042 } else { 16043 // Prevent the form submission 16044 e.preventDefault(); 16045 // Stock validation failed, modal already shown by validateBeforeCart 16046 return false; 16047 } 16048 16049 } catch (error) { 16050 e.preventDefault(); 16051 // On error, show modal and don't proceed 16052 showStockModal({ 16053 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16054 is_active: false 16055 }, 'cart'); 16056 return false; 16057 } 16058 }; 16059 } else { 16060 // Original logic for when pd-addtocart.js is not present 16061 const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null; 16062 16063 if (originalAddToCartSubmit) { 16064 window._originalAddToCartSubmit = originalAddToCartSubmit; 16065 } 16066 16067 // Override with stock validation 16068 window.addToCartSubmit = async function(e, productId) { 16069 16070 e.preventDefault(); 16071 16072 try { 16073 const form = e.target; 16074 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16075 16076 // CRITICAL: Always get current variant from selector (real-time) 16077 let variantId = 0; 16078 const variantSelector = document.getElementById('pd-variant-id'); 16079 if (variantSelector) { 16080 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16081 } 16082 16083 if (!productId || productId <= 0) { 16084 if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId); 16085 return false;
16086 } 16087 16088 16089 const isValid = await validateBeforeCart(productId, variantId, qty, 'cart'); 16090 16091 if (isValid) { 16092 if (originalAddToCartSubmit) { 16093 return originalAddToCartSubmit(e, productId); 16094 } else { 16095 if (typeof window.addToCart === 'function') { 16096 await window.addToCart(productId, qty, variantId); 16097 } 16098 } 16099 } else { 16100 } 16101 16102 } catch (error) { 16103 showStockModal({ 16104 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16105 is_active: false 16106 }, 'cart'); 16107 } 16108 16109 return false; 16110 }; 16111 } 16112 16113 16114 } 16115 16116 /** 16117 * Override buyNow function to add stock validation 16118 */ 16119 function overrideBuyNowFunction() { 16120 // Store original buyNow function if it exists 16121 const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null; 16122 16123 // Override with stock validation 16124 window.buyNow = async function(e) { 16125 16126 try { 16127 // Get product data (same way as pd-buynow.js) 16128 const form = document.getElementById('pd-buy-form'); 16129 if (!form) { 16130 if (originalBuyNow) return originalBuyNow(e); 16131 return; 16132 } 16133 16134 const productId = parseInt(form.dataset.pid || '0', 10); 16135 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16136 16137 // CRITICAL: Always get current variant from selector (real-time) 16138 let variantId = 0; 16139 const variantSelector = document.getElementById('pd-variant-id'); 16140 if (variantSelector) { 16141 variantId = parseInt(variantSelector.value || '0', 10) || 0; 16142 } 16143 16144 if (!productId || productId <= 0) { 16145 if (originalBuyNow) return originalBuyNow(e); 16146 return; 16147 } 16148 16149 16150 // Validate stock before proceeding 16151 const isValid = await validateBeforeCart(productId, variantId, qty, 'buy'); 16152 16153 if (isValid) { 16154 // Stock OK, call original buyNow function 16155 if (originalBuyNow) { 16156 return originalBuyNow(e); 16157 } else { 16158 } 16159 } else { 16160 // Stock validation failed, modal already shown by validateBeforeCart 16161 } 16162 16163 } catch (error) { 16164 // On error, show modal and don't proceed 16165 showStockModal({ 16166 message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.', 16167 is_active: false 16168 }, 'buy'); 16169 } 16170 }; 16171 16172 16173 } 16174 16175 /** 16176 * Initialize stock validation 16177 */ 16178 function init() { 16179 // Wait for DOM ready 16180 if (document.readyState === 'loading') { 16181 addEventListener(document, 'DOMContentLoaded', function() { 16182 initializeButtonAttributes(); 16183 setupVariantChangeIntegration(); // Setup variant change integration 16184 interceptCartButtons(); 16185 setupGlobalClickInterceptor(); // Setup global click security interceptor // Override functions after other scripts load 16186 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16187 setTimeout(() => { 16188 overrideAddToCartSubmitFunction(); 16189 overrideBuyNowFunction(); 16190 }, 100); 16191 16192 setTimeout(() => { 16193 // Re-check and re-override if pd-addtocart.js loaded later 16194 overrideAddToCartSubmitFunction(); 16195 }, 500); 16196 16197 setTimeout(() => { 16198 // Final check for very late loading scripts 16199 overrideAddToCartSubmitFunction(); 16200 }, 1000); 16201 16202 16203 }); 16204 } else { 16205 initializeButtonAttributes(); 16206 setupVariantChangeIntegration(); // Setup variant change integration 16207 interceptCartButtons(); 16208 setupGlobalClickInterceptor(); // Setup global click security interceptor 16209 16210 // Override functions after other scripts load 16211 // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js 16212 setTimeout(() => { 16213 overrideAddToCartSubmitFunction(); 16214 overrideBuyNowFunction(); 16215 }, 100); 16216 16217 setTimeout(() => { 16218 // Re-check and re-override if pd-addtocart.js loaded later 16219 overrideAddToCartSubmitFunction(); 16220 }, 500); 16221 16222 setTimeout(() => { 16223 // Final check for very late loading scripts 16224 overrideAddToCartSubmitFunction(); 16225 }, 1000); 16226 16227 16228 } 16229 } 16230 16231 // Public API 16232 return { 16233 init: init, 16234 checkStock: checkStock, 16235 validateBeforeCart: validateBeforeCart, 16236 updateStockDisplay: updateStockDisplay, 16237 showStockModal: showStockModal, 16238 config: config 16239 }; 16240})(); 16241 16242// Auto-initialize 16243if (typeof window !== 'undefined') { 16244 StockValidator.init(); 16245} 16246 16247</script>
vendor: 1 bytes, line 16247
16247
16248<script data-inlined="pd-addtocart.js"> 16249(function(){ 16250 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16251 function getVariantSummaryText(){ 16252 const vs = document.getElementById('pd-variant-summary'); 16253 if (!vs) return ''; 16254 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16255 return items.length ? items.join(' / ') : ''; 16256 } 16257 window.addToCartSubmit = async function(e, productId){ 16258 e.preventDefault(); 16259 try { 16260 const form = e.target; 16261 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16262 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16263 await (window.addToCart ? window.addToCart(productId, qty, variantId) : Promise.reject(new Error('addToCart tidak tersedia'))); 16264 16265 if (window.PlazaAnalytics) { 16266 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16267 } 16268 16269 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16270 const sku = (function(){ const el=document.getElementById('pd-sku'); return el?el.textContent.trim(): (window.PD_SKU||''); })(); 16271 const variantText = getVariantSummaryText(); 16272 16273 if (window.showToast) { 16274 window.showToast({ 16275 type: 'success', 16276 title: 'Berhasil ditambahkan ke Keranjang', 16277 message: `<strong>${esc(name)}</strong>${variantText?` · <em>${esc(variantText)}</em>`:''}<br>SKU: ${esc(sku)} · Qty: ${qty}`, 16278 duration: 5000, 16279 actions: [ 16280 { label: 'Lihat Keranjang', onClick: ()=> { window.location.href = '/keranjang'; } }, 16281 { label: 'Lanjut Belanja', variant: 'alt', onClick: ()=>{} } 16282 ] 16283 }); 16284 } 16285 } catch (err) { 16286 if (window.showToast) { 16287 window.showToast({ type:'warn', title:'Gagal menambahkan', message: String(err?.message||'Terjadi kesalahan'), duration: 4500 }); 16288 } else { 16289 console.error(err); 16290 } 16291 } 16292 return false; 16293 }; 16294})(); 16295</script>
vendor: 1 bytes, line 16295
16295
16296<script data-inlined="pd-buynow.js"> 16297(function(){ 16298 16299 // CSRF Token Helper 16300 function getCsrfToken() { 16301 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 16302 } 16303 16304 // Utility functions 16305 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 16306 16307 function getVariantSummaryText(){ 16308 const vs = document.getElementById('pd-variant-summary'); 16309 if (!vs) return ''; 16310 const items = Array.from(vs.querySelectorAll('.vs-item strong')).map(el => el.textContent.trim()).filter(Boolean); 16311 return items.length ? items.join(' / ') : ''; 16312 } 16313 16314 function getCurrentProductData(){ 16315 const form = document.getElementById('pd-buy-form'); 16316 if (!form) return null; 16317 16318 const productId = parseInt(form.dataset.pid || '0', 10); 16319 const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1; 16320 const variantId = parseInt(document.getElementById('pd-variant-id')?.value || '0', 10) || 0; 16321 16322 if (!productId || productId <= 0) { 16323 throw new Error('Product ID tidak valid'); 16324 } 16325 16326 if (qty <= 0) { 16327 throw new Error('Quantity harus lebih dari 0'); 16328 } 16329 16330 return { productId, qty, variantId }; 16331 } 16332 16333 function createBuyNowPayload(productId, qty, variantId) { 16334 // Get product metadata 16335 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16336 16337 // Get correct price, SKU, and weight from selected variant data instead of DOM elements 16338 let price = 0; 16339 let sku = ''; 16340 let weight = 0; 16341 let dimensions = null; 16342 16343 if (variantId > 0 && typeof window.variants !== 'undefined' && Array.isArray(window.variants)) { 16344 // Find the selected variant in the variants array 16345 const selectedVariant = window.variants.find(v => v.id === variantId); 16346 if (selectedVariant) { 16347 price = selectedVariant.final || 0; 16348 sku = selectedVariant.sku || ''; 16349 weight = selectedVariant.weight || 0; 16350 16351 // Include dimensions if available 16352 if (selectedVariant.length_cm || selectedVariant.width_cm || selectedVariant.height_cm) { 16353 dimensions = { 16354 length_cm: selectedVariant.length_cm || 0, 16355 width_cm: selectedVariant.width_cm || 0, 16356 height_cm: selectedVariant.height_cm || 0 16357 }; 16358 } 16359 16360 // Debug info to verify fix is working 16361 if (window.console && window.console.debug) { 16362 console.debug('BuyNow: Using variant data', { 16363 variantId: variantId, 16364 price: price, 16365 sku: sku, 16366 weight: weight, 16367 dimensions: dimensions, 16368 variantData: selectedVariant 16369 }); 16370 } 16371 } 16372 } 16373 16374 // Fallback to DOM elements if variant data not found 16375 if (price === 0) { 16376 const priceTxt = document.getElementById('pd-price-current')?.textContent || ''; 16377 price = parseInt(String(priceTxt).replace(/[^\d]/g,''), 10) || 0; 16378 16379 if (window.console && window.console.debug) { 16380 console.debug('BuyNow: Using DOM price fallback', { 16381 priceTxt: priceTxt, 16382 price: price, 16383 variantId: variantId, 16384 reason: variantId > 0 ? 'variant_data_not_found' : 'no_variant_selected' 16385 }); 16386 } 16387 } 16388 16389 if (!sku) { 16390 const skuEl = document.getElementById('pd-sku'); 16391 sku = skuEl ? skuEl.textContent.trim() : (window.PD_SKU || ''); 16392 } 16393 16394 const imgEl = document.getElementById('pd-main-img'); 16395 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 16396 16397 const variantText = getVariantSummaryText(); 16398 16399 // Get pre-order data if product is pre-order 16400 const preorderData = window.PD_PREORDER_DATA || null; 16401 16402 return { 16403 product_id: productId, 16404 qty: qty, 16405 variant_id: variantId, 16406 meta: { 16407 name, 16408 sku, 16409 price, 16410 image_url, 16411 variant_text: variantText, 16412 weight: weight, 16413 dimensions: dimensions 16414 }, 16415 is_preorder: preorderData ? 'Y' : 'N', 16416 preorder_estimated_arrival: preorderData ? preorderData.estimated_arrival : null, 16417 preorder_note: preorderData ? preorderData.note : '' 16418 }; 16419 } 16420 16421 /** 16422 * Main Buy Now function
16423 * Performs validation and redirects to checkout with buy_now mode 16424 */ 16425 window.buyNow = async function(e) { 16426 if (e) e.preventDefault(); 16427 16428 try { 16429 // Check user login status 16430 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16431 document.body.classList.contains('logged-in') || 16432 window.userLoggedIn === true || 16433 document.body.dataset.userLoggedIn === 'true'; 16434 16435 if (!isLoggedIn) { 16436 // Redirect to login with return URL to current page 16437 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16438 window.location.href = '/login?return=' + currentUrl; 16439 return; 16440 } 16441 16442 // Get current product data 16443 const { productId, qty, variantId } = getCurrentProductData(); 16444 16445 // Create payload for buy now 16446 const payload = createBuyNowPayload(productId, qty, variantId); 16447 16448 // Store buy now data in sessionStorage for checkout page 16449 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16450 16451 // Track add_to_cart (buy now counts as purchase intent) 16452 if (window.PlazaAnalytics) { 16453 window.PlazaAnalytics.track('add_to_cart', { slug: String(window._pit_slug || '') }); 16454 } 16455 16456 // Redirect to checkout with buy_now mode 16457 window.location.href = '/checkout?mode=buy_now'; 16458 16459 } catch (err) { 16460 console.error('Buy Now error:', err); 16461 16462 if (window.showToast) { 16463 window.showToast({ 16464 type: 'warn', 16465 title: 'Gagal memproses', 16466 message: String(err?.message || 'Terjadi kesalahan saat memproses pembelian'), 16467 duration: 4500 16468 }); 16469 } else { 16470 alert(err.message || 'Terjadi kesalahan saat memproses pembelian'); 16471 } 16472 } 16473 }; 16474 16475 /** 16476 * Alternative Buy Now with confirmation dialog 16477 */ 16478 window.buyNowWithConfirm = async function(e) { 16479 if (e) e.preventDefault(); 16480 16481 try { 16482 const { productId, qty, variantId } = getCurrentProductData(); 16483 16484 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : (document.getElementById('pd-title')?.textContent || ''); 16485 const variantText = getVariantSummaryText(); 16486 const priceTxt = document.getElementById('pd-price-current')?.textContent || 'Rp0'; 16487 16488 const confirmMessage = `Lanjut ke checkout?\n\nProduk: ${name}${variantText ? `\nVariant: ${variantText}` : ''}\nQty: ${qty}\nHarga: ${priceTxt}`; 16489 16490 if (!confirm(confirmMessage)) { 16491 return; 16492 } 16493 16494 // Proceed with buy now 16495 await window.buyNow(e); 16496 16497 } catch (err) { 16498 console.error('Buy Now with confirm error:', err); 16499 alert(err.message || 'Terjadi kesalahan'); 16500 } 16501 }; 16502 16503 /** 16504 * Attach event listener for Buy with Voucher button 16505 */ 16506 function setupBuyButtons() { 16507 // Setup desktop buy button (regular buy now without voucher) 16508 const buyBtn = document.getElementById('btn-buy'); 16509 if (buyBtn) { 16510 buyBtn.addEventListener('click', function(e) { 16511 e.preventDefault(); 16512 if (buyBtn.disabled) return; 16513 if (window.buyNow) { 16514 window.buyNow(e); 16515 } 16516 }); 16517 } 16518 16519 // Setup desktop buy with voucher button 16520 const voucherBtn = document.getElementById('btn-buy-voucher'); 16521 if (voucherBtn) { 16522 voucherBtn.addEventListener('click', function(e) { 16523 e.preventDefault(); 16524 16525 // Validate stock availability before proceeding 16526 if (this.disabled || this.getAttribute('data-stock-blocked') === 'true') { 16527 const stockReason = this.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16528 16529 if (window.showToast) { 16530 window.showToast({ 16531 type: 'warn', 16532 title: 'Stok Tidak Tersedia', 16533 message: stockReason, 16534 duration: 3500 16535 }); 16536 } else { 16537 alert(stockReason); 16538 } 16539 return; 16540 } 16541 16542 const voucherCode = this.dataset.voucherCode; 16543 if (voucherCode) { 16544 window.buyNowWithVoucher(e, voucherCode); 16545 } 16546 }); 16547 } 16548 16549 } 16550 16551 // CRITICAL: Attach mobile button handler at DOCUMENT level with CAPTURE phase 16552 // This is the ONLY way to guarantee it fires regardless of other scripts 16553 document.addEventListener('click', function(e) { 16554 // Check if click is on mobile buy button or its children 16555 const target = e.target.closest('#mf-buy'); 16556 if (!target) return; // Not our button 16557 16558 e.preventDefault(); 16559 e.stopPropagation(); 16560 e.stopImmediatePropagation(); 16561 16562 if (target.disabled) return; 16563
16564 // Get voucher code from parent FAB's data attribute 16565 const mobileFab = document.getElementById('pd-mobile-fab'); 16566 const voucherCode = mobileFab ? (mobileFab.getAttribute('data-voucher-code') || '') : ''; 16567 16568 if (voucherCode && voucherCode.trim() !== '' && window.buyNowWithVoucher) { 16569 window.buyNowWithVoucher(e, voucherCode); 16570 } else if (window.buyNow) { 16571 window.buyNow(e); 16572 } 16573 }, true); // CAPTURE PHASE - fires BEFORE any bubble phase handlers 16574 16575 // Execute immediately if DOM already loaded, otherwise wait 16576 if (document.readyState === 'loading') { 16577 document.addEventListener('DOMContentLoaded', setupBuyButtons); 16578 } else { 16579 setupBuyButtons(); 16580 } 16581 16582 /** 16583 * Buy Now with Auto-Applied Voucher 16584 * Applies voucher to session before redirecting to checkout (like cart does) 16585 * 16586 * @param {Event} e - Click event 16587 * @param {string} voucherCode - Voucher code to apply 16588 */ 16589 window.buyNowWithVoucher = async function(e, voucherCode) { 16590 if (e) e.preventDefault(); 16591 16592 console.log('[buyNowWithVoucher] Called with voucher code:', voucherCode); 16593 16594 try { 16595 // CRITICAL: Check stock availability first 16596 const voucherBtn = document.getElementById('btn-buy-voucher'); 16597 if (voucherBtn) { 16598 if (voucherBtn.disabled || voucherBtn.getAttribute('data-stock-blocked') === 'true') { 16599 const stockReason = voucherBtn.getAttribute('data-stock-reason') || 'Stok tidak tersedia'; 16600 16601 if (window.showToast) { 16602 window.showToast({ 16603 type: 'warn', 16604 title: 'Stok Tidak Tersedia', 16605 message: stockReason, 16606 duration: 3500 16607 }); 16608 } else { 16609 alert(stockReason); 16610 } 16611 return; 16612 } 16613 } 16614 16615 // Check login status first 16616 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 16617 document.body.classList.contains('logged-in') || 16618 window.userLoggedIn === true || 16619 document.body.dataset.userLoggedIn === 'true'; 16620 16621 console.log('[buyNowWithVoucher] User logged in:', isLoggedIn); 16622 16623 if (!isLoggedIn) { 16624 const currentUrl = encodeURIComponent(window.location.pathname + window.location.search); 16625 window.location.href = '/login?return=' + currentUrl; 16626 return; 16627 } 16628 16629 // Get current product data 16630 const { productId, qty, variantId } = getCurrentProductData(); 16631 const payload = createBuyNowPayload(productId, qty, variantId); 16632 16633 console.log('[buyNowWithVoucher] Product data:', { productId, qty, variantId }); 16634 16635 // Get category path for voucher validation 16636 const categoryPath = window.PD_CATEGORY_PATH || ''; 16637 16638 // Step 1: Clear any existing voucher first (important for clean state) 16639 console.log('[buyNowWithVoucher] Step 1: Clearing existing voucher...'); 16640 try { 16641 await fetch('/api/voucher/remove.php', { 16642 method: 'POST', 16643 headers: { 16644 'X-CSRF-Token': getCsrfToken() 16645 }, 16646 body: JSON.stringify({ 16647 csrf_token: getCsrfToken() 16648 }), 16649 credentials: 'same-origin' 16650 }); 16651 } catch (e) { 16652 // Ignore error if no voucher exists 16653 console.debug('No existing voucher to remove or removal failed'); 16654 } 16655 16656 // Step 2: Apply voucher to session (like cart does) 16657 console.log('[buyNowWithVoucher] Step 2: Applying voucher to session...'); 16658 const applyRes = await fetch('/api/voucher/apply.php', { 16659 method: 'POST', 16660 headers: { 16661 'Content-Type': 'application/json', 16662 'X-CSRF-Token': getCsrfToken() 16663 }, 16664 credentials: 'same-origin', 16665 body: JSON.stringify({ 16666 csrf_token: getCsrfToken(), 16667 code: voucherCode, 16668 items: [{ 16669 pid: productId, 16670 price: payload.meta.price, 16671 qty: qty, 16672 category_path: categoryPath 16673 }], 16674 shipping_cost: null // Will be calculated in checkout 16675 }) 16676 }); 16677 16678 const applyData = await applyRes.json(); 16679 16680 console.log('[buyNowWithVoucher] Voucher apply response:', applyData); 16681 16682 if (!applyData.success) { 16683 // Voucher invalid - show error and fallback to regular buy now 16684 console.warn('[buyNowWithVoucher] Voucher application failed:', applyData.message); 16685 if (window.showToast) { 16686 window.showToast({ 16687 type: 'error', 16688 title: 'Voucher Tidak Valid', 16689 message: applyData.message || 'Voucher tidak dapat diterapkan untuk produk ini', 16690 duration: 4000 16691 }); 16692 } else { 16693 alert(applyData.message || 'Voucher tidak dapat diterapkan'); 16694 } 16695 16696 // Fallback to regular buy now without voucher 16697 console.log('Voucher failed, proceeding with regular buy now'); 16698 return window.buyNow(e); 16699 } 16700 16701 // Step 3: Store buy now data in sessionStorage 16702 console.log('[buyNowWithVoucher] Step 3: Storing buy now data in sessionStorage'); 16703 sessionStorage.setItem('buyNowData', JSON.stringify(payload)); 16704 16705 // Step 4: Redirect to checkout 16706 // Voucher sudah di session, checkout akan auto-load via loadAppliedVoucher() 16707 console.log('[buyNowWithVoucher] Step 4: Redirecting to checkout with voucher applied'); 16708 window.location.href = '/checkout?mode=buy_now'; 16709 16710 } catch (err) { 16711 console.error('Buy Now with Voucher error:', err); 16712 16713 if (window.showToast) { 16714 window.showToast({ 16715 type: 'error', 16716 title: 'Terjadi Kesalahan', 16717 message: 'Gagal memproses pembelian dengan voucher. Silakan coba lagi.', 16718 duration: 3000 16719 }); 16720 } else { 16721 alert('Gagal memproses pembelian. Silakan coba lagi.'); 16722 } 16723 } 16724 }; 16725})(); 16726</script>
vendor: 1 bytes, line 16726
16726
16727<script data-inlined="pd-voucher-recalc.js"> 16728/** 16729 * Product Detail - Voucher Discount Recalculation 16730 * 16731 * Handles dynamic voucher discount recalculation when variant price changes 16732 * Supports both PERCENTAGE and FIXED_AMOUNT voucher types 16733 */ 16734 16735(function() { 16736 'use strict'; 16737 16738 /** 16739 * Calculate voucher discount based on type and price 16740 * 16741 * @param {string} type - Voucher type: 'PERCENTAGE' or 'FIXED_AMOUNT' 16742 * @param {number} value - Voucher value (percentage or fixed amount) 16743 * @param {number} price - Current variant price 16744 * @param {number} maxDiscount - Maximum discount for percentage vouchers (0 = no limit) 16745 * @return {number} Calculated discount amount 16746 */ 16747 function calculateVoucherDiscount(type, value, price, maxDiscount) { 16748 if (type === 'PERCENTAGE') { 16749 // Calculate percentage discount 16750 let discount = (price * value) / 100; 16751 16752 // Apply max discount cap if set 16753 if (maxDiscount > 0 && discount > maxDiscount) { 16754 discount = maxDiscount; 16755 } 16756 16757 return Math.round(discount); 16758 } else { 16759 // FIXED_AMOUNT - discount doesn't change with price 16760 return Math.round(value); 16761 } 16762 } 16763 16764 /** 16765 * Format number as Rupiah without 'Rp' prefix 16766 * 16767 * @param {number} num - Number to format 16768 * @return {string} Formatted number (e.g., "20.000") 16769 */ 16770 function formatNumber(num) { 16771 return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 16772 } 16773 16774 /** 16775 * Update voucher button discount label 16776 * 16777 * @param {number} newPrice - New variant price 16778 */ 16779 function updateVoucherDiscount(newPrice) { 16780 const voucherBtn = document.getElementById('btn-buy-voucher'); 16781 if (!voucherBtn) return; 16782 16783 // Get voucher data from button attributes 16784 const voucherType = voucherBtn.dataset.voucherType; 16785 const voucherValue = parseFloat(voucherBtn.dataset.voucherValue) || 0; 16786 const voucherMax = parseFloat(voucherBtn.dataset.voucherMax) || 0; 16787 16788 if (!voucherType || voucherValue === 0) return; 16789 16790 // Calculate new discount 16791 const newDiscount = calculateVoucherDiscount(voucherType, voucherValue, newPrice, voucherMax); 16792 16793 // Update the discount label 16794 const saveLabel = voucherBtn.querySelector('.bv-save'); 16795 if (saveLabel) { 16796 saveLabel.textContent = '-Rp' + formatNumber(newDiscount); 16797 } 16798 16799 // Also update mobile FAB voucher display if exists 16800 updateMobileFabVoucher(newDiscount); 16801 } 16802 16803 /** 16804 * Update mobile FAB voucher value 16805 * 16806 * @param {number} discountAmount - New discount amount 16807 */ 16808 function updateMobileFabVoucher(discountAmount) { 16809 const fab = document.getElementById('pd-mobile-fab'); 16810 if (fab) { 16811 fab.dataset.voucherValue = discountAmount.toString(); 16812 } 16813 } 16814 16815 /** 16816 * Listen to variant change events and recalculate discount 16817 */ 16818 function initVoucherRecalculation() { 16819 window.addEventListener('pd-variant-changed', function(e) { 16820 const detail = e.detail || {}; 16821 const newPrice = detail.price || 0; 16822 16823 if (newPrice > 0) { 16824 updateVoucherDiscount(newPrice); 16825 } 16826 }); 16827 } 16828 16829 // Initialize when DOM is ready 16830 if (document.readyState === 'loading') { 16831 document.addEventListener('DOMContentLoaded', initVoucherRecalculation); 16832 } else { 16833 initVoucherRecalculation(); 16834 } 16835 16836})(); 16837 16838</script>
vendor: 1 bytes, line 16838
16838
16839<script nonce="se3iyzEHMc9FQEB4wSyd0w==">document.addEventListener('DOMContentLoaded',function(){if(typeof ProductReviews!=='undefined'){ProductReviews.init(325);}const r=document.getElementById('pd-rating-inline');if(r){r.addEventListener('click',function(e){e.preventDefault();const s=document.getElementById('reviews');if(s){s.scrollIntoView({behavior:'smooth',block:'start'});s.classList.add('highlight-flash');setTimeout(()=>{s.classList.remove('highlight-flash')},1500)}});}});</script>
16839 16840
16841<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16842(function(){function _0x9i0j(el){if(typeof gtag==='function'){gtag('event','qoala_insurance_click',{event_category:'Insurance',event_label:'Qoala Banner Click - Product Detail',link_url:'/plazait-protect',page_location:window.location.href,product_id:parseInt(el.getAttribute('data-product-id'))||0,product_name:el.getAttribute('data-product-name')||'',category_path:el.getAttribute('data-category-path')||'',custom_map:{custom_parameter_1:'qoala_banner_product_detail'}})}} 16843 16844function _0x1k2l(){var a=document.querySelector('.qoala-cta[data-ga-event="qoala_banner_click"]');if(a){a.addEventListener('click',function(e){_0x9i0j(this)})}}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',_0x1k2l)}else{_0x1k2l()}})(); 16845 16846(function(){const pwpSection=document.getElementById('pwp-deals-section');const pwpBadge=document.getElementById('pwp-trigger-badge');if(!pwpSection)return;const productId=parseInt(pwpSection.getAttribute('data-product-id'))||0;const variantId=parseInt(pwpSection.getAttribute('data-variant-id'))||0;const categoryPath=pwpSection.getAttribute('data-category-path')||'';if(!productId)return;function formatRupiah(n){return'Rp'+Math.floor(n).toLocaleString('id-ID')}function loadPWPDeals(){const url=`/api/pwp/get_by_product.php?product_id=${productId}${variantId?`&variant_id=${variantId}`:''}${categoryPath?`&category_path=${encodeURIComponent(categoryPath)}`:''}`;fetch(url).then(r=>r.json()).then(data=>{if(data.success&&data.promotions&&data.promotions.length>0){renderPWPDeals(data.promotions);pwpSection.hidden=false;if(pwpBadge)pwpBadge.hidden=false;setTimeout(function(){if(typeof window.adjustSidebarBanner==='function'){window.adjustSidebarBanner()}},150)}else{pwpSection.hidden=true;if(pwpBadge)pwpBadge.hidden=true}}).catch(err=>{console.error('PWP load error:',err);pwpSection.hidden=true;if(pwpBadge)pwpBadge.hidden=true})}function renderPWPDeals(promotions){const loading=pwpSection.querySelector('.pwp-deals-loading');const grid=document.getElementById('pwp-deals-grid');if(loading)loading.hidden=true;if(!grid)return;grid.innerHTML='';promotions.forEach(promo=>{promo.bundled_products.forEach(product=>{const card=document.createElement('div');card.className='pwp-deal-card';const discount=product.discount>0?product.discount:0;const discountPercent=product.regular_price>0?Math.round((discount/product.regular_price)*100):0;const imgUrl=product.image_url?`/uploads/products/${product.image_url}`:'/assets/img/no-image.png';card.innerHTML=`<div class="pwp-deal-thumb"><img src="${imgUrl}" alt="${escapeHtml(product.name)}" loading="lazy"></div><div class="pwp-deal-info"><h4 class="pwp-deal-name">${escapeHtml(product.name)}</h4><div class="pwp-deal-prices"><span class="pwp-deal-price">${formatRupiah(product.pwp_price)}</span>${product.regular_price>product.pwp_price?`<span class="pwp-deal-original">${formatRupiah(product.regular_price)}</span>`:''}</div></div>${discountPercent>0?`<span class="pwp-deal-badge">-${discountPercent}%</span>`:''}`;grid.appendChild(card)})})}function escapeHtml(str){const div=document.createElement('div');div.textContent=str||'';return div.innerHTML}loadPWPDeals();window.addEventListener('pd-variant-changed',function(e){const detail=e.detail||{};const newVid=detail.variant_id||0;const currentVid=parseInt(pwpSection.getAttribute('data-variant-id'))||0;if(newVid!==currentVid){pwpSection.setAttribute('data-variant-id',newVid);loadPWPDeals()}})})(); 16847</script>
16847<footer class="site-footer footer-compact variant-stripes" role="contentinfo"> 16848 <div class="ft-top ft-collapsible" id="ftTop"> 16849 <div class="container ft-grid"> 16850 16851 <div class="ft-col ft-help"> 16852 <h4 class="ft-title">Butuh Bantuan?</h4> 16853 <p class="ft-desc">Hubungi kami melalui Official WhatsApp interaktif sekarang.</p> 16854 <a class="ft-wa-btn" href="https://mikale.me/whatsappIWANTOGEL" target="_blank" rel="nofollow noopener"> 16855 <span class="ico" aria-hidden="true"> 16856 <svg viewBox="0 0 24 24" width="18" height="18"><path fill="#25D366" d="M12.04 2c-5.52 0-10 4.43-10 9.9 0 1.75.47 3.45 1.38 4.94L2 22l5.3-1.38c1.43.78 3.05 1.19 4.7 1.19 5.52 0 10-4.43 10-9.9S17.56 2 12.04 2Zm5.86 13.9c-.25.7-1.23 1.28-1.7 1.33-.45.06-1.02.08-1.65-.1-.38-.12-.88-.29-1.52-.56-2.66-1.15-4.4-3.83-4.53-4.01-.13-.18-1.08-1.44-1.08-2.75 0-1.31.68-1.95.92-2.22.25-.27.54-.33.72-.33.18 0 .36.01.52.01.17 0 .39-.06.62.47.25.6.85 2.07.92 2.22.07.15.12.33.02.53-.1.2-.15.33-.3.5-.15.17-.32.39-.46.53-.15.15-.3.31-.13.6.18.3.8 1.31 1.73 2.12 1.19 1.03 2.17 1.36 2.48 1.5.31.14.49.12.67-.07.18-.18.77-.82.98-1.1.21-.27.42-.23.7-.14.27.09 1.72.81 2.02.95.3.14.5.22.58.35.08.13.08.72-.17 1.42Z"/></svg> 16857 </span> 16858 Kirim Pesan 16859 </a> 16860 16861 <p class="ft-desc sm-gap">Ikuti kami:</p> 16862 <div class="ft-social"> 16863 <a aria-label="Facebook" href="https://facebook.com/PlazaIt/"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M13.5 22v-8.2h2.7l.4-3.2h-3.1V8c0-.9.3-1.5 1.6-1.5H17V3.7c-.3 0-1.2-.1-2.2-.1-2.2 0-3.8 1.3-3.8 3.9v2.2H8v3.2h3v8.1h2.5Z"/></svg></a> 16864 <a aria-label="Instagram" href="https://www.instagram.com/plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M7.8 2h8.4A5.8 5.8 0 0 1 22 7.8v8.4A5.8 5.8 0 0 1 16.2 22H7.8A5.8 5.8 0 0 1 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2Zm0 2A3.8 3.8 0 0 0 4 7.8v8.4A3.8 3.8 0 0 0 7.8 20h8.4a3.8 3.8 0 0 0 3.8-3.8
16864V7.8A3.8 3.8 0 0 0 16.2 4H7.8Zm9.9 1.7a1.1 1.1 0 1 1 0 2.3 1.1 1.1 0 0 1 0-2.3ZM12 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg></a> 16865 <a aria-label="TikTok" href="https://www.tiktok.com/@plazait"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18.9 7.4a6.4 6.4 0 0 1-3.2-1.2v6.3a5.47 5.47 0 1 1-5.5-5.5c.2 0 .4 0 .6.02v2.37a2.92 2.92 0 0 0-.6-.07 2.8 2.8 0 1 0 2.8 2.8V2h2.3a4.6 4.6 0 0 0 4.5 4.1v2.3Z"/></svg></a> 16866 </div> 16867 </div> 16868 16869 <!-- Collapsible Content Wrapper (Non-Homepage Only) --> 16870 <div class="ft-collapsible-content" id="ftCollapsible"> 16871 16872 <div class="ft-col"> 16873 <h4 class="ft-title">Tentang IWANTOGEL</h4> 16874 <ul class="ft-links"> 16875 <li><a href="https://loklokpro.com/">Tentang IWANTOGEL</a></li> 16876 <li><a href="https://loklokpro.com/">Kontak IWANTOGEL</a></li> 16877 <li><a href="https://loklokpro.com/">Lokasi Toko</a></li> 16878 <li><a href="https://loklokpro.com/">FAQ</a></li> 16879 </ul> 16880 <div class="ft-address"> 16881 <p class="company">PT.IWANTOGEL</p> 16882 <p>,Jl. IWANTOGEL. No.42, RT.1/RW.4, Kp. IWANTOGEL, Kecamatan IWANTOGEL, Kota IWANTOGEL,</p> 16883 <p>Daerah Khusus Ibukota IWANTOGEL 888</p> 16884 </div> 16885 </div> 16886 16887 <div class="ft-col"> 16888 <h4 class="ft-title">Informasi</h4> 16889 <ul class="ft-links"> 16890 <li><a href="/syarat-dan-ketentuan">Syarat & Ketentuan</a></li> 16891 <li><a href="/kebijakan-privasi">Kebijakan Privasi</a></li> 16892 <li><a href="/kebijakan-cookie">Kebijakan Cookie</a></li> 16893 <li><a href="/pengiriman-dan-pengembalian">Pengiriman & Pengembalian</a></li> 16894 <li><a href="/pembayaran-dan-cicilan">Pembayaran & Cicilan</a></li> 16895 <li><a href="/garansi-dan-layanan">Garansi & Layanan</a></li> 16896 </ul> 16897 16898 <div class="ft-payments payments-small" aria-label="Metode Pembayaran"> 16899 <div class="pay-row"> 16900 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-mandiri.png" alt="Mandiri" loading="lazy"> 16901 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bni.gif" alt="BNI" loading="lazy"> 16902 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bri.gif" alt="BRI" loading="lazy"> 16903 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-qris.gif" alt="QRIS" loading="lazy"> 16904 <img src="https://imgmahasuhu.io/images/2026/05/20/wopslot-bca.gif" alt="BCA" loading="lazy"> 16905 </div> 16906 </div> 16907 </div> 16908 16909 </div> 16910 <!-- End Collapsible Content --> 16911 16912 <!-- Expand Button (Mobile Only) --> 16913 <button class="ft-expand-btn" id="ftExpandBtn" aria-label="Lihat semua informasi footer"> 16914 <span class="expand-text">Selengkapnya</span> 16915 <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> 16916 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 16917 </svg> 16918 </button> 16919 16920 </div> 16921 </div> 16922 <div class="ft-bottom"> 16923 <div class="container ft-bottom-inner"> 16924 <div class="ft-copy"> 16925 <span>ANGIN-RIBUT © <span id="yearNow">2026</span></span> 16926 <span>PT ANGIN-RIBUT. All rights reserved.</span> 16927 </div> 16928 </div> 16929 </div> 16930 16931 <style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 16932 /* Chat Widget Styles - Lightweight & Minimal */ 16933 .chat-widget { 16934 position: fixed; 16935 bottom: 20px; 16936 right: 20px; 16937 z-index: 9999; 16938 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 16939 } 16940 16941 /* Hide floating button on mobile - use bottom nav instead */ 16942 @media (max-width: 640px) { 16943 .chat-toggle-btn { 16944 display: none !important; 16945 } 16946 16947 /* Ensure popup is still visible on mobile even when button is hidden */ 16948 .chat-widget { 16949 display: block !important; 16950 } 16951 16952 .chat-popup { 16953 display: none; /* Hidden by default, shown via JS */ 16954 } 16955 16956 .chat-popup[style*="display: block"], 16957 .chat-popup[style*="display:block"] { 16958 display: block !important; 16959 } 16960 } 16961 16962 .chat-toggle-btn { 16963 width: 60px; 16964 height: 60px; 16965 border-radius: 50%; 16966 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 16967 border: none; 16968 color: white; 16969 cursor: pointer; 16970 box-shadow: 0 4px 12px rgba(242, 140, 26, 0.3); 16971 position: relative; 16972 transition: transform 0.2s, box-shadow 0.2s; 16973 } 16974 16975 .chat-toggle-btn:hover { 16976 transform: scale(1.05); 16977 box-shadow: 0 6px 20px rgba(242, 140, 26, 0.4); 16978 } 16979 16980 #chat-toggle-btn .chat-icon { 16981 position: absolute; 16982 top: 50%; 16983 left: 50%; 16984 transform: translate(-50%, -50%); 16985 width: 28px !important; 16986 height: 28px !important; 16987 } 16988 16989 .chat-unread-badge { 16990 position: absolute; 16991 top: -5px; 16992 right: -5px; 16993 background: #ef4444; 16994 color: white; 16995 border-radius: 12px; 16996 padding: 2px 6px; 16997 font-size: 11px; 16998 font-weight: 600;
16999 min-width: 20px; 17000 text-align: center; 17001 } 17002 17003 .chat-popup { 17004 position: absolute; 17005 bottom: 80px; 17006 right: 0; 17007 width: 380px; 17008 max-width: calc(100vw - 40px); 17009 height: 500px; 17010 max-height: calc(100vh - 120px); 17011 background: white; 17012 border-radius: 12px; 17013 box-shadow: 0 10px 40px rgba(0,0,0,0.2); 17014 display: flex; 17015 flex-direction: column; 17016 overflow: hidden; 17017 } 17018 17019 .chat-popup-header { 17020 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17021 color: white; 17022 padding: 16px 20px; 17023 display: flex; 17024 justify-content: space-between; 17025 align-items: center; 17026 flex-shrink: 0; 17027 } 17028 17029 .chat-header-title { 17030 display: flex; 17031 flex-direction: column; 17032 gap: 2px; 17033 min-height: 0; 17034 } 17035 17036 .chat-popup-header h3 { 17037 margin: 0; 17038 font-size: 18px; 17039 font-weight: 600; 17040 line-height: 1.2; 17041 } 17042 17043 .chat-operating-hours { 17044 font-size: 11px; 17045 opacity: 0.9; 17046 font-weight: 400; 17047 display: flex; 17048 align-items: center; 17049 gap: 4px; 17050 line-height: 1.2; 17051 } 17052 17053 .chat-operating-hours svg { 17054 width: 12px; 17055 height: 12px; 17056 opacity: 0.8; 17057 flex-shrink: 0; 17058 } 17059 17060 .chat-header-actions { 17061 display: flex; 17062 align-items: center; 17063 gap: 8px; 17064 } 17065 17066 /* Notification Toggle Button */ 17067 .btn-notif-toggle { 17068 background: none; 17069 border: none; 17070 color: white; 17071 font-size: 18px; 17072 cursor: pointer; 17073 padding: 4px 8px; 17074 transition: all 0.2s; 17075 opacity: 0.8; 17076 } 17077 17078 .btn-notif-toggle:hover { 17079 opacity: 1; 17080 transform: scale(1.1); 17081 } 17082 17083 /* Icon states */ 17084 .btn-notif-toggle[data-status="not-subscribed"] svg { 17085 opacity: 0.7; 17086 } 17087 17088 .btn-notif-toggle[data-status="not-subscribed"]:hover svg { 17089 opacity: 1; 17090 } 17091 17092 .btn-notif-toggle[data-status="subscribed"] svg { 17093 opacity: 1; 17094 animation: bellRing 2s ease-in-out; 17095 } 17096 17097 .btn-notif-toggle[data-status="blocked"] svg { 17098 color: #ff6b6b; 17099 } 17100 17101 .btn-notif-toggle[data-status="not-supported"] { 17102 display: none !important; 17103 } 17104 17105 @keyframes bellRing { 17106 0%, 100% { transform: rotate(0deg); } 17107 10%, 30% { transform: rotate(-10deg); } 17108 20%, 40% { transform: rotate(10deg); } 17109 } 17110 17111 /* Notification Info Bar */ 17112 .chat-notif-info-bar { 17113 position: absolute; 17114 top: 64px; 17115 left: 0; 17116 right: 0; 17117 background: linear-gradient(to right, #e3f2fd, #f5f5f5); 17118 border-bottom: 1px solid #bbdefb; 17119 padding: 8px 12px; 17120 display: flex; 17121 align-items: center; 17122 gap: 8px; 17123 font-size: 12px; 17124 color: #616161; 17125 z-index: 10; 17126 animation: slideDown 0.3s ease; 17127 } 17128 17129 .chat-notif-info-bar svg { 17130 flex-shrink: 0; 17131 } 17132 17133 .chat-notif-info-bar .small { 17134 flex: 1; 17135 line-height: 1.4; 17136 } 17137 17138 .chat-notif-info-bar .btn-dismiss { 17139 background: none; 17140 border: none; 17141 color: #9e9e9e; 17142 cursor: pointer; 17143 padding: 0 4px; 17144 font-size: 16px; 17145 transition: color 0.2s; 17146 } 17147 17148 .chat-notif-info-bar .btn-dismiss:hover { 17149 color: #616161; 17150 } 17151 17152 @keyframes slideDown { 17153 from { 17154 max-height: 0; 17155 opacity: 0; 17156 } 17157 to { 17158 max-height: 50px; 17159 opacity: 1; 17160 } 17161 } 17162 17163 .chat-close-btn { 17164 background: none; 17165 border: none; 17166 color: white; 17167 font-size: 28px; 17168 line-height: 1; 17169 cursor: pointer; 17170 padding: 0; 17171 width: 30px; 17172 height: 30px; 17173 } 17174 17175 .chat-popup-body { 17176 flex: 1 1 auto; 17177 display: flex; 17178 flex-direction: column; 17179 min-height: 0; 17180 overflow: hidden; 17181 height: calc(100% - 64px); 17182 position: relative; 17183 transition: padding-top 0.3s ease; 17184 } 17185 17186 /* Add padding when info bar is visible */ 17187 .chat-popup-body.has-info-bar { 17188 padding-top: 40px; 17189 } 17190 17191 .chat-view { 17192 flex: 1 1 auto; 17193 display: none; 17194 flex-direction: column; 17195 min-height: 0; 17196 overflow: hidden; 17197 } 17198 17199 #chat-list-view { 17200 display: flex; 17201 } 17202 17203 #chat-messages-view { 17204 display: none; 17205 } 17206 17207 #chat-messages-view.active { 17208 display: flex; 17209 flex-direction: column; 17210 } 17211 17212 #chat-new-view { 17213 display: none; 17214 } 17215 17216 #chat-new-view.active { 17217 display: flex; 17218 } 17219 17220 .chat-conversations-list { 17221 flex: 1; 17222 overflow-y: auto; 17223 padding: 0; 17224 background: white; 17225 } 17226 17227 /* Custom Scrollbar for Conversations List */ 17228 .chat-conversations-list::-webkit-scrollbar {
17229 width: 6px; 17230 } 17231 17232 .chat-conversations-list::-webkit-scrollbar-track { 17233 background: #f1f1f1; 17234 } 17235 17236 .chat-conversations-list::-webkit-scrollbar-thumb { 17237 background: #F28C1A; 17238 border-radius: 3px; 17239 } 17240 17241 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17242 background: #D97809; 17243 } 17244 17245 /* Firefox */ 17246 .chat-conversations-list { 17247 scrollbar-width: thin; 17248 scrollbar-color: #F28C1A #f1f1f1; 17249 } 17250 17251 .chat-conversation-item { 17252 padding: 12px 16px; 17253 cursor: pointer; 17254 display: flex; 17255 gap: 12px; 17256 align-items: flex-start; 17257 transition: background 0.15s ease; 17258 color: #33371f; 17259 border-bottom: 1px solid #f0f0f0; 17260 } 17261 17262 .chat-conversation-item:hover { 17263 background: #f5f5f5; 17264 } 17265 17266 .chat-conversation-item.active { 17267 background: #FFF5EB; 17268 } 17269 17270 /* WhatsApp-like Avatar */ 17271 .conversation-avatar { 17272 width: 48px; 17273 height: 48px; 17274 border-radius: 50%; 17275 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17276 flex-shrink: 0; 17277 display: flex; 17278 align-items: center; 17279 justify-content: center; 17280 color: white; 17281 font-weight: 600; 17282 font-size: 18px; 17283 } 17284 17285 .conversation-avatar svg { 17286 width: 24px; 17287 height: 24px; 17288 } 17289 17290 /* WhatsApp-like Content */ 17291 .conversation-content { 17292 flex: 1; 17293 min-width: 0; 17294 display: flex; 17295 flex-direction: column; 17296 gap: 4px; 17297 } 17298 17299 .conversation-header { 17300 display: flex; 17301 justify-content: space-between; 17302 align-items: center; 17303 gap: 8px; 17304 } 17305 17306 .conversation-name { 17307 font-weight: 600; 17308 font-size: 15px; 17309 color: #111; 17310 white-space: nowrap; 17311 overflow: hidden; 17312 text-overflow: ellipsis; 17313 } 17314 17315 .conversation-time { 17316 font-size: 12px; 17317 color: #7d8166; 17318 flex-shrink: 0; 17319 } 17320 17321 .conversation-preview { 17322 display: flex; 17323 justify-content: space-between; 17324 align-items: center; 17325 gap: 8px; 17326 } 17327 17328 .conversation-message { 17329 font-size: 14px; 17330 color: #667781; 17331 white-space: nowrap; 17332 overflow: hidden; 17333 text-overflow: ellipsis; 17334 margin: 0; 17335 flex: 1; 17336 } 17337 17338 .conversation-unread { 17339 background: #25D366; 17340 color: white; 17341 border-radius: 12px; 17342 padding: 2px 8px; 17343 font-size: 12px; 17344 font-weight: 600; 17345 min-width: 20px; 17346 text-align: center; 17347 flex-shrink: 0; 17348 } 17349 17350 /* Active item styling */ 17351 .chat-conversation-item.active .conversation-name { 17352 color: #F28C1A; 17353 } 17354 17355 .chat-conversation-item.active .conversation-time { 17356 color: #F28C1A; 17357 } 17358 17359 .chat-new-btn { 17360 margin: 12px; 17361 padding: 12px; 17362 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17363 color: white; 17364 border: none; 17365 border-radius: 8px; 17366 cursor: pointer; 17367 font-weight: 500; 17368 font-size: 14px; 17369 display: flex; 17370 align-items: center; 17371 justify-content: center; 17372 gap: 8px; 17373 transition: all 0.2s; 17374 flex-shrink: 0; 17375 } 17376 17377 .chat-new-btn:hover { 17378 transform: translateY(-1px); 17379 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17380 } 17381 17382 .chat-conversation-title { 17383 flex: 1; 17384 font-weight: 600; 17385 font-size: 16px; 17386 color: #1f2937; 17387 } 17388 17389 .chat-messages-container { 17390 flex: 1; 17391 overflow-y: auto; 17392 padding: 16px; 17393 display: flex; 17394 flex-direction: column; 17395 gap: 8px; 17396 min-height: 0; 17397 background: #E5DDD5; 17398 } 17399 17400 /* Custom Scrollbar for Messages Container */ 17401 .chat-messages-container::-webkit-scrollbar {
17402 width: 6px; 17403 } 17404 17405 .chat-messages-container::-webkit-scrollbar-track { 17406 background: rgba(0, 0, 0, 0.05); 17407 } 17408 17409 .chat-messages-container::-webkit-scrollbar-thumb { 17410 background: rgba(242, 140, 26, 0.6); 17411 border-radius: 3px; 17412 } 17413 17414 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17415 background: #F28C1A; 17416 } 17417 17418 /* Firefox */ 17419 .chat-messages-container { 17420 scrollbar-width: thin; 17421 scrollbar-color: rgba(242, 140, 26, 0.6) rgba(0, 0, 0, 0.05); 17422 } 17423 17424 /* WhatsApp Group-like Messages */ 17425 .chat-message { 17426 display: flex; 17427 margin-bottom: 4px; 17428 animation: fadeIn 0.2s ease; 17429 } 17430 17431 @keyframes fadeIn { 17432 from { opacity: 0; transform: translateY(10px); } 17433 to { opacity: 1; transform: translateY(0); } 17434 } 17435 17436 .chat-message.mine { 17437 justify-content: flex-end; 17438 } 17439 17440 .chat-message-bubble { 17441 max-width: 80%; 17442 padding: 6px 8px 8px 10px; 17443 border-radius: 8px; 17444 position: relative; 17445 box-shadow: 0 1px 0.5px rgba(0,0,0,0.13); 17446 } 17447 17448 /* Bubble from others (Admin/CS) */ 17449 .chat-message:not(.mine) .chat-message-bubble { 17450 background: #FFFFFF; 17451 border-radius: 0 8px 8px 8px; 17452 } 17453 17454 /* Bubble tail for others */ 17455 .chat-message:not(.mine) .chat-message-bubble::before { 17456 content: ''; 17457 position: absolute; 17458 top: 0; 17459 left: -8px; 17460 width: 0; 17461 height: 0; 17462 border-style: solid; 17463 border-width: 0 8px 13px 0; 17464 border-color: transparent #FFFFFF transparent transparent; 17465 } 17466 17467 /* Bubble from user (mine) */ 17468 .chat-message.mine .chat-message-bubble { 17469 background: #e4f5d0; /* Light blue for user messages */ 17470 border-radius: 8px 0 8px 8px; 17471 } 17472 17473 /* Bubble tail for mine */ 17474 .chat-message.mine .chat-message-bubble::before { 17475 content: ''; 17476 position: absolute; 17477 top: 0; 17478 right: -8px; 17479 width: 0; 17480 height: 0; 17481 border-style: solid; 17482 border-width: 0 0 13px 8px; 17483 border-color: transparent transparent transparent #eaf5d0; 17484 } 17485 17486 .message-sender { 17487 font-size: 12.5px; 17488 font-weight: 600; 17489 color: #F28C1A !important; 17490 margin-bottom: 4px; 17491 } 17492 17493 .chat-message.mine .message-sender { 17494 display: none; 17495 } 17496 17497 /* Message text */ 17498 .message-text { 17499 font-size: 14px; 17500 line-height: 19px; 17501 color: #ffffff !important; 17502 word-wrap: break-word; 17503 white-space: pre-wrap; 17504 margin-bottom: 4px; 17505 } 17506 17507 /* Message metadata (time + status) */ 17508 .message-meta { 17509 display: flex; 17510 align-items: center; 17511 justify-content: flex-end; 17512 gap: 4px; 17513 margin-top: 2px; 17514 } 17515 17516 .message-time { 17517 font-size: 11px; 17518 color: #728166 !important; 17519 line-height: 15px; 17520 } 17521 17522 /* Use multiple selectors to override global styles */ 17523 .chat-message-bubble .message-status, 17524 .message-meta .message-status, 17525 .chat-message .message-status { 17526 width: 16px !important; 17527 height: 16px !important; 17528 min-width: 16px !important; 17529 min-height: 16px !important; 17530 max-width: 16px !important; 17531 max-height: 16px !important; 17532 flex-shrink: 0; 17533 color: #728166 !important; 17534 fill: currentColor !important; 17535 } 17536 17537 .chat-message:not(.mine) .message-status { 17538 display: none; 17539 } 17540 17541 /* Loading dots animation */ 17542 @keyframes loadingDots { 17543 0%, 20% { content: '.'; } 17544 40% { content: '..'; } 17545 60%, 100% { content: '...'; } 17546 } 17547 17548 .message-status.loading::after { 17549 content: '...'; 17550 animation: loadingDots 1.5s infinite; 17551 font-size: 16px; 17552 line-height: 16px; 17553 } 17554 17555 .message-status.loading path { 17556 display: none; 17557 } 17558 17559 .message-status.failed { 17560 color: #ef4444 !important; 17561 cursor: pointer; 17562 } 17563 17564 .message-status.sent { 17565 color: #667781 !important; 17566 } 17567 17568 .chat-new-form { 17569 flex: 1; 17570 overflow-y: auto; 17571 padding: 16px; 17572 display: flex; 17573 flex-direction: column; 17574 } 17575 17576 .chat-new-form form { 17577 display: flex; 17578 flex-direction: column; 17579 gap: 16px; 17580 flex: 1; 17581 } 17582 17583 .chat-new-form .form-group { 17584 display: flex; 17585 flex-direction: column; 17586 gap: 6px; 17587 } 17588 17589 .chat-new-form label { 17590 font-size: 14px; 17591 font-weight: 500; 17592 color: #374151; 17593 } 17594 17595 .chat-new-form .form-control { 17596 width: 100%; 17597 padding: 10px 12px; 17598 border: 1px solid #d1d5db; 17599 border-radius: 8px; 17600 font-size: 14px; 17601 font-family: inherit; 17602 transition: border-color 0.2s; 17603 box-sizing: border-box; 17604 } 17605 17606 .chat-new-form .form-control:focus { 17607 outline: none; 17608 border-color: #F28C1A; 17609 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17610 } 17611 17612 .chat-new-form textarea.form-control { 17613 resize: vertical; 17614 min-height: 100px; 17615 } 17616 17617 .chat-new-form .btn { 17618 padding: 12px 20px; 17619 border: none; 17620 border-radius: 8px; 17621 font-size: 15px; 17622 font-weight: 500; 17623 cursor: pointer; 17624 transition: all 0.2s; 17625 text-align: center; 17626 } 17627 17628 .chat-new-form .btn-primary { 17629 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17630 color: white; 17631 } 17632 17633 .chat-new-form .btn-primary:hover { 17634 transform: translateY(-1px); 17635 box-shadow: 0 4px 8px rgba(242, 140, 26, 0.3); 17636 } 17637 17638 .chat-new-form .btn-primary:active { 17639 transform: scale(0.98); 17640 } 17641 17642 .chat-new-form .btn-block { 17643 width: 100%; 17644 margin-top: auto; 17645 } 17646 17647 .chat-message-form { 17648 padding: 12px; 17649 border-top: 1px solid #e5e7eb; 17650 display: flex; 17651 flex-wrap: wrap; 17652 gap: 8px; 17653 align-items: flex-end; 17654 flex-shrink: 0; 17655 background: white; 17656 } 17657 17658 .chat-product-card { 17659 flex: 1 1 100%; 17660 max-width: 100%; 17661 display: flex; 17662 gap: 12px; 17663 padding: 12px; 17664 background: #f3f4f6; 17665 border-radius: 8px; 17666 margin-bottom: 4px; 17667 border: 1px solid #e5e7eb; 17668 } 17669 17670 .chat-product-card img { 17671 width: 60px; 17672 height: 60px; 17673 object-fit: cover; 17674 border-radius: 6px; 17675 flex-shrink: 0; 17676 } 17677 17678 .chat-product-card-content { 17679 flex: 1; 17680 min-width: 0; 17681 } 17682 17683 .chat-product-card-name { 17684 font-weight: 500; 17685 font-size: 14px; 17686 color: #1f2937; 17687 margin-bottom: 4px; 17688 white-space: nowrap; 17689 overflow: hidden; 17690 text-overflow: ellipsis; 17691 } 17692 17693 .chat-product-card-variant { 17694 font-size: 12px; 17695 color: #74806b; 17696 margin-bottom: 4px; 17697 } 17698 17699 .chat-product-card-price { 17700 font-size: 13px; 17701 color: #8bea66; 17702 font-weight: 600; 17703 } 17704 17705 .chat-product-card-remove { 17706 align-self: flex-start; 17707 background: none; 17708 border: none; 17709 color: #9ca3af; 17710 cursor: pointer; 17711 padding: 4px; 17712 font-size: 18px; 17713 line-height: 1; 17714 transition: color 0.2s; 17715 } 17716 17717 .chat-product-card-remove:hover { 17718 color: #ef4444; 17719 } 17720 17721 /* Product card in messages (read-only display) */ 17722 .message-product-card { 17723 display: flex; 17724 gap: 10px; 17725 padding: 10px; 17726 background: rgba(255, 255, 255, 0.5); 17727 border-radius: 8px; 17728 margin-bottom: 6px; 17729 border: 1px solid rgba(0, 0, 0, 0.08); 17730 align-items: center; 17731 } 17732 17733 .chat-message.mine .message-product-card { 17734 background: rgba(255, 255, 255, 0.7); 17735 } 17736 17737 .message-product-card img { 17738 width: 50px; 17739 height: 50px; 17740 object-fit: cover; 17741 border-radius: 6px; 17742 flex-shrink: 0; 17743 } 17744 17745 .message-product-info { 17746 flex: 1; 17747 min-width: 0; 17748 } 17749 17750 .message-product-name { 17751 font-weight: 500; 17752 font-size: 13px; 17753 color: #33371f; 17754 margin-bottom: 2px; 17755 overflow: hidden; 17756 text-overflow: ellipsis; 17757 display: -webkit-box; 17758 -webkit-line-clamp: 2; 17759 -webkit-box-orient: vertical; 17760 line-height: 1.3; 17761 } 17762 17763 .message-product-variant { 17764 font-size: 11px; 17765 color: #78806b; 17766 margin-bottom: 2px; 17767 } 17768 17769 .message-product-price { 17770 font-size: 12px; 17771 color: #c7ea66; 17772 font-weight: 600; 17773 } 17774 17775 .message-product-link { 17776 flex-shrink: 0; 17777 width: 32px; 17778 height: 32px; 17779 display: flex; 17780 align-items: center; 17781 justify-content: center; 17782 background: rgba(102, 126, 234, 0.1); 17783 border-radius: 6px; 17784 color: #c7ea66; 17785 text-decoration: none; 17786 transition: all 0.2s; 17787 } 17788 17789 .message-product-link:hover { 17790 background: #b7ea66; 17791 color: white; 17792 } 17793 17794 .message-product-link svg { 17795 width: 16px; 17796 height: 16px; 17797 } 17798 17799 .chat-message-input { 17800 flex: 1; 17801 border: 1px solid #d1d5db; 17802 border-radius: 20px; 17803 padding: 10px 16px; 17804 resize: none; 17805 max-height: 100px; 17806 font-family: inherit; 17807 font-size: 14px; 17808 line-height: 1.5; 17809 box-sizing: border-box; 17810 overflow-y: hidden; 17811 } 17812 17813 .chat-message-input:focus { 17814 outline: none; 17815 border-color: #F28C1A; 17816 box-shadow: 0 0 0 3px rgba(242, 140, 26, 0.1); 17817 } 17818 17819 .chat-send-btn { 17820 width: 40px; 17821 height: 40px; 17822 border-radius: 50%; 17823 background: linear-gradient(135deg, #F28C1A 0%, #D97809 100%); 17824 border: none; 17825 color: white; 17826 cursor: pointer; 17827 flex-shrink: 0; 17828 display: flex; 17829 align-items: center; 17830 justify-content: center; 17831 transition: all 0.2s; 17832 } 17833 17834 .chat-send-btn:hover { 17835 transform: scale(1.05); 17836 box-shadow: 0 2px 8px rgba(242, 140, 26, 0.3); 17837 } 17838 17839 .chat-send-btn:active { 17840 transform: scale(0.95); 17841 } 17842 17843 .chat-loading { 17844 text-align: center; 17845 padding: 20px; 17846 color: #6b7280; 17847 } 17848 17849 /* Date Separator (WhatsApp-style) */ 17850 .date-separator { 17851 display: flex; 17852 align-items: center; 17853 gap: 12px; 17854 margin: 24px 0 16px; 17855 opacity: 0; 17856 animation: fadeInSeparator 0.3s ease forwards; 17857 } 17858 17859 @keyframes fadeInSeparator { 17860 from { 17861 opacity: 0; 17862 transform: translateY(-10px); 17863 } 17864 to { 17865 opacity: 1; 17866 transform: translateY(0); 17867 } 17868 } 17869 17870 .date-separator-line { 17871 flex: 1; 17872 height: 1px; 17873 background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1) 50%, transparent); 17874 } 17875 17876 .date-separator-text { 17877 flex-shrink: 0; 17878 padding: 4px 12px; 17879 background: #e9ecef; 17880 color: #6c757d; 17881 font-size: 11px; 17882 font-weight: 600; 17883 text-transform: uppercase; 17884 letter-spacing: 0.5px; 17885 border-radius: 12px; 17886 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 17887 } 17888 17889 /* Mobile responsive */ 17890 @media (max-width: 640px) { 17891 .chat-popup { 17892 position: fixed !important; /* Override absolute positioning */ 17893 bottom: 45px !important; /* Above bottom nav */ 17894 right: 0 !important; 17895 left: 0 !important; 17896 width: 100% !important; 17897 max-width: 100% !important; 17898 /* Use dvh (dynamic viewport height) for browser UI awareness */ 17899 height: calc(100dvh - 45px) !important; 17900 max-height: calc(100dvh - 45px) !important; 17901 /* Fallback for browsers that don't support dvh */ 17902 height: calc(var(--viewport-height, 100vh) - 45px) !important; 17903 max-height: calc(var(--viewport-height, 100vh) - 45px) !important; 17904 border-radius: 0 !important; 17905 z-index: 9999 !important; 17906 } 17907 17908 .chat-popup-header { 17909 border-radius: 0; 17910 } 17911
17912 .chat-messages-container, 17913 .chat-conversations-list { 17914 /* Adjust for mobile keyboard using dynamic viewport */ 17915 max-height: calc(100dvh - 190px); 17916 /* Fallback for browsers that don't support dvh */ 17917 max-height: calc(var(--viewport-height, 100vh) - 190px); 17918 } 17919 } 17920 </style> 17921 <!-- Chat Widget Button --> 17922 <div id="chat-widget" class="chat-widget" 17923 data-user-id="" 17924 data-is-logged-in="false" 17925 data-csrf-token="df5d776748b1ed8c8dd0d987bae99b1ee3dea6c6dcbe0eedb1bea766c56d569a"> 17926 <button id="chat-toggle-btn" class="chat-toggle-btn" aria-label="Toggle chat"> 17927 <svg class="chat-icon" width="28" height="28" viewBox="0 0 640 640" fill="currentColor"> 17928 <path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/> 17929 </svg> 17930 <span id="chat-unread-badge" class="chat-unread-badge" style="display: none;">0</span> 17931 </button> 17932 17933 <div id="chat-popup" class="chat-popup" style="display: none;"> 17934 <div class="chat-popup-header"> 17935 <div class="chat-header-title"> 17936 <h3>Bantuan</h3> 17937 <span id="chatOperatingHours" class="chat-operating-hours" style="display: none;"></span> 17938 </div> 17939 <div class="chat-header-actions"> 17940 <button id="chatNotifToggle" class="btn-notif-toggle" data-status="not-supported" title="Push notification" style="display: none;"> 17941 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 17942 <path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/> 17943 </svg> 17944 </button> 17945 <button id="chat-close-btn" class="chat-close-btn" aria-label="Close chat">Ã</button> 17946 </div> 17947 </div> 17948 17949 <!-- Notification Info Bar (conditional) --> 17950 <div class="chat-notif-info-bar" id="chatNotifInfoBar" style="display:none;"> 17951 <svg width="14" height="14" viewBox="0 0 24 24" fill="#ffc107"> 17952 <path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/> 17953 </svg> 17954 <span class="small">Aktifkan notifikasi untuk pemberitahuan balasan</span> 17955 <button class="btn-dismiss" onclick="ChatNotification.dismissInfo()">â</button> 17956 </div> 17957 17958 <div class="chat-popup-body"> 17959 <!-- Conversations List View --> 17960 <div id="chat-list-view" class="chat-view"> 17961 <div id="chat-conversations-list" class="chat-conversations-list"> 17962 <div class="chat-loading">Memuat...</div> 17963 <!-- Conversation Item Structure (rendered by JS): 17964 <div class="chat-conversation-item"> 17965 <div class="conversation-avatar"> 17966 <svg>...</svg> 17967 </div> 17968 <div class="conversation-content"> 17969 <div class="conversation-header">
17970 <span class="conversation-name">Admin Support</span> 17971 <span class="conversation-time">10:30</span> 17972 </div> 17973 <div class="conversation-preview"> 17974 <p class="conversation-message">Terima kasih telah menghubungi kami...</p> 17975 <span class="conversation-unread">2</span> 17976 </div> 17977 </div> 17978 </div> 17979 --> 17980 </div> 17981 <button id="chat-new-btn" class="chat-new-btn"> 17982 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 17983 <line x1="12" y1="5" x2="12" y2="19"></line> 17984 <line x1="5" y1="12" x2="19" y2="12"></line> 17985 </svg> 17986 Percakapan Baru 17987 </button> 17988 </div> 17989 17990 <!-- Chat Messages View --> 17991 <div id="chat-messages-view" class="chat-view" style="display: none;"> 17992 <div id="chat-messages-container" class="chat-messages-container"> 17993 <div class="chat-loading">Memuat pesan...</div> 17994 <!-- Message Structure (rendered by JS): 17995 <div class="chat-message"> (or .chat-message.mine) 17996 <div class="chat-message-bubble"> 17997 <div class="message-sender">Admin Support</div> (only for non-mine) 17998 <div class="message-text">Halo, ada yang bisa dibantu?</div> 17999 <div class="message-meta"> 18000 <span class="message-time">10:30</span> 18001 <svg class="message-status">...</svg> (only for mine) 18002 </div> 18003 </div> 18004 </div> 18005 --> 18006 </div> 18007 18008 <form id="chat-message-form" class="chat-message-form"> 18009 <input type="hidden" id="chat-current-conversation-id" value=""> 18010 <textarea 18011 id="chat-message-input" 18012 class="chat-message-input" 18013 placeholder="Ketik pesan..." 18014 rows="1" 18015 maxlength="5000" 18016 ></textarea> 18017 <button type="submit" class="chat-send-btn" aria-label="Send message"> 18018 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18019 <line x1="22" y1="2" x2="11" y2="13"></line> 18020 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon> 18021 </svg> 18022 </button> 18023 </form> 18024 </div> 18025 18026 <!-- New Chat View --> 18027 <div id="chat-new-view" class="chat-view" style="display: none;"> 18028 <div class="chat-messages-header"> 18029 <button id="chat-new-back-btn" class="chat-back-btn" aria-label="Back to list"> 18030 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18031 <path d="M19 12H5M12 19l-7-7 7-7"/> 18032 </svg> 18033 </button> 18034 <div class="chat-conversation-title">Percakapan Baru</div> 18035 </div> 18036 18037 <div class="chat-new-form"> 18038 <form id="chat-start-form"> 18039 <div class="form-group"> 18040 <label for="chat-new-subject">Subjek</label> 18041 <input 18042 type="text" 18043 id="chat-new-subject" 18044 class="form-control" 18045 placeholder="Bantuan Pelanggan" 18046 maxlength="255" 18047 > 18048 </div> 18049 <div class="form-group"> 18050 <label for="chat-new-message">Pesan</label> 18051 <textarea 18052 id="chat-new-message" 18053 class="form-control" 18054 rows="4" 18055 placeholder="Tulis pertanyaan Anda..." 18056 required 18057 maxlength="5000" 18058 ></textarea> 18059 </div> 18060 <button type="submit" class="btn btn-primary btn-block">Kirim</button> 18061 </form> 18062 </div> 18063 </div> 18064 </div> 18065 </div> 18066 </div> 18067 18068 <!-- Login Required Modal --> 18069 <div class="modal-overlay" id="chatLoginModal"> 18070 <div class="modal-dialog"> 18071 <div class="modal-header"> 18072 <h3 class="modal-title"> 18073 <div class="modal-icon info"> 18074 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18075 <path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"/> 18076 </svg> 18077 </div> 18078 Login Diperlukan 18079 </h3> 18080 </div> 18081 <div class="modal-body"> 18082 <p class="modal-message">
18082Anda harus login terlebih dahulu untuk menggunakan fitur chat bantuan pelanggan.</p> 18083 <div class="modal-actions"> 18084 <button class="modal-btn modal-btn-secondary" id="chatLoginCancel">Batal</button> 18085 <button class="modal-btn modal-btn-primary" id="chatLoginConfirm">Login Sekarang</button> 18086 </div> 18087 </div> 18088 </div> 18089 </div> 18090
18090<script src="https://plazait.co.id/assets/js/chat-widget.js" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script>
18090<script data-inlined="chat-notification.js"> 18091/** 18092 * Customer Chat Push Notification Manager 18093 * Handles browser push notification subscription for customers 18094 * Version: 1.0.0 18095 * Date: 2026-01-19 18096 */ 18097 18098const ChatNotification = { 18099 // State 18100 status: 'not-supported', // not-supported, not-subscribed, subscribed, blocked 18101 subscription: null, 18102 vapidPublicKey: null, // Will be loaded from server 18103 18104 /** 18105 * Initialize notification manager 18106 */ 18107 async init() { 18108 // Check if user is logged in (only for logged-in users) 18109 if (!this.isUserLoggedIn()) { 18110 this.updateUI('not-supported'); 18111 return; 18112 } 18113 18114 // Check browser support 18115 if (!this.isSupported()) { 18116 this.updateUI('not-supported'); 18117 return; 18118 } 18119 18120 // Load VAPID key from server 18121 await this.loadVapidKey(); 18122 18123 if (!this.vapidPublicKey) { 18124 this.updateUI('not-supported'); 18125 return; 18126 } 18127 18128 // Register service worker if needed 18129 await this.registerServiceWorker(); 18130 18131 // Check current permission 18132 const permission = Notification.permission; 18133 18134 if (permission === 'denied') { 18135 this.updateUI('blocked'); 18136 } else if (permission === 'granted') { 18137 // Check if already subscribed 18138 const isSubscribed = await this.checkSubscriptionStatus(); 18139 this.updateUI(isSubscribed ? 'subscribed' : 'not-subscribed'); 18140 } else { 18141 // Default: not subscribed yet 18142 this.updateUI('not-subscribed'); 18143 this.showInfoBar(); 18144 } 18145 }, 18146 18147 /** 18148 * Check if user is logged in 18149 */ 18150 isUserLoggedIn() { 18151 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 18152 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 18153 }, 18154 18155 /** 18156 * Check if browser supports push notifications 18157 */ 18158 isSupported() { 18159 return ( 18160 'serviceWorker' in navigator && 18161 'PushManager' in window && 18162 'Notification' in window 18163 ); 18164 }, 18165 18166 /** 18167 * Load VAPID public key from server 18168 */ 18169 async loadVapidKey() { 18170 try { 18171 const response = await fetch('/api/chat/notifications/vapid_key', { 18172 method: 'GET', 18173 headers: { 18174 'Accept': 'application/json' 18175 } 18176 }); 18177 18178 if (!response.ok) { 18179 return false; 18180 } 18181 18182 const data = await response.json(); 18183 if (data.success && data.data?.public_key) { 18184 this.vapidPublicKey = data.data.public_key; 18185 return true; 18186 } 18187 18188 return false; 18189 18190 } catch (error) { 18191 console.error('[ChatNotification] Error loading VAPID key:', error); 18192 return false; 18193 } 18194 }, 18195 18196 /** 18197 * Register service worker 18198 */ 18199 async registerServiceWorker() { 18200 if (!('serviceWorker' in navigator)) return null; 18201 18202 try { 18203 const registration = await navigator.serviceWorker.register('/sw-push.js', { 18204 scope: '/' 18205 }); 18206 return registration; 18207 } catch (error) { 18208 return null; 18209 } 18210 }, 18211 18212 /** 18213 * Check subscription status from server 18214 */ 18215 async checkSubscriptionStatus() { 18216 try { 18217 const response = await fetch('/api/chat/notifications/status', { 18218 method: 'GET', 18219 credentials: 'include', 18220 headers: { 18221 'Accept': 'application/json' 18222 } 18223 }); 18224 18225 if (!response.ok) { 18226 return false; 18227 } 18228 18229 const data = await response.json(); 18230 return data.data?.subscribed || false; 18231 18232 } catch (error) { 18233 console.error('[ChatNotification] Error checking subscription:', error); 18234 return false;
18235 } 18236 }, 18237 18238 /** 18239 * Update UI based on status 18240 */ 18241 updateUI(status) { 18242 this.status = status; 18243 const btn = document.getElementById('chatNotifToggle'); 18244 18245 if (!btn) { 18246 // Retry after a short delay (button might not be in DOM yet) 18247 setTimeout(() => this.updateUI(status), 100); 18248 return; 18249 } 18250 18251 btn.setAttribute('data-status', status); 18252 18253 // Show/hide button based on support 18254 if (status === 'not-supported') { 18255 btn.style.display = 'none'; 18256 } else { 18257 btn.style.display = 'block'; 18258 } 18259 18260 // Update SVG icon (bell vs bell-slash) 18261 const svg = btn.querySelector('svg'); 18262 if (svg) { 18263 if (status === 'subscribed') { 18264 // Bell icon (active) 18265 svg.innerHTML = '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>'; 18266 } else { 18267 // Bell slash icon (inactive/blocked) 18268 svg.innerHTML = '<path d="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"/>'; 18269 } 18270 } 18271 18272 // Update tooltip 18273 const tooltips = { 18274 'not-subscribed': 'Klik untuk aktifkan notifikasi', 18275 'subscribed': 'Notifikasi aktif', 18276 'blocked': 'Notifikasi diblokir' 18277 }; 18278 18279 if (tooltips[status]) { 18280 btn.title = tooltips[status]; 18281 } 18282 18283 // Hide info bar if subscribed or blocked 18284 if (status === 'subscribed' || status === 'blocked') { 18285 this.hideInfoBar(); 18286 } 18287 }, 18288 18289 /** 18290 * Show info bar (if not dismissed) 18291 */ 18292 showInfoBar() { 18293 if (localStorage.getItem('chat_notif_info_dismissed')) { 18294 return; 18295 } 18296 18297 const infoBar = document.getElementById('chatNotifInfoBar'); 18298 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18299 18300 if (infoBar) { 18301 infoBar.style.display = 'flex'; 18302 18303 // Bind dismiss button here so it's always attached when bar is visible 18304 const btn = infoBar.querySelector('#chatNotifDismissBtn'); 18305 if (btn && !btn._dismissBound) { 18306 btn._dismissBound = true; 18307 btn.addEventListener('click', () => { 18308 infoBar.style.display = 'none'; 18309 localStorage.setItem('chat_notif_info_dismissed', '1'); 18310 if (popupBody) popupBody.classList.remove('has-info-bar'); 18311 }); 18312 } 18313 } 18314 18315 if (popupBody) { 18316 popupBody.classList.add('has-info-bar'); 18317 } 18318 }, 18319 18320 /** 18321 * Hide info bar 18322 */ 18323 hideInfoBar() { 18324 const infoBar = document.getElementById('chatNotifInfoBar'); 18325 const popupBody = document.getElementById('chat-popup')?.querySelector('.chat-popup-body'); 18326 18327 if (infoBar) { 18328 infoBar.style.display = 'none'; 18329 } 18330 18331 if (popupBody) { 18332 popupBody.classList.remove('has-info-bar'); 18333 } 18334 }, 18335 18336 /** 18337 * Dismiss info bar permanently 18338 */ 18339 dismissInfo() { 18340 this.hideInfoBar(); 18341 localStorage.setItem('chat_notif_info_dismissed', '1'); 18342 }, 18343 18344 /** 18345 * Handle bell icon click 18346 */ 18347 async handleToggleClick() { 18348 if (this.status === 'not-subscribed') { 18349 await this.subscribe(); 18350 } else if (this.status === 'subscribed') { 18351 await this.unsubscribe(); 18352 } else if (this.status === 'blocked') { 18353 this.showUnblockHelp(); 18354 } 18355 }, 18356 18357 /** 18358 * Subscribe to push notifications 18359 */ 18360 async subscribe() { 18361 try { 18362 // Request permission 18363 const permission = await Notification.requestPermission(); 18364 18365 if (permission === 'denied') { 18366 this.updateUI('blocked'); 18367 this.showToast('Notifikasi diblokir', 'error'); 18368 return; 18369 } 18370 18371 if (permission !== 'granted') { 18372 return; 18373 } 18374 18375 // Get service worker registration 18376 const registration = await navigator.serviceWorker.ready; 18377 18378 // Subscribe to push 18379 const subscription = await registration.pushManager.subscribe({ 18380 userVisibleOnly: true, 18381 applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) 18382 }); 18383 18384 // Send subscription to server 18385 const response = await fetch('/api/chat/notifications/subscribe', { 18386 method: 'POST', 18387 credentials: 'include', 18388 headers: { 18389 'Content-Type': 'application/json', 18390 'Accept': 'application/json' 18391 }, 18392 body: JSON.stringify(subscription.toJSON()) 18393 }); 18394 18395 if (!response.ok) { 18396 throw new Error('Failed to save subscription'); 18397 } 18398 18399 const result = await response.json(); 18400 18401 this.subscription = subscription; 18402 this.updateUI('subscribed'); 18403 this.hideInfoBar(); 18404 this.showToast('Notifikasi berhasil diaktifkan!', 'success'); 18405 18406 // Send test notification 18407 setTimeout(() => this.sendTestNotification(), 1000); 18408 18409 } catch (error) { 18410 console.error('[ChatNotification] Subscribe error:', error); 18411 this.showToast('Gagal mengaktifkan notifikasi', 'error'); 18412 } 18413 }, 18414 18415 /** 18416 * Unsubscribe from push notifications 18417 */ 18418 async unsubscribe() { 18419 try { 18420 // Confirm first 18421 if (!confirm('Nonaktifkan notifikasi chat?')) { 18422 return; 18423 } 18424 18425 // Get current subscription 18426 const registration = await navigator.serviceWorker.ready; 18427 const subscription = await registration.pushManager.getSubscription(); 18428 18429 if (subscription) { 18430 // Unsubscribe from browser 18431 await subscription.unsubscribe(); 18432 18433 // Tell server 18434 await fetch('/api/chat/notifications/unsubscribe', { 18435 method: 'POST', 18436 credentials: 'include', 18437 headers: { 18438 'Content-Type': 'application/json', 18439 'Accept': 'application/json' 18440 }, 18441 body: JSON.stringify(subscription.toJSON()) 18442 }); 18443 } 18444 18445 this.subscription = null; 18446 this.updateUI('not-subscribed'); 18447 this.showInfoBar(); 18448 this.showToast('Notifikasi dinonaktifkan', 'info'); 18449 18450 } catch (error) { 18451 console.error('[ChatNotification] Unsubscribe error:', error); 18452 this.showToast('Gagal menonaktifkan notifikasi', 'error'); 18453 } 18454 }, 18455 18456 /** 18457 * Send test notification 18458 */ 18459 async sendTestNotification() { 18460 try { 18461 const response = await fetch('/api/chat/notifications/send_test', { 18462 method: 'POST', 18463 credentials: 'include', 18464 headers: { 18465 'Accept': 'application/json' 18466 } 18467 }); 18468 18469 if (response.ok) { 18470 // Test notification sent successfully 18471 } 18472 } catch (error) { 18473 // Test notification failed silently 18474 } 18475 }, 18476 18477 /** 18478 * Show unblock help modal/message 18479 */ 18480 showUnblockHelp() { 18481 const message = ` 18482 Notifikasi diblokir oleh browser.\n\n 18483 Untuk mengaktifkan:\n 18484 1. Klik ikon gembok/info di address bar\n 18485 2. Cari "Notifications" atau "Notifikasi"\n 18486 3. Ubah menjadi "Allow" atau "Izinkan"\n 18487 4. Refresh halaman ini 18488 `.trim().replace(/\s+/g, ' '); 18489 18490 alert(message); 18491 }, 18492 18493 /** 18494 * Show toast notification 18495 */ 18496 showToast(message, type = 'info') { 18497 // Simple toast implementation 18498 const toast = document.createElement('div'); 18499 toast.className = `chat-toast chat-toast-${type}`; 18500 toast.textContent = message; 18501 toast.style.cssText = `
18502 position: fixed; 18503 bottom: 80px; 18504 right: 20px; 18505 background: ${type === 'success' ? '#4caf50' : type === 'error' ? '#f44336' : '#2196f3'}; 18506 color: white; 18507 padding: 12px 20px; 18508 border-radius: 4px; 18509 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 18510 z-index: 10000; 18511 animation: slideInUp 0.3s ease; 18512 `; 18513 18514 document.body.appendChild(toast); 18515 18516 setTimeout(() => { 18517 toast.style.animation = 'slideOutDown 0.3s ease'; 18518 setTimeout(() => toast.remove(), 300); 18519 }, 3000); 18520 }, 18521 18522 /** 18523 * Convert VAPID key to Uint8Array 18524 */ 18525 urlBase64ToUint8Array(base64String) { 18526 const padding = '='.repeat((4 - base64String.length % 4) % 4); 18527 const base64 = (base64String + padding) 18528 .replace(/\-/g, '+') 18529 .replace(/_/g, '/'); 18530 18531 const rawData = window.atob(base64); 18532 const outputArray = new Uint8Array(rawData.length); 18533 18534 for (let i = 0; i < rawData.length; ++i) { 18535 outputArray[i] = rawData.charCodeAt(i); 18536 } 18537 return outputArray; 18538 }, 18539 18540 /** 18541 * Check if push notification is currently active 18542 * @returns {Promise<boolean>} 18543 */ 18544 async isPushActive() { 18545 try { 18546 // Check browser subscription 18547 if (!('serviceWorker' in navigator)) return false; 18548 18549 const registration = await navigator.serviceWorker.getRegistration(); 18550 if (!registration) return false; 18551 18552 const subscription = await registration.pushManager.getSubscription(); 18553 if (!subscription) return false; 18554 18555 // Also check server status 18556 const serverStatus = await this.checkSubscriptionStatus(); 18557 18558 return serverStatus; 18559 } catch (error) { 18560 return false; 18561 } 18562 }, 18563 18564 /** 18565 * Show logout confirmation if push notification is active 18566 * Call this before logout action 18567 * @returns {Promise<boolean>} true if user confirms logout, false if cancelled 18568 */ 18569 async confirmLogoutWithPushNotification() { 18570 const isActive = await this.isPushActive(); 18571 18572 if (!isActive) { 18573 // No active push, proceed with logout 18574 return true; 18575 } 18576 18577 // Show modal confirmation using modal.css 18578 return new Promise((resolve) => { 18579 // Create modal HTML 18580 const modalHTML = ` 18581 <div class="modal-overlay" id="logoutConfirmModal"> 18582 <div class="modal-dialog"> 18583 <div class="modal-header"> 18584 <h3 class="modal-title"> 18585 <span class="modal-icon warning">â ï¸</span> 18586 Konfirmasi Logout 18587 </h3> 18588 </div> 18589 <div class="modal-body"> 18590 <p class="modal-message"> 18591 Anda memiliki <strong>notifikasi push yang aktif</strong>.<br><br> 18592 Setelah logout, Anda tidak akan menerima notifikasi chat lagi di perangkat ini.<br><br> 18593 Apakah Anda yakin ingin melanjutkan logout? 18594 </p> 18595 </div> 18596 <div class="modal-actions" style="padding: 0 20px 20px;"> 18597 <button class="modal-btn modal-btn-secondary" id="logoutCancelBtn"> 18598 Batal 18599 </button> 18600 <button class="modal-btn modal-btn-danger" id="logoutConfirmBtn"> 18601 Lanjutkan Logout 18602 </button> 18603 </div> 18604 </div> 18605 </div> 18606 `; 18607 18608 // Append to body 18609 const modalContainer = document.createElement('div'); 18610 modalContainer.innerHTML = modalHTML; 18611 document.body.appendChild(modalContainer.firstElementChild); 18612 18613 const modal = document.getElementById('logoutConfirmModal'); 18614 const confirmBtn = document.getElementById('logoutConfirmBtn'); 18615 const cancelBtn = document.getElementById('logoutCancelBtn'); 18616 18617 // Show modal with animation 18618 requestAnimationFrame(() => { 18619 modal.classList.add('show'); 18620 }); 18621 18622 // Handle confirm 18623 const handleConfirm = () => { 18624 cleanup(); 18625 resolve(true); 18626 }; 18627 18628 // Handle cancel 18629 const handleCancel = () => { 18630 cleanup(); 18631 resolve(false); 18632 }; 18633 18634 // Cleanup function 18635 const cleanup = () => { 18636 modal.classList.remove('show'); 18637 setTimeout(() => { 18638 modal.remove(); 18639 }, 200); 18640 confirmBtn.removeEventListener('click', handleConfirm); 18641 cancelBtn.removeEventListener('click', handleCancel); 18642 modal.removeEventListener('click', handleBackdropClick); 18643 }; 18644 18645 // Handle backdrop click 18646 const handleBackdropClick = (e) => { 18647 if (e.target === modal) { 18648 handleCancel(); 18649 } 18650 }; 18651 18652 // Add event listeners 18653 confirmBtn.addEventListener('click', handleConfirm); 18654 cancelBtn.addEventListener('click', handleCancel); 18655 modal.addEventListener('click', handleBackdropClick); 18656 }); 18657 } 18658}; 18659 18660// Add CSS animations 18661const chatNotifStyle = document.createElement('style'); 18662chatNotifStyle.textContent = ` 18663 @keyframes slideInUp { 18664 from { 18665 transform: translateY(100%); 18666 opacity: 0; 18667 } 18668 to { 18669 transform: translateY(0); 18670 opacity: 1; 18671 } 18672 } 18673 18674 @keyframes slideOutDown { 18675 from { 18676 transform: translateY(0); 18677 opacity: 1; 18678 } 18679 to { 18680 transform: translateY(100%); 18681 opacity: 0; 18682 } 18683 } 18684`; 18685document.head.appendChild(chatNotifStyle); 18686 18687// Auto-initialize when DOM is ready 18688if (document.readyState === 'loading') { 18689 document.addEventListener('DOMContentLoaded', () => ChatNotification.init()); 18690} else { 18691 ChatNotification.init(); 18692} 18693 18694// Expose to window for access from other scripts 18695window.ChatNotification = ChatNotification; 18696 18697</script>
18697 18698 <!-- Footer Collapse Toggle Script --> 18699
18699<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 18700 document.addEventListener('DOMContentLoaded', function() { 18701 const ftTop = document.getElementById('ftTop'); 18702 const expandBtn = document.getElementById('ftExpandBtn'); 18703 18704 if (ftTop && expandBtn) { 18705 const expandText = expandBtn.querySelector('.expand-text'); 18706 18707 expandBtn.addEventListener('click', function() { 18708 ftTop.classList.toggle('expanded'); 18709 const isExpanded = ftTop.classList.contains('expanded'); 18710 if (expandText) { 18711 expandText.textContent = isExpanded ? 'Sembunyikan' : 'Selengkapnya'; 18712 } 18713 }); 18714 } 18715 }); 18716 </script>
18716 18717 </footer> 18718 18719<!-- Compare Sidebar Overlay --> 18720<div class="compare-overlay" id="compareOverlay"></div> 18721 18722<!-- Floating Compare Badge (Sticky) --> 18723<button class="compare-floating-badge" id="compareFloatingBadge" aria-label="Lihat produk yang dibandingkan" style="display:none;"> 18724 <svg class="cfb-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> 18725 <path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4v3zm5.98-1v-3H22V8h-7.01V5L11 9l3.99 4z"/> 18726 </svg> 18727 <span class="cfb-text">Bandingkan</span> 18728 <span class="cfb-count" id="cfbCount">0</span> 18729</button> 18730 18731<!-- AI Comparison Modal --> 18732<div class="ai-compare-modal" id="aiCompareModal"> 18733 <div class="modal-backdrop"></div> 18734 <div class="modal-container"> 18735 <div class="modal-header"> 18736 <h3 class="modal-title"> 18737 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 18738 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/> 18739 </svg> 18740 Analisis AI: Perbandingan Produk 18741 </h3> 18742 <button class="modal-close" id="aiCompareModalClose" aria-label="Tutup"> 18743 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18744 <path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7a1 1 0 1 0-1.4 1.4L10.6 12l-4.9 4.9a1 1 0 0 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z"/> 18745 </svg> 18746 </button> 18747 </div> 18748 18749 <div class="modal-content"> 18750 <!-- Loading State --> 18751 <div class="ai-loading" id="aiLoading"> 18752 <div class="loading-animation"> 18753 <div class="typing-indicator"> 18754 <span></span> 18755 <span></span> 18756 <span></span> 18757 </div> 18758 <p>AI sedang menganalisis produk...</p> 18759 <small>Proses ini membutuhkan waktu 10-30 detik</small> 18760 </div> 18761 </div> 18762 18763 <!-- Error State --> 18764 <div class="ai-error" id="aiError" hidden> 18765 <div class="error-icon"> 18766 <svg width="48" height="48" viewBox="0 0 24 24" fill="#ef4444"> 18767 <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/> 18768 </svg> 18769 </div> 18770 <h4>Analisis AI Gagal</h4> 18771 <p id="aiErrorMessage">Terjadi kesalahan saat memproses analisis.</p> 18772 <button class="btn btn-outline-primary" id="aiRetry">Coba Lagi</button> 18773 </div> 18774 18775 <!-- Analysis Results --> 18776 <div class="ai-results" id="aiResults" hidden> 18777 <!-- Summary Section --> 18778 <div class="ai-summary-card"> 18779 <h4> 18780 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18781 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,16.5L6.5,12L7.91,10.59L11,13.67L16.59,8.09L18,9.5L11,16.5Z"/> 18782 </svg> 18783 Ringkasan Analisis 18784 </h4> 18785 <p id="aiSummary" class="summary-text"></p> 18786 <div class="winner-badge" id="winnerBadge"> 18787 <strong>Rekomendasi Terbaik:</strong> <span id="winnerProduct"></span> 18788 <small id="winnerReason"></small> 18789 </div> 18790 </div> 18791 18792 <!-- Comparison Table --> 18793 <div class="ai-comparison-table"> 18794 <h4> 18795 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18796 <path d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"/> 18797 </svg> 18798 Perbandingan Detail 18799 </h4> 18800 <div id="comparisonTable" class="comparison-grid"></div> 18801 </div> 18802 18803 <!-- Detailed Analysis --> 18804 <div class="ai-detailed-analysis"> 18805 <h4> 18806 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18807 <path d="M14,2A8,8 0 0,1 22,10A8,8 0 0,1 14,18A8,8 0 0,1 6,10A8,8 0 0,1 14,2M14,4A6,6 0 0,0 8,10A6,6 0 0,0 14,16A6,6 0 0,0 20,10A6,6 0 0,0 14,4M14,6A4,4 0 0,1 18,10A4,4 0 0,1 14,14A4,4 0 0,1 10,10A4,4 0 0,1 14,6M14,8A2,2 0 0,0 12,10A2,2 0 0,0 14,12A2,2 0 0,0 16,10A2,2 0 0,0 14,8Z"/> 18808 </svg> 18809 Analisis Mendalam 18810 </h4> 18811 <div id="detailedAnalysis" class="products-analysis"></div> 18812 </div> 18813 18814 <!-- Recommendations --> 18815 <div class="ai-recommendations"> 18816 <h4> 18817 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18818 <path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/> 18819 </svg> 18820 Rekomendasi Berdasarkan Kebutuhan 18821 </h4> 18822 <div class="recommendations-grid"> 18823 <div class="recommendation-card"> 18824 <h5>ð° Budget Conscious</h5> 18825 <p id="budgetRecommendation"></p> 18826 </div> 18827 <div class="recommendation-card"> 18828 <h5>â¡ Performance Focused</h5> 18829 <p id="performanceRecommendation"></p> 18830 </div> 18831 <div class="recommendation-card"> 18832 <h5>âï¸ Balanced Choice</h5> 18833 <p id="balancedRecommendation"></p> 18834 </div> 18835 </div> 18836 </div> 18837 18838 <!-- Buying Tips --> 18839 <div class="ai-tips"> 18840 <h4> 18841 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18842 <path d="M11,18H13V16H11V18M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6
18842M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3Z"/> 18843 </svg> 18844 Tips Pembelian 18845 </h4> 18846 <ul id="buyingTips" class="tips-list"></ul> 18847 </div> 18848 </div> 18849 </div> 18850 </div> 18851</div> 18852 18853<!-- Cart Login Modal --> 18854<div class="modal-overlay" id="cartLoginModal"> 18855 <div class="modal-dialog"> 18856 <div class="modal-header"> 18857 <h3 class="modal-title"> 18858 <div class="modal-icon info"> 18859 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 18860 <path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/> 18861 </svg> 18862 </div> 18863 Login untuk Melanjutkan 18864 </h3> 18865 </div> 18866 <div class="modal-body"> 18867 <p class="modal-message" style="margin-bottom: 16px;">Silakan login atau daftar untuk mengakses keranjang belanja dan menyelesaikan pesanan Anda.</p> 18868 <div style="background: #f8fafc; border-radius: 8px; padding: 12px; margin-bottom: 20px;"> 18869 <p style="font-size: 14px; color: #646947; margin: 0; line-height: 1.6;"> 18870 <strong style="color: #2a2a0f;">Keuntungan dengan akun:</strong><br> 18871 â Simpan keranjang belanja Anda<br> 18872 â Lacak status pesanan secara real-time<br> 18873 â Checkout lebih cepat dengan data tersimpan<br> 18874 â Dapatkan notifikasi promo & diskon eksklusif 18875 </p> 18876 </div> 18877 <div class="modal-actions"> 18878 <button class="modal-btn modal-btn-secondary" id="cartLoginCancel">Batal</button> 18879 <button class="modal-btn modal-btn-primary" id="cartLoginConfirm" style="background: #ff6b35; border-color: #ff6b35;">Login</button> 18880 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #f7931e; border-color: #f7931e;">Daftar</button> 18881 </div> 18882 </div> 18883 </div> 18884</div> 18885 18886
18887<script data-inlined="app.js"> 18888(function initLiveSearch(){ 18889 function setup() { 18890 const searchInput = document.getElementById('searchInput'); 18891 const suggest = document.getElementById('searchSuggest'); 18892 const searchBtn = document.getElementById('searchBtn'); 18893 const aiToggleBtn = document.getElementById('aiToggleBtn'); 18894 18895 if (!searchInput || !suggest || !aiToggleBtn) { 18896 console.log('[LiveSearch] Elemen belum ada, retry...'); 18897 return false; 18898 } 18899 18900 let debounceTimer = null; 18901 let lastQuery = ''; 18902 let currentIndex = -1; 18903 let currentAbortController = null; 18904 let isAIModeEnabled = false; // AI mode OFF by default 18905 let isAIProcessing = false; // Flag to prevent closing suggest box during AI processing 18906 18907 // Trending cache 18908 let trendingCache = null; 18909 let trendingLoading = false; 18910 18911 // Trending products cache 18912 let trendingProductsCache = null; 18913 let trendingProductsLoading = false; 18914 18915 const MIN_LEN = 2; 18916 const DEBOUNCE = 300; 18917 const AI_TIMEOUT = 15000; // 15 seconds timeout for AI endpoint 18918 18919 // Load AI mode preference from localStorage 18920 const savedAIMode = localStorage.getItem('plazait_ai_search_mode'); 18921 if (savedAIMode === 'enabled') { 18922 isAIModeEnabled = false; 18923 aiToggleBtn.classList.remove('active'); 18924 } 18925 18926 // Update placeholder based on AI mode 18927 function updatePlaceholder() { 18928 if (isAIModeEnabled) { 18929 searchInput.placeholder = 'ð¤ Coba: "laptop HP core i3 untuk kerja" atau "tinta epson l3210"'; 18930 } else { 18931 // Static placeholder for typing animation 18932 searchInput.placeholder = ''; 18933 } 18934 } 18935 18936 // Typing animation for placeholder 18937 let typingTimeout = null; 18938 let currentProductIndex = 0; 18939 let currentCharIndex = 0; 18940 let isTyping = false; 18941 let isDeleting = false; 18942 18943 const popularProducts = [ 18944 'Oppo A6', 18945 'Asus Ryzen 3', 18946 'Starlink V4', 18947 'HP Victus', 18948 'Lenovo RTX 5060', 18949 'ROG Strix', 18950 'iPhone 13', 18951 'Xpad 20', 18952 'Xiaomi 14', 18953 'Epson L3210', 18954 'Realme C85', 18955 'Brother T720', 18956 'Mouse Logitech' 18957 ]; 18958 18959 function typeWriter() { 18960 if (isAIModeEnabled || document.activeElement === searchInput) { 18961 return; // Skip typing if AI mode is on or input is focused 18962 } 18963 18964 const currentProduct = popularProducts[currentProductIndex]; 18965 18966 if (!isDeleting) { 18967 // Typing forward 18968 if (currentCharIndex < currentProduct.length) { 18969 searchInput.placeholder = currentProduct.substring(0, currentCharIndex + 1); 18970 currentCharIndex++; 18971 typingTimeout = setTimeout(typeWriter, 100); // Typing speed 18972 } else { 18973 // Finished typing, wait before deleting 18974 typingTimeout = setTimeout(() => { 18975 isDeleting = true; 18976 typeWriter(); 18977 }, 2000); // Pause before deleting 18978 } 18979 } else { 18980 // Deleting backward 18981 if (currentCharIndex > 0) { 18982 searchInput.placeholder = currentProduct.substring(0, currentCharIndex - 1); 18983 currentCharIndex--; 18984 typingTimeout = setTimeout(typeWriter, 50); // Deleting speed (faster) 18985 } else { 18986 // Finished deleting, move to next product 18987 isDeleting = false; 18988 currentProductIndex = (currentProductIndex + 1) % popularProducts.length; 18989 typingTimeout = setTimeout(typeWriter, 500); // Pause before typing next 18990 } 18991 } 18992 } 18993 18994 function startTypingAnimation() { 18995 if (!isAIModeEnabled && !typingTimeout) { 18996 currentCharIndex = 0; 18997 isDeleting = false; 18998 typeWriter(); 18999 } 19000 } 19001 19002 function stopTypingAnimation() { 19003 if (typingTimeout) { 19004 clearTimeout(typingTimeout); 19005 typingTimeout = null; 19006 } 19007 } 19008 19009 // Set initial placeholder 19010 updatePlaceholder(); 19011 19012 // Start typing animation for normal mode 19013 if (!isAIModeEnabled) { 19014 startTypingAnimation(); 19015 } 19016 19017 function esc(str){ 19018 if (str === null || str === undefined) return ''; 19019 return String(str) 19020 .replace(/&/g,'&') 19021 .replace(/</g,'<') 19022 .replace(/>/g,'>') 19023 .replace(/"/g,'"') 19024 .replace(/'/g,'''); 19025 } 19026 19027 function clearSuggest(){ 19028 suggest.innerHTML = ''; 19029 suggest.hidden = true; 19030 suggest.classList.remove('open', 'trending-mode'); 19031 currentIndex = -1; 19032 } 19033 19034 // ==================== TRENDING FUNCTIONS ==================== 19035 19036 async function fetchTrending() { 19037 if (trendingCache) return trendingCache; 19038 if (trendingLoading) return null; 19039 19040 trendingLoading = true; 19041
19042 try { 19043 const res = await fetch('/api/trending_searches.php?timeframe=7d&limit=8'); 19044 const data = await res.json(); 19045 19046 if (data.ok && Array.isArray(data.data)) { 19047 trendingCache = data.data; 19048 console.log('[Trending] Loaded', trendingCache.length, 'items'); 19049 return trendingCache; 19050 } 19051 } catch (err) { 19052 console.error('[Trending] Fetch error:', err); 19053 } finally { 19054 trendingLoading = false; 19055 } 19056 19057 return null; 19058 } 19059 19060 async function fetchTrendingProducts() { 19061 if (trendingProductsCache) return trendingProductsCache; 19062 if (trendingProductsLoading) return null; 19063 19064 trendingProductsLoading = true; 19065 19066 try { 19067 const res = await fetch('/api/trending_products.php?timeframe=7d&limit=6'); 19068 const data = await res.json(); 19069 19070 if (data.ok && Array.isArray(data.data)) { 19071 trendingProductsCache = data.data; 19072 console.log('[Trending Products] Loaded', trendingProductsCache.length, 'items'); 19073 return trendingProductsCache; 19074 } 19075 } catch (err) { 19076 console.error('[Trending Products] Fetch error:', err); 19077 } finally { 19078 trendingProductsLoading = false; 19079 } 19080 19081 return null; 19082 } 19083 19084 function renderTrendingSuggestions(trends, products) { 19085 if ((!trends || trends.length === 0) && (!products || products.length === 0)) { 19086 clearSuggest(); 19087 return; 19088 } 19089 19090 let html = `<div class="trending-container">`; 19091 19092 // Left column: Trending searches 19093 if (trends && trends.length > 0) { 19094 html += ` 19095 <div class="trending-column trending-searches"> 19096 <div class="trending-header"> 19097 <span class="trending-icon">ð¥</span> 19098 <span class="trending-title">Pencarian Trending</span> 19099 </div> 19100 `; 19101 19102 html += trends.map((t, idx) => { 19103 const categoryHint = t.category_hint ? `<span class="trending-category">${esc(t.category_hint)}</span>` : ''; 19104 return ` 19105 <div class="trending-item" data-query="${esc(t.query)}" data-index="${idx}"> 19106 <span class="trending-rank"> 19107 <img src="/assets/img/uptrend.image" alt="trending" width="16" height="16" style="vertical-align: middle;"> 19108 </span> 19109 <span class="trending-query">${esc(t.query)}</span> 19110 ${categoryHint} 19111 </div> 19112 `; 19113 }).join(''); 19114 19115 html += `</div>`; 19116 } 19117 19118 // Right column: Trending products 19119 if (products && products.length > 0) { 19120 html += ` 19121 <div class="trending-column trending-products"> 19122 <div class="trending-header"> 19123 <span class="trending-icon">â</span> 19124 <span class="trending-title">Produk Populer</span> 19125 </div> 19126 `; 19127 19128 html += products.map((p, idx) => { 19129 return ` 19130 <div class="trending-product-item" data-product-id="${esc(p.id)}" data-slug="${esc(p.slug)}" data-query="${esc(p.top_query || '')}" data-index="${idx}"> 19131 <img src="${esc(p.image)}" alt="${esc(p.name)}" class="trending-product-thumb"> 19132 <div class="trending-product-info"> 19133 <div class="trending-product-name">${esc(p.name)}</div> 19134 <div class="trending-product-price">${esc(p.price_label)}</div> 19135 </div> 19136 </div> 19137 `; 19138 }).join(''); 19139 19140 html += `</div>`; 19141 } 19142 19143 html += `</div>`; 19144 19145 suggest.innerHTML = html; 19146 suggest.hidden = false; 19147 suggest.classList.add('open', 'trending-mode'); 19148 suggest.style.display = 'block'; 19149 19150 console.log('[Trending] Rendered', trends?.length || 0, 'searches and', products?.length || 0, 'products'); 19151 } 19152 19153 function trackSearchClick(query, productId) { 19154 fetch('/api/track_search_click.php', { 19155 method: 'POST', 19156 headers: { 'Content-Type': 'application/json' }, 19157 body: JSON.stringify({ 19158 query: query, 19159 product_id: productId,
19160 click_type: 'suggestion' 19161 }) 19162 }).catch(() => {}); // silent fail 19163 } 19164 19165 function trackTrendingClick(query) { 19166 fetch('/api/track_search_click.php', { 19167 method: 'POST', 19168 headers: { 'Content-Type': 'application/json' }, 19169 body: JSON.stringify({ 19170 query: query, 19171 click_type: 'trending' 19172 }) 19173 }).catch(() => {}); 19174 } 19175 19176 function trackTrendingProductClick(productId, query) { 19177 fetch('/api/track_search_click.php', { 19178 method: 'POST', 19179 headers: { 'Content-Type': 'application/json' }, 19180 body: JSON.stringify({ 19181 query: query || 'trending_product', 19182 product_id: productId, 19183 click_type: 'trending_product' 19184 }) 19185 }).catch(() => {}); 19186 } 19187 19188 function trackSearchSubmit(query) { 19189 fetch('/api/track_search_click.php', { 19190 method: 'POST', 19191 headers: { 'Content-Type': 'application/json' }, 19192 body: JSON.stringify({ 19193 query: query, 19194 click_type: 'submit' 19195 }) 19196 }).catch(() => {}); 19197 } 19198 19199 function logFinalSearch(query) { 19200 // Log the final search to analytics (not during typing) 19201 fetch('/api/search_products.php?q=' + encodeURIComponent(query) + '&log=1', { 19202 headers: { 'Accept': 'application/json' } 19203 }).catch(() => {}); 19204 } 19205 19206 // ==================== END TRENDING FUNCTIONS ==================== 19207 19208 function clearSuggest(){ 19209 suggest.innerHTML = ''; 19210 suggest.hidden = true; 19211 suggest.classList.remove('open', 'trending-mode'); 19212 currentIndex = -1; 19213 } 19214 19215 // AI Toggle Handler 19216 aiToggleBtn.addEventListener('click', (e) => { 19217 e.preventDefault(); 19218 e.stopPropagation(); 19219 19220 isAIModeEnabled = !isAIModeEnabled; 19221 19222 if (isAIModeEnabled) { 19223 aiToggleBtn.classList.add('active'); 19224 localStorage.setItem('plazait_ai_search_mode', 'enabled'); 19225 updatePlaceholder(); // Update placeholder text 19226 stopTypingAnimation(); // Stop typing animation in AI mode 19227 19228 // Show AI prompt if there's a query 19229 const query = searchInput.value.trim(); 19230 if (query.length >= MIN_LEN) { 19231 showAISearchPrompt(query); 19232 } else { 19233 showAIActivationMessage(); 19234 } 19235 } else { 19236 aiToggleBtn.classList.remove('active'); 19237 localStorage.setItem('plazait_ai_search_mode', 'disabled'); 19238 updatePlaceholder(); // Update placeholder text 19239 startTypingAnimation(); // Start typing animation in normal mode 19240 clearSuggest(); 19241 19242 // Switch to normal search if there's a query 19243 const query = searchInput.value.trim(); 19244 if (query.length >= MIN_LEN) { 19245 performSearch(query); 19246 } 19247 } 19248 }); 19249 19250 function showAIActivationMessage() { 19251 suggest.innerHTML = ` 19252 <div class="ai-progress-container"> 19253 <div class="ai-progress-header"> 19254 <div class="ai-progress-icon">â¨</div> 19255 <div class="ai-progress-text"> 19256 <div class="ai-progress-title">AI Search Mode Activated!</div> 19257 <div class="ai-progress-subtitle">Ketik query Anda untuk pencarian cerdas</div> 19258 </div> 19259 </div> 19260 </div> 19261 `; 19262 suggest.hidden = false; 19263 suggest.classList.add('open'); 19264 suggest.style.display = 'block'; 19265 } 19266 19267 function renderAIInsights(insights){ 19268 if (!insights) return ''; 19269 19270 // Generate AI summary via backend endpoint 19271 const summary = insights.ai_summary || generateAISummary(insights); 19272 19273 if (!summary) return ''; 19274 19275 return ` 19276 <div class="ai-result-banner"> 19277 <div class="ai-result-icon">â¨</div> 19278 <div class="ai-result-text"> 19279 <div class="ai-result-title">AI Assistant</div> 19280 <div class="ai-result-description">${summary}</div> 19281 </div> 19282 </div> 19283 `; 19284 } 19285 19286 function generateAISummary(insights) { 19287 // Fallback client-side summary generation if backend doesn't provide ai_summary 19288 const productName = insights.product_name || ''; 19289 const categoryPath = insights.category_path || ''; 19290 const priceRange = insights.price_range || {}; 19291 const intent = insights.intent || ''; 19292 19293 let summary = ''; 19294 19295 // Parse category for natural display 19296 let categoryDisplay = ''; 19297 if (categoryPath) { 19298 const categoryParts = categoryPath.split('/');
19299 categoryDisplay = categoryParts[categoryParts.length - 1] || categoryParts[categoryParts.length - 2] || categoryPath; 19300 } 19301 19302 // Intent-based opening 19303 if (intent === 'cek_stok') { 19304 summary = `Berikut ketersediaan stok untuk <strong>${esc(productName)}</strong>. `; 19305 } else if (intent === 'rekomendasi_produk') { 19306 summary = `Berikut rekomendasi produk <strong>${esc(categoryDisplay || productName)}</strong> yang kami pilihkan untuk Anda. `; 19307 } else { 19308 // Default: cari_harga or general 19309 if (productName && categoryDisplay) { 19310 summary = `Kami menemukan <strong>${esc(productName)}</strong> di kategori ${esc(categoryDisplay)}`; 19311 } else if (productName) { 19312 summary = `Kami menemukan produk <strong>${esc(productName)}</strong>`; 19313 } else if (categoryDisplay) { 19314 summary = `Kami menemukan produk ${esc(categoryDisplay)}`; 19315 } 19316 19317 // Add price context 19318 if (priceRange.min > 0 && priceRange.max > 0) { 19319 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19320 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19321 summary += ` dengan kisaran harga ${minPrice} - ${maxPrice}`; 19322 } else if (priceRange.min > 0) { 19323 const minPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.min); 19324 summary += ` dengan harga mulai ${minPrice}`; 19325 } else if (priceRange.max > 0) { 19326 const maxPrice = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(priceRange.max); 19327 summary += ` dengan harga maksimal ${maxPrice}`; 19328 } 19329 19330 summary += ' yang sesuai dengan kebutuhan Anda.'; 19331 } 19332 19333 return summary || esc(insights.explanation || ''); 19334 } 19335 19336 function showAIProgressSteps() { 19337 return ` 19338 <div class="ai-progress-container"> 19339 <div class="ai-progress-header"> 19340 <div class="ai-progress-icon">ð¤</div> 19341 <div class="ai-progress-text"> 19342 <div class="ai-progress-title">AI sedang menganalisis...</div> 19343 <div class="ai-progress-subtitle">Memproses query Anda dengan kecerdasan buatan</div> 19344 </div> 19345 </div> 19346 <div class="ai-progress-steps"> 19347 <div class="ai-progress-step active"> 19348 <div class="ai-step-icon">1</div> 19349 <div class="ai-step-text">Memahami maksud pencarian...</div> 19350 </div> 19351 <div class="ai-progress-step pending"> 19352 <div class="ai-step-icon">2</div> 19353 <div class="ai-step-text">Mengekstrak parameter pencarian...</div> 19354 </div> 19355 <div class="ai-progress-step pending"> 19356 <div class="ai-step-icon">3</div> 19357 <div class="ai-step-text">Mencari produk yang relevan...</div> 19358 </div> 19359 </div> 19360 </div> 19361 `; 19362 } 19363 19364 function updateAIProgressStep(step) { 19365 const steps = suggest.querySelectorAll('.ai-progress-step'); 19366 19367 steps.forEach((stepEl, idx) => { 19368 const icon = stepEl.querySelector('.ai-step-icon'); 19369 19370 if (idx < step) { 19371 stepEl.classList.remove('active', 'pending'); 19372 stepEl.classList.add('completed'); 19373 icon.textContent = 'â'; 19374 } else if (idx === step) { 19375 stepEl.classList.remove('pending', 'completed'); 19376 stepEl.classList.add('active'); 19377 } 19378 }); 19379 } 19380
19381 function renderSuggest(items, aiInsights){ 19382 if (!items.length){ 19383 clearSuggest(); 19384 return; 19385 } 19386 19387 let html = ''; 19388 19389 // Show AI insights if available 19390 if (aiInsights && aiInsights.explanation) { 19391 html += renderAIInsights(aiInsights); 19392 } 19393 19394 // Section header for products with hint 19395 html += ` 19396 <div class="suggest-section-header"> 19397 <div class="suggest-header-main"> 19398 <span class="suggest-header-icon">ð¦</span> 19399 <span class="suggest-header-text">Produk yang ditemukan (${items.length})</span> 19400 </div> 19401 <div class="suggest-header-hint">Tekan <kbd>Enter</kbd> untuk lihat semua hasil</div> 19402 </div> 19403 `; 19404 19405 // Render product items 19406 html += items.map((it, idx)=>{ 19407 // Normalisasi & fallback 19408 const id = it.id != null ? it.id : ''; 19409 const name = esc(it.name); 19410 const slug = esc(it.slug != null ? it.slug : it.id); 19411 const img = esc(it.image || '/assets/img/no-image.png'); 19412 const price = esc(it.price_label || ''); 19413 19414 const url = '/product/' + encodeURIComponent(slug); 19415 19416 return ` 19417 <div class="suggest-item" data-index="${idx}" data-url="${url}" data-id="${esc(id)}"> 19418 <img src="${img}" alt="${name}" class="suggest-thumb"> 19419 <div class="suggest-meta"> 19420 <div class="suggest-name">${name}</div> 19421 <div class="suggest-price">${price}</div> 19422 </div> 19423 </div> 19424 `; 19425 }).join(''); 19426 19427 // Footer hint for more results - only show if 10 items (means there might be more) 19428 if (items.length >= 10) { 19429 html += ` 19430 <div class="suggest-footer-hint"> 19431 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: -3px; margin-right: 6px;"> 19432 <path d="M9.29 6.71a.996.996 0 0 0 0 1.41L13.17 12l-3.88 3.88a.996.996 0 1 0 1.41 1.41l4.59-4.59a.996.996 0 0 0 0-1.41L10.7 6.7c-.38-.39-1.02-.39-1.41.01z"/> 19433 </svg> 19434 <span>Lihat hasil lengkap dengan menekan <kbd>Enter</kbd> atau <a href="#" class="suggest-footer-link" id="footerSearchLink">klik di sini</a></span> 19435 </div> 19436 `; 19437 } 19438 19439 suggest.innerHTML = html; 19440 suggest.hidden = false; 19441 suggest.classList.add('open'); 19442 suggest.style.display = 'block'; 19443 suggest.style.visibility = 'visible'; 19444 suggest.style.opacity = '1'; 19445 } 19446 19447 function fetchRemote(q, shouldLog = false){ 19448 // Cancel previous request if any 19449 if (currentAbortController) { 19450 currentAbortController.abort(); 19451 } 19452 currentAbortController = new AbortController(); 19453 19454 // Always use standard search for normal mode 19455 // Only add log=1 parameter when shouldLog is true (intentional search) 19456 const logParam = shouldLog ? '&log=1' : ''; 19457 const url = '/api/search_products.php?q=' + encodeURIComponent(q) + logParam; 19458 const fetchOptions = { 19459 headers: {'Accept':'application/json'}, 19460 signal: currentAbortController.signal 19461 }; 19462 19463 return fetch(url, fetchOptions) 19464 .then(r=>r.json()) 19465 .then(j=>{ 19466 if (j.ok && Array.isArray(j.data)) { 19467 return j; 19468 } 19469 return { ok: false, data: [] }; 19470 }) 19471 .catch(err=>{ 19472 if (err.name === 'AbortError') { 19473 return { ok: false, data: [] }; 19474 } 19475 return { ok:false, data:[] }; 19476 }); 19477 } 19478 19479 function showNoResults(q, aiInsights) { 19480 suggest.innerHTML = ` 19481 <div style="padding: 32px 24px; text-align: center; color: #64748b;"> 19482 <div style="font-size: 56px; margin-bottom: 16px; opacity: 0.6;">ð</div> 19483 <div style="font-size: 16px; font-weight: 600; color: #334155; margin-bottom: 8px;"> 19484 Tidak ada hasil untuk "${esc(q)}" 19485 </div> 19486 ${aiInsights && aiInsights.explanation ? ` 19487 <div style="font-size: 13px; margin: 16px auto; padding: 12px 16px; background: #f1f5f9; border-radius: 8px; max-width: 400px; color: #475569;"> 19488 <strong style="color: #667eea;">ð¤ AI memahami:</strong><br> 19489 ${esc(aiInsights.explanation)} 19490 </div> 19491 ` : ''} 19492 <div style="font-size: 13px; margin-top: 16px; opacity: 0.8;"> 19493 Coba kata kunci lain atau hubungi kami untuk bantuan 19494 </div> 19495 </div> 19496 `; 19497 suggest.hidden = false;
19498 suggest.classList.add('open'); 19499 suggest.style.display = 'block'; 19500 } 19501 19502 function showErrorMessage() { 19503 suggest.innerHTML = ` 19504 <div style="padding: 32px 24px; text-align: center; color: #dc2626;"> 19505 <div style="font-size: 48px; margin-bottom: 12px;">â ï¸</div> 19506 <div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;"> 19507 Pencarian tidak tersedia 19508 </div> 19509 <div style="font-size: 12px; opacity: 0.8;"> 19510 Silakan coba lagi dalam beberapa saat 19511 </div> 19512 </div> 19513 `; 19514 suggest.hidden = false; 19515 suggest.classList.add('open'); 19516 suggest.style.display = 'block'; 19517 } 19518 19519 function showAIErrorState(q) { 19520 suggest.innerHTML = ` 19521 <div class="ai-progress-container"> 19522 <div class="ai-progress-header"> 19523 <div class="ai-progress-icon" style="background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);">â¡</div> 19524 <div class="ai-progress-text"> 19525 <div class="ai-progress-title">AI sedang memproses...</div> 19526 <div class="ai-progress-subtitle">Menganalisis query: "${esc(q)}"</div> 19527 </div> 19528 </div> 19529 <div style="padding: 16px; margin-top: 12px; background: #fef3c7; border-radius: 8px; border-left: 3px solid #f59e0b;"> 19530 <div style="font-size: 13px; color: #92400e; line-height: 1.6;"> 19531 <strong style="display: block; margin-bottom: 4px;">ð¡ Sedang mencari produk terbaik untuk Anda</strong> 19532 AI kami sedang menganalisis database untuk menemukan hasil yang paling relevan. 19533 Mohon tunggu sebentar... 19534 </div> 19535 </div> 19536 <div style="padding: 12px 16px; margin-top: 12px; text-align: center;"> 19537 <button onclick="location.reload()" style="padding: 8px 16px; background: #fb923c; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;"> 19538 ð Refresh & Coba Lagi 19539 </button> 19540 </div> 19541 </div> 19542 `; 19543 suggest.hidden = false; 19544 suggest.classList.add('open'); 19545 suggest.style.display = 'block'; 19546 } 19547 19548 function triggerAISearch(q) { 19549 lastQuery = q; // Update lastQuery to prevent re-triggering 19550 isAIProcessing = true; // Set flag to prevent suggest box from closing 19551 19552 // Show AI progress UI - keep suggest box visible 19553 suggest.innerHTML = showAIProgressSteps(); 19554 suggest.hidden = false; 19555 suggest.classList.add('open'); 19556 suggest.style.display = 'block'; 19557 19558 setTimeout(() => updateAIProgressStep(1), 1000); 19559 setTimeout(() => updateAIProgressStep(2), 2500); 19560 19561 // Cancel previous request if any 19562 if (currentAbortController) { 19563 currentAbortController.abort(); 19564 } 19565 currentAbortController = new AbortController(); 19566 19567 const url = '/api/search_ai.php?q=' + encodeURIComponent(q); 19568 const fetchOptions = { 19569 headers: {'Accept':'application/json'}, 19570 signal: currentAbortController.signal 19571 }; 19572 19573 // Add timeout 19574 const timeoutId = setTimeout(() => { 19575 currentAbortController.abort(); 19576 19577 // Show fallback loading state 19578 suggest.innerHTML = ` 19579 <div class="ai-progress-container"> 19580 <div class="ai-progress-header"> 19581 <div class="ai-progress-icon">â³</div> 19582 <div class="ai-progress-text"> 19583 <div class="ai-progress-title">AI timeout - switching to standard search</div> 19584 <div class="ai-progress-subtitle">Mencari dengan metode tradisional...</div> 19585 </div> 19586 </div> 19587 </div> 19588 `; 19589 19590 // Fallback to traditional search 19591 fetch('/api/search_products.php?q=' + encodeURIComponent(q), { 19592 headers: { 'Accept': 'application/json' } 19593 }) 19594 .then(r => r.json()) 19595 .then(j => { 19596 isAIProcessing = false; // Reset flag 19597 if (j.ok && Array.isArray(j.data) && j.data.length) { 19598 renderSuggest(j.data, null); 19599 } else { 19600 showNoResults(q, null); 19601 } 19602 }) 19603 .catch(() => { 19604 isAIProcessing = false; // Reset flag 19605 showAIErrorState(q); 19606 }); 19607 }, AI_TIMEOUT); 19608 19609 fetch(url, fetchOptions) 19610 .then(r => r.json()) 19611 .then(j => { 19612 clearTimeout(timeoutId); 19613 isAIProcessing = false; // Reset flag when done 19614 19615 if (j.ok && Array.isArray(j.data)) { 19616 if (j.data.length > 0) { 19617 renderSuggest(j.data, j.ai_insights || null); 19618 } else { 19619 showNoResults(q, j.ai_insights || null); 19620 } 19621 } else { 19622 showAIErrorState(q); 19623 } 19624 }) 19625 .catch(err => { 19626 clearTimeout(timeoutId); 19627 isAIProcessing = false; // Reset flag on error 19628 if (err.name !== 'AbortError') { 19629 showAIErrorState(q); 19630 } 19631 }); 19632 } 19633 19634 function performSearch(q){ 19635 if (q.length < MIN_LEN){ 19636 clearSuggest(); 19637 return; 19638 } 19639 if (q === lastQuery) return; 19640 lastQuery = q; 19641 19642 // Normal mode only - AI mode uses triggerAISearch() 19643 fetchRemote(q).then(j=>{ 19644 if (j.ok && Array.isArray(j.data)){ 19645 if (j.data.length > 0) { 19646 renderSuggest(j.data, null); 19647 } else { 19648 showNoResults(q, null); 19649 } 19650 } else { 19651 clearSuggest(); 19652 } 19653 }).catch(() => { 19654 clearSuggest(); 19655 }); 19656 } 19657 19658 function debounce(){ 19659 const q = searchInput.value.trim(); 19660 clearTimeout(debounceTimer); 19661 19662 // If empty, show trending suggestions and products 19663 if (q.length === 0) { 19664 debounceTimer = setTimeout(async () => { 19665 const trends = trendingCache || await fetchTrending(); 19666 const products = trendingProductsCache || await fetchTrendingProducts(); 19667 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19668 renderTrendingSuggestions(trends, products); 19669 } else { 19670 clearSuggest(); 19671 } 19672 }, 100); 19673 return; 19674 } 19675 19676 // For AI mode, just show search prompt (no auto search) 19677 if (isAIModeEnabled && q.length >= MIN_LEN) { 19678 showAISearchPrompt(q); 19679 } else if (!isAIModeEnabled && q.length >= MIN_LEN) { 19680 // Normal mode: auto search with debounce 19681 debounceTimer = setTimeout(()=> performSearch(q), DEBOUNCE); 19682 } else { 19683 clearSuggest(); 19684 } 19685 } 19686 19687 function showAISearchPrompt(q) { 19688 suggest.innerHTML = ` 19689 <div class="ai-search-prompt"> 19690 <div class="ai-prompt-header"> 19691 <div class="ai-prompt-icon">ð¤</div> 19692 <div class="ai-prompt-text"> 19693 <div class="ai-prompt-title">Pencarian AI Siap!</div> 19694 <div class="ai-prompt-subtitle">Klik tombol di bawah untuk mencari dengan AI</div> 19695 </div> 19696 </div> 19697 <div class="ai-prompt-query"> 19698 <div class="ai-query-label">
19698Query Anda:</div> 19699 <div class="ai-query-text">"${esc(q)}"</div> 19700 </div> 19701 <button class="ai-search-trigger-btn" data-query="${esc(q)}"> 19702 <span class="ai-btn-icon">â¨</span> 19703 <span class="ai-btn-text">Cari dengan AI</span> 19704 </button> 19705 <div class="ai-prompt-footer"> 19706 AI akan menganalisis query Anda untuk hasil yang lebih relevan 19707 </div> 19708 </div> 19709 `; 19710 suggest.hidden = false; 19711 suggest.classList.add('open'); 19712 suggest.style.display = 'block'; 19713 } 19714 19715 function moveHighlight(dir){ 19716 if (suggest.hidden) return; 19717 const items = suggest.querySelectorAll('.suggest-item'); 19718 if (!items.length) return; 19719 currentIndex += dir; 19720 if (currentIndex < 0) currentIndex = items.length - 1; 19721 if (currentIndex >= items.length) currentIndex = 0; 19722 items.forEach(it=> it.classList.remove('active')); 19723 const active = items[currentIndex]; 19724 if (active){ 19725 active.classList.add('active'); 19726 active.scrollIntoView({block:'nearest'}); 19727 } 19728 } 19729 19730 function activate(){ 19731 const active = suggest.querySelector('.suggest-item.active'); 19732 if (!active) return; 19733 window.location.href = active.getAttribute('data-url'); 19734 } 19735 19736 searchInput.addEventListener('input', debounce); 19737 19738 // Stop typing animation when focused & load trending if empty 19739 searchInput.addEventListener('focus', async () => { 19740 stopTypingAnimation(); 19741 19742 const q = searchInput.value.trim(); 19743 19744 // If empty, show trending suggestions and products 19745 if (q.length === 0) { 19746 const trends = trendingCache || await fetchTrending(); 19747 const products = trendingProductsCache || await fetchTrendingProducts(); 19748 if ((trends && trends.length > 0) || (products && products.length > 0)) { 19749 renderTrendingSuggestions(trends, products); 19750 } 19751 return; 19752 } 19753 19754 // If AI mode and has query, show AI prompt 19755 if (isAIModeEnabled && q.length >= MIN_LEN && suggest.hidden) { 19756 showAISearchPrompt(q); 19757 } 19758 }); 19759 19760 // Resume typing animation when blurred (if suggest is closed) 19761 searchInput.addEventListener('blur', () => { 19762 // Add delay to allow click events to fire first 19763 setTimeout(() => { 19764 if (!isAIModeEnabled && suggest.hidden && searchInput.value.trim() === '') { 19765 startTypingAnimation(); 19766 } 19767 }, 200); 19768 }); 19769 19770 searchInput.addEventListener('keydown', e=>{ 19771 switch(e.key){ 19772 case 'ArrowDown': moveHighlight(1); e.preventDefault(); break; 19773 case 'ArrowUp': moveHighlight(-1); e.preventDefault(); break; 19774 case 'Enter': 19775 if (!suggest.hidden){ 19776 activate(); 19777 e.preventDefault(); 19778 } else { 19779 // Direct enter without suggestion - track search submit and log final search 19780 const q = searchInput.value.trim(); 19781 if (q.length >= MIN_LEN) { 19782 trackSearchSubmit(q); 19783 logFinalSearch(q); 19784 } 19785 } 19786 break; 19787 case 'Escape': clearSuggest(); break; 19788 } 19789 }); 19790 19791 suggest.addEventListener('click', e=>{ 19792 // Check if footer link clicked 19793 const footerLink = e.target.closest('#footerSearchLink'); 19794 if (footerLink) { 19795 e.preventDefault(); 19796 const q = searchInput.value.trim(); 19797 if (q.length >= MIN_LEN) { 19798 trackSearchSubmit(q); 19799 logFinalSearch(q); 19800 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19801 } 19802 return; 19803 } 19804 19805 // Check if AI search button clicked 19806 const aiBtn = e.target.closest('.ai-search-trigger-btn'); 19807 if (aiBtn) { 19808 const query = aiBtn.getAttribute('data-query'); 19809 if (query) { 19810 triggerAISearch(query); 19811 } 19812 return; 19813 } 19814 19815 // Handle trending item click 19816 const trendingItem = e.target.closest('.trending-item'); 19817 if (trendingItem) { 19818 const query = trendingItem.getAttribute('data-query'); 19819 if (query) { 19820 searchInput.value = query; 19821 trackTrendingClick(query); 19822 logFinalSearch(query); // Log final search when user clicks trending 19823 performSearch(query); 19824 } 19825 return; 19826 } 19827 19828 // Handle trending product click 19829 const trendingProduct = e.target.closest('.trending-product-item'); 19830 if (trendingProduct) { 19831 const productId = trendingProduct.getAttribute('data-product-id'); 19832 const slug = trendingProduct.getAttribute('data-slug'); 19833 const query = trendingProduct.getAttribute('data-query'); 19834 19835 if (productId && slug) {
19836 trackTrendingProductClick(productId, query); 19837 window.location.href = '/product/' + encodeURIComponent(slug); 19838 } 19839 return; 19840 } 19841 19842 // Handle product item click 19843 const it = e.target.closest('.suggest-item'); 19844 if (!it) return; 19845 19846 const productId = it.getAttribute('data-id'); 19847 trackSearchClick(lastQuery, productId); 19848 logFinalSearch(lastQuery); // Log final search when user clicks on suggestion 19849 19850 window.location.href = it.getAttribute('data-url'); 19851 }); 19852 19853 document.addEventListener('click', e=>{ 19854 // Don't close suggest box if AI is processing 19855 if (isAIProcessing) return; 19856 19857 if (e.target === searchInput || e.target === aiToggleBtn || 19858 suggest.contains(e.target) || aiToggleBtn.contains(e.target)) return; 19859 clearSuggest(); 19860 }); 19861 19862 if (searchBtn){ 19863 searchBtn.addEventListener('click', ()=>{ 19864 const q = searchInput.value.trim(); 19865 if (q.length >= MIN_LEN){ 19866 // Track search submit and log final search 19867 trackSearchSubmit(q); 19868 logFinalSearch(q); 19869 window.location.href = '/product/search/?q=' + encodeURIComponent(q); 19870 } 19871 }); 19872 } 19873 19874 // Pastikan style dasar muncul 19875 if (!suggest.classList.contains('wired')) { 19876 suggest.classList.add('wired'); 19877 suggest.style.position = 'absolute'; 19878 suggest.style.zIndex = '9999'; 19879 suggest.style.background = '#fff'; 19880 suggest.style.border = '1px solid #ccc'; 19881 suggest.style.boxShadow = '0 4px 12px rgba(0,0,0,0.12)'; 19882 suggest.style.maxHeight = '360px'; 19883 suggest.style.overflowY = 'auto'; 19884 } 19885 19886 return true; 19887 } 19888 19889 if (!setup()) { 19890 if (document.readyState === 'loading') { 19891 document.addEventListener('DOMContentLoaded', setup); 19892 } else { 19893 setTimeout(setup, 80); 19894 } 19895 } 19896})(); 19897</script>
vendor: 1 bytes, line 19897
19897
19898<script data-inlined="site-notice.js"> 19899/** 19900 * Site Notice Banner - Interactive Behavior 19901 * Handles dismiss functionality with localStorage persistence 19902 * Supports dynamic notice IDs for different campaigns 19903 */ 19904 19905(function() { 19906 'use strict'; 19907 19908 const STORAGE_KEY = 'plazait_site_notice_dismissed'; 19909 19910 const banner = document.getElementById('holidayNotice'); 19911 const closeBtn = document.getElementById('holidayNoticeClose'); 19912 19913 if (!banner || !closeBtn) return; 19914 19915 // Get notice ID from data attribute 19916 const noticeId = banner.dataset.noticeId; 19917 if (!noticeId) { 19918 console.warn('Notice ID not found. Banner will not persist dismiss state.'); 19919 } 19920 19921 /** 19922 * Check if banner was previously dismissed 19923 */ 19924 function wasDismissed() { 19925 if (!noticeId) return false; 19926 19927 try { 19928 const dismissed = localStorage.getItem(STORAGE_KEY); 19929 return dismissed === noticeId; 19930 } catch (e) { 19931 console.warn('localStorage not available:', e); 19932 return false; 19933 } 19934 } 19935 19936 /** 19937 * Mark banner as dismissed 19938 */ 19939 function markDismissed() { 19940 if (!noticeId) return; 19941 19942 try { 19943 localStorage.setItem(STORAGE_KEY, noticeId); 19944 } catch (e) { 19945 console.warn('Could not save to localStorage:', e); 19946 } 19947 } 19948 19949 /** 19950 * Hide banner with animation 19951 */ 19952 function hideBanner() { 19953 banner.classList.add('hidden'); 19954 document.body.classList.remove('has-holiday-notice'); 19955 19956 // Remove from DOM after animation 19957 setTimeout(() => { 19958 banner.remove(); 19959 }, 300); 19960 19961 markDismissed(); 19962 19963 // Analytics tracking (optional) 19964 if (window.gtag) { 19965 window.gtag('event', 'banner_dismiss', { 19966 'event_category': 'Site Notice', 19967 'event_label': noticeId || 'unknown', 19968 }); 19969 } 19970 } 19971 19972 /** 19973 * Initialize banner 19974 */ 19975 function init() { 19976 // Check if already dismissed 19977 if (wasDismissed()) { 19978 banner.remove(); 19979 return; 19980 } 19981 19982 // Add body class for layout adjustment 19983 document.body.classList.add('has-holiday-notice'); 19984 19985 // Close button click 19986 closeBtn.addEventListener('click', hideBanner); 19987 19988 // Keyboard support for close button 19989 closeBtn.addEventListener('keydown', (e) => { 19990 if (e.key === 'Enter' || e.key === ' ') { 19991 e.preventDefault(); 19992 hideBanner(); 19993 } 19994 }); 19995 19996 // Optional: Auto-hide after certain time (disabled by default) 19997 // setTimeout(hideBanner, 30000); // 30 seconds 19998 } 19999 20000 // Initialize when DOM is ready 20001 if (document.readyState === 'loading') { 20002 document.addEventListener('DOMContentLoaded', init); 20003 } else { 20004 init(); 20005 } 20006 20007})(); 20008 20009</script>
vendor: 1 bytes, line 20009
20009
20010<script data-inlined="guest-cart.js"> 20011/** 20012 * Guest Cart Manager - localStorage based cart for guest users 20013 * 20014 * Manages shopping cart items in browser localStorage for users who are not logged in. 20015 * Provides seamless transition to database when user logs in. 20016 */ 20017 20018class GuestCart { 20019 constructor() { 20020 this.storageKey = 'plazait_guest_cart'; 20021 this.apiBase = '/api/cart'; 20022 20023 // Initialize cart structure 20024 this.cart = this.loadFromStorage(); 20025 20026 // Bind methods 20027 this.add = this.add.bind(this); 20028 this.remove = this.remove.bind(this); 20029 this.update = this.update.bind(this); 20030 this.clear = this.clear.bind(this); 20031 this.getItems = this.getItems.bind(this); 20032 this.mergeToDatabase = this.mergeToDatabase.bind(this); 20033 } 20034 20035 /** 20036 * Load cart from localStorage 20037 */ 20038 loadFromStorage() { 20039 try { 20040 const stored = localStorage.getItem(this.storageKey); 20041 if (stored) { 20042 const parsed = JSON.parse(stored); 20043 if (parsed && typeof parsed === 'object' && parsed.items) { 20044 return parsed; 20045 } 20046 } 20047 } catch (error) { 20048 // Silent error handling 20049 } 20050 20051 return { 20052 items: {}, 20053 updated_at: Date.now(), 20054 version: '1.0' 20055 }; 20056 } 20057 20058 /** 20059 * Save cart to localStorage 20060 */ 20061 saveToStorage() { 20062 try { 20063 this.cart.updated_at = Date.now(); 20064 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 20065 20066 // Dispatch custom event for cart updates 20067 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 20068 detail: { 20069 itemsCount: this.getItemsCount(), 20070 linesCount: this.getLinesCount(), 20071 items: this.cart.items 20072 } 20073 })); 20074 20075 return true; 20076 } catch (error) { 20077 return false; 20078 } 20079 } 20080 20081 /** 20082 * Generate cart item key 20083 */ 20084 generateKey(productId, variantId = 0) { 20085 return `p${productId}v${variantId}`; 20086 } 20087 20088 /** 20089 * Add item to guest cart 20090 */ 20091 add(productId, qty = 1, variantId = 0, meta = {}) { 20092 const key = this.generateKey(productId, variantId); 20093 20094 // Validate inputs 20095 if (!productId || productId <= 0 || !qty || qty <= 0) { 20096 throw new Error('Invalid product ID or quantity'); 20097 } 20098 20099 qty = Math.max(1, Math.min(999, parseInt(qty))); 20100 20101 // Initialize item if not exists 20102 if (!this.cart.items[key]) { 20103 this.cart.items[key] = { 20104 pid: parseInt(productId), 20105 vid: parseInt(variantId) || 0, 20106 qty: 0, 20107 name: meta.name || '', 20108 sku: meta.sku || '', 20109 price: parseInt(meta.price) || 0, 20110 image_url: meta.image_url || '', 20111 product_url: meta.product_url || '/product/' + parseInt(productId), 20112 added_at: Date.now() 20113 }; 20114 } 20115 20116 // Update quantity 20117 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 20118 20119 // Update metadata if provided 20120 if (meta.name) this.cart.items[key].name = meta.name; 20121 if (meta.sku) this.cart.items[key].sku = meta.sku; 20122 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 20123 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 20124 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 20125 20126 this.saveToStorage(); 20127 20128 return { 20129 key, 20130 item: this.cart.items[key], 20131 itemsCount: this.getItemsCount(), 20132 linesCount: this.getLinesCount() 20133 }; 20134 } 20135 20136 /** 20137 * Remove item from guest cart 20138 */ 20139 remove(productId, variantId = 0) { 20140 const key = this.generateKey(productId, variantId); 20141 20142 if (this.cart.items[key]) { 20143 delete this.cart.items[key]; 20144 this.saveToStorage(); 20145 20146 return true; 20147 } 20148 20149 return false;
20150 } 20151 20152 /** 20153 * Update item quantity 20154 */ 20155 update(productId, qty, variantId = 0) { 20156 const key = this.generateKey(productId, variantId); 20157 20158 if (!this.cart.items[key]) { 20159 return false; 20160 } 20161 20162 qty = parseInt(qty); 20163 if (qty <= 0) { 20164 return this.remove(productId, variantId); 20165 } 20166 20167 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 20168 this.saveToStorage(); 20169 20170 return true; 20171 } 20172 20173 /** 20174 * Clear entire cart 20175 */ 20176 clear() { 20177 this.cart = { 20178 items: {}, 20179 updated_at: Date.now(), 20180 version: '1.0' 20181 }; 20182 20183 this.saveToStorage(); 20184 20185 return true; 20186 } 20187 20188 /** 20189 * Get all cart items 20190 */ 20191 getItems() { 20192 return this.cart.items; 20193 } 20194 20195 /** 20196 * Get cart items as array 20197 */ 20198 getItemsArray() { 20199 return Object.values(this.cart.items); 20200 } 20201 20202 /** 20203 * Get total items count (sum of quantities) 20204 */ 20205 getItemsCount() { 20206 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 20207 } 20208 20209 /** 20210 * Get lines count (number of different items) 20211 */ 20212 getLinesCount() { 20213 return Object.keys(this.cart.items).length; 20214 } 20215 20216 /** 20217 * Get cart summary 20218 */ 20219 getSummary() { 20220 const itemsCount = this.getItemsCount(); 20221 const linesCount = this.getLinesCount(); 20222 const totalValue = Object.values(this.cart.items).reduce( 20223 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 20224 ); 20225 20226 return { 20227 itemsCount, 20228 linesCount, 20229 totalValue, 20230 updatedAt: this.cart.updated_at 20231 }; 20232 } 20233 20234 /** 20235 * Check if cart has items 20236 */ 20237 isEmpty() { 20238 return this.getLinesCount() === 0; 20239 } 20240 20241 /** 20242 * Merge guest cart to database after user login 20243 */ 20244 async mergeToDatabase() { 20245 const items = this.cart.items; 20246 20247 if (Object.keys(items).length === 0) { 20248 return { ok: true, merged_count: 0, message: 'No items to merge' }; 20249 } 20250 20251 try { 20252 // Use new endpoint that handles localStorage format 20253 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 20254 method: 'POST', 20255 headers: { 20256 'Content-Type': 'application/json', 20257 }, 20258 credentials: 'same-origin', 20259 body: JSON.stringify({ 20260 items: items // Send localStorage format directly 20261 }) 20262 }); 20263 20264 const result = await response.json(); 20265 20266 if (result.ok) { 20267 // Clear localStorage cart after successful merge 20268 this.clear(); 20269 20270 // Update cart counter 20271 if (window.updateCartCounter) { 20272 try { 20273 await window.updateCartCounter(); 20274 } catch (e) { 20275 // Silent error handling 20276 } 20277 } 20278 20279 // Dispatch merge completion event 20280 window.dispatchEvent(new CustomEvent('guestCartMerged', { 20281 detail: result 20282 })); 20283 20284 return result; 20285 } else { 20286 throw new Error(result.error || 'Failed to merge cart'); 20287 } 20288 20289 } catch (error) { 20290 throw error; 20291 } 20292 } 20293 20294 /** 20295 * Export cart data (for debugging or backup) 20296 */ 20297 export() { 20298 return { 20299 ...this.cart, 20300 summary: this.getSummary() 20301 }; 20302 } 20303 20304 /** 20305 * Import cart data (for restore or testing) 20306 */ 20307 import(cartData) { 20308 if (cartData && typeof cartData === 'object' && cartData.items) { 20309 this.cart = { 20310 items: cartData.items, 20311 updated_at: cartData.updated_at || Date.now(), 20312 version: cartData.version || '1.0' 20313 }; 20314 20315 this.saveToStorage(); 20316 return true; 20317 } 20318 20319 return false;
20320 } 20321} 20322 20323// Create global instance 20324window.guestCart = new GuestCart(); 20325 20326// Auto-merge on page load if user is logged in 20327document.addEventListener('DOMContentLoaded', async function() { 20328 // Multiple ways to check if user is logged in 20329 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20330 document.body.classList.contains('logged-in') || 20331 window.userLoggedIn === true || 20332 document.body.dataset.userLoggedIn === 'true'; 20333 20334 if (isLoggedIn && !window.guestCart.isEmpty()) { 20335 // Small delay to ensure page is fully loaded 20336 setTimeout(async () => { 20337 try { 20338 const result = await window.guestCart.mergeToDatabase(); 20339 } catch (error) { 20340 // Silent error handling 20341 } 20342 }, 500); 20343 } 20344}); 20345 20346// Also listen for login events (if login happens via AJAX) 20347window.addEventListener('userLoggedIn', async function() { 20348 if (!window.guestCart.isEmpty()) { 20349 try { 20350 await window.guestCart.mergeToDatabase(); 20351 } catch (error) { 20352 // Silent error handling 20353 } 20354 } 20355}); 20356</script>
vendor: 1 bytes, line 20356
20356
20357<script data-inlined="cart.js"> 20358(function(){ 20359 function $(s,ctx=document){ return ctx.querySelector(s); } 20360 function esc(s){ return String(s||'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); } 20361 20362 // CSRF token helper 20363 function getCsrfToken() { 20364 const metaTag = document.querySelector('meta[name="csrf-token"]'); 20365 return metaTag ? metaTag.getAttribute('content') : ''; 20366 } 20367 20368 function guessPdMeta(){ 20369 const name = (window.PD_NAME !== undefined) ? String(window.PD_NAME) : ($('#pd-title')?.textContent || ''); 20370 const sku = (function(){ const el=$('#pd-sku'); return el ? el.textContent.trim() : (window.PD_SKU||''); })(); 20371 // SECURITY FIX: Remove price from client-side metadata (will be validated server-side) 20372 const imgEl = $('#pd-main-img'); 20373 const image_url = imgEl ? (imgEl.getAttribute('src') || '') : ''; 20374 return { name, sku, image_url }; // price removed for security 20375 } 20376 20377 async function addToCart(productId, qty=1, variantId=0, metaOpt){ 20378 const meta = Object.assign(guessPdMeta(), metaOpt||{}); 20379 const payload = { 20380 product_id: parseInt(productId||0,10) || 0, 20381 qty: parseInt(qty||1,10) || 1, 20382 variant_id: parseInt(variantId||0,10) || 0, 20383 meta 20384 }; 20385 20386 // Check if user is logged in 20387 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 20388 document.body.classList.contains('logged-in') || 20389 window.userLoggedIn === true || 20390 document.body.dataset.userLoggedIn === 'true'; 20391 20392 // Skip localStorage untuk logged user - mereka pakai database 20393 if (!isLoggedIn && window.guestCart) { 20394 try { 20395 window.guestCart.add(payload.product_id, payload.qty, payload.variant_id, meta); 20396 } catch (e) { 20397 console.warn('Failed to add to localStorage cart:', e); 20398 } 20399 } 20400 20401 const res = await fetch('/api/cart/add.php', { 20402 method: 'POST', 20403 headers: { 20404 'Content-Type':'application/json', 20405 'X-CSRF-Token': getCsrfToken() 20406 }, 20407 credentials: 'same-origin', 20408 body: JSON.stringify({ 20409 ...payload, 20410 csrf_token: getCsrfToken() 20411 }), 20412 }); 20413 const data = await res.json().catch(()=>({ ok:false, error:'Gagal menambahkan ke keranjang' })); 20414 if (!res.ok || !data.ok) { 20415 const msg = data && data.error ? data.error : 'Gagal menambahkan ke keranjang'; 20416 throw new Error(msg); 20417 } 20418 20419 // Update badge di navbar dengan data dari response 20420 try { 20421 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20422 if (el && data.cart && typeof data.cart.items_count === 'number') { 20423 el.textContent = String(data.cart.items_count); 20424 el.classList.toggle('is-empty', data.cart.items_count <= 0); 20425 } 20426 } catch(_){} 20427 20428 // Siarkan event global (dipakai cart-badge.js dan halaman lain) 20429 try { 20430 document.dispatchEvent(new CustomEvent('cart:updated', { detail: data.cart || {} })); 20431 } catch(_){} 20432 20433 // Update CartBadge dengan data dari response (lebih reliable) 20434 try { 20435 if (window.CartBadge && window.CartBadge.set) { 20436 window.CartBadge.set(data.cart.items_count||0); 20437 } 20438 } catch(_){} 20439 20440 // Tambahan: refresh cart counter setelah delay kecil untuk memastikan data tersinkronisasi 20441 setTimeout(() => { 20442 if (window.updateCartCounter) { 20443 window.updateCartCounter(); 20444 } 20445 }, 100); 20446 20447 return data; 20448 } 20449 20450 // Function to update cart counter from server 20451 async function updateCartCounter(){ 20452 try { 20453 const res = await fetch('/api/cart/get.php', { credentials:'same-origin' }); 20454 const data = await res.json().catch(()=>({ok:false})); 20455 if (data.ok && typeof data.items_count === 'number') { 20456 const el = document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 20457 if (el) { 20458 el.textContent = String(data.items_count); 20459 el.classList.toggle('is-empty', data.items_count <= 0); 20460 } 20461 // Dispatch event 20462 try { 20463 document.dispatchEvent(new CustomEvent('cart:updated', { detail: { items_count: data.items_count } })); 20464 } catch(_){} 20465 } 20466 } catch (err) { 20467 console.log('Could not update cart counter:', err); 20468 } 20469 } 20470 20471 // Function to manually merge guest cart (if needed) 20472 async function mergeGuestCart(){ 20473 try { 20474 const res = await fetch('/api/cart/merge-guest.php', { 20475 method: 'POST', 20476 headers: { 20477 'Content-Type': 'application/json', 20478 'X-CSRF-Token': getCsrfToken() 20479 }, 20480 credentials: 'same-origin', 20481 body: JSON.stringify({ csrf_token: getCsrfToken() }) 20482 }); 20483 const data = await res.json().catch(()=>({ok:false})); 20484 20485 if (data.ok && data.merged_items > 0) { 20486 console.log(`Guest cart merged: ${data.merged_items} items`); 20487 // Update cart counter after merge 20488 await updateCartCounter(); 20489 return data; 20490 } 20491 return data; 20492 } catch (err) { 20493 console.log('Could not merge guest cart:', err); 20494 return { ok: false, error: err.message }; 20495 } 20496 } 20497 20498 window.addToCart = addToCart; 20499 window.updateCartCounter = updateCartCounter; 20500 window.mergeGuestCart = mergeGuestCart; 20501 20502 // Optional: auto-bind untuk tombol apapun yang punya data-addtocart 20503 document.addEventListener('click', async (e)=>{ 20504 const btn = e.target.closest('[data-addtocart]'); 20505 if (!btn) return; 20506 e.preventDefault(); 20507 if (btn.disabled) return; 20508 20509 const pid = parseInt(btn.getAttribute('data-pid')||'0',10); 20510 const vid = parseInt(btn.getAttribute('data-vid')||'0',10);
20511 const qty = parseInt(btn.getAttribute('data-qty')||'1',10) || 1; 20512 20513 try { 20514 const data = await addToCart(pid, qty, vid); 20515 if (window.showToast) { 20516 const meta = guessPdMeta(); 20517 window.showToast({ 20518 type:'success', 20519 title:'Berhasil ditambahkan', 20520 message:`<strong>${esc(meta.name)}</strong>${meta.sku?` · <em>SKU: ${esc(meta.sku)}</em>`:''}<br>Qty: ${qty}`, 20521 duration: 3800, 20522 actions: [ 20523 { label:'Lihat Keranjang', onClick: ()=> { window.location.href='/keranjang'; } }, 20524 { label:'Lanjut Belanja', variant:'alt', onClick: ()=>{} } 20525 ] 20526 }); 20527 } 20528 } catch (err) { 20529 if (window.showToast) { 20530 window.showToast({ type:'warn', title:'Gagal', message: String(err?.message||'Gagal menambahkan'), duration: 4200 }); 20531 } else { 20532 console.error(err); 20533 } 20534 } 20535 }); 20536})(); 20537</script>
vendor: 1 bytes, line 20537
20537
20538<script data-inlined="mega-menu-tiles.js"> 20539// Tile Mega Menu â Enhanced 2025 (Stagger animations, product counts, loading states) 20540// Includes safe-area hover handling (no backdrop flicker) 20541 20542(function(){ 20543 const trigger = document.querySelector('[data-mm2="trigger"]'); 20544 const panel = document.querySelector('[data-mm2="panel"]'); 20545 const topView = document.querySelector('[data-mm2="top"]'); 20546 const detailView= document.querySelector('[data-mm2="detail"]'); 20547 const backdrop = document.querySelector('[data-mm2="backdrop"]'); 20548 if (!trigger || !panel || !topView || !detailView) return; 20549 20550 const apiUrl = '/api/categories_tree.php?top=16&child=18&grand=14'; 20551 const isDesktop = () => window.matchMedia('(min-width: 961px)').matches; 20552 20553 let data = []; 20554 let hoverTimer, closeTimer; 20555 let isLoading = false; 20556 20557 const esc = s => (s+'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); 20558 20559 // Mobile close button HTML 20560 const mobileCloseBtn = () => ` 20561 <button type="button" class="mm2-mobile-close" data-mm2="close" aria-label="Tutup menu"> 20562 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 20563 <line x1="18" y1="6" x2="6" y2="18"></line> 20564 <line x1="6" y1="6" x2="18" y2="18"></line> 20565 </svg> 20566 </button> 20567 `; 20568 20569 // Category-specific gradient colors (16 unique combinations) 20570 const categoryGradients = [ 20571 { from: '#e0e7ff', to: '#fff', border: '#c7d2fe', color: '#4f46e5' }, // Indigo 20572 { from: '#dbeafe', to: '#fff', border: '#bfdbfe', color: '#1e40af' }, // Blue 20573 { from: '#d1fae5', to: '#fff', border: '#a7f3d0', color: '#059669' }, // Green 20574 { from: '#fef3c7', to: '#fff', border: '#fde68a', color: '#d97706' }, // Amber 20575 { from: '#fce7f3', to: '#fff', border: '#fbcfe8', color: '#db2777' }, // Pink 20576 { from: '#e9d5ff', to: '#fff', border: '#d8b4fe', color: '#9333ea' }, // Purple 20577 { from: '#ffedd5', to: '#fff', border: '#fed7aa', color: '#ea580c' }, // Orange 20578 { from: '#ccfbf1', to: '#fff', border: '#99f6e4', color: '#0d9488' }, // Teal 20579 { from: '#fef9c3', to: '#fff', border: '#fef08a', color: '#ca8a04' }, // Yellow 20580 { from: '#ffe4e6', to: '#fff', border: '#fecdd3', color: '#e11d48' }, // Rose 20581 { from: '#e0f2fe', to: '#fff', border: '#bae6fd', color: '#0284c7' }, // Sky 20582 { from: '#f3e8ff', to: '#fff', border: '#e9d5ff', color: '#a855f7' }, // Violet 20583 { from: '#dcfce7', to: '#fff', border: '#bbf7d0', color: '#16a34a' }, // Emerald 20584 { from: '#fef2f2', to: '#fff', border: '#fecaca', color: '#dc2626' }, // Red 20585 { from: '#eff6ff', to: '#fff', border: '#dbeafe', color: '#2563eb' }, // Blue-600 20586 { from: '#f5f3ff', to: '#fff', border: '#ede9fe', color: '#7c3aed' }, // Violet-600 20587 ]; 20588 20589 const icon = () => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7h5l2 3h11v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><path d="M3 7V5a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v3"></path></svg>`; 20590 20591 // Recent (optional future use) 20592 const REC_KEY='mm2_recent'; 20593 const saveRecent=(path,label)=>{ try{ 20594 let list=JSON.parse(localStorage.getItem(REC_KEY)||'[]').filter(x=>x&&x.path!==path); 20595 list.unshift({path,label,t:Date.now()}); localStorage.setItem(REC_KEY, JSON.stringify(list.slice(0,8))); 20596 }catch{} }; 20597 20598 function isToSafeArea(e){ 20599 const t = e && e.relatedTarget ? e.relatedTarget : null; 20600 if (!t) return false; 20601 return (trigger.contains(t) || panel.contains(t) || (backdrop && backdrop.contains(t))); 20602 } 20603 20604 function open(){ 20605 document.body.classList.add('mm2-open'); 20606 showTop(); 20607 // Add entrance animation class 20608 setTimeout(() => panel.classList.add('mm2-animated'), 10); 20609 } 20610 function close(){ 20611 document.body.classList.remove('mm2-open'); 20612 panel.classList.remove('mm2-animated'); 20613 } 20614 function showTop(){ 20615 topView.classList.add('is-active'); 20616 detailView.classList.remove('is-active'); 20617 } 20618 20619 // Loading skeleton 20620 function showLoading() { 20621 const skeletons = Array.from({length: 12}, (_, i) => ` 20622 <div class="mm2-card mm2-skeleton" style="animation-delay: ${i * 0.02}s">
20623 <span class="mm2-ico skeleton-box"></span> 20624 <span style="flex: 1;"> 20625 <div class="skeleton-line" style="width: 60%; height: 16px; margin-bottom: 6px;"></div> 20626 <div class="skeleton-line" style="width: 40%; height: 12px;"></div> 20627 </span> 20628 </div> 20629 `).join(''); 20630 topView.innerHTML = `<div class="mm2-top"><div class="mm2-grid">${skeletons}</div></div>`; 20631 } 20632 20633 // Count total products in a category tree 20634 function countProducts(node) { 20635 let count = node.product_count || 0; 20636 if (Array.isArray(node.children)) { 20637 node.children.forEach(child => { 20638 count += countProducts(child); 20639 }); 20640 } 20641 return count; 20642 } 20643 20644 // Cache for fetched thumbnails to avoid duplicate requests 20645 const thumbnailCache = new Map(); 20646 20647 // Fetch product thumbnails for a category 20648 function fetchCategoryThumbnails(categoryPath) { 20649 // Check cache first 20650 if (thumbnailCache.has(categoryPath)) { 20651 injectThumbnails(categoryPath, thumbnailCache.get(categoryPath)); 20652 return; 20653 } 20654 20655 const url = `/api/category_products_preview.php?path=${encodeURIComponent(categoryPath)}&limit=2`; 20656 20657 fetch(url) 20658 .then(r => { 20659 if (!r.ok) throw new Error('Network error'); 20660 return r.json(); 20661 }) 20662 .then(data => { 20663 if (data.success && data.products && data.products.length > 0) { 20664 thumbnailCache.set(categoryPath, data.products); 20665 injectThumbnails(categoryPath, data.products); 20666 } else { 20667 // No products, remove skeleton 20668 removeThumbSkeletons(categoryPath); 20669 } 20670 }) 20671 .catch(err => { 20672 console.warn('Failed to load thumbnails for', categoryPath, err); 20673 removeThumbSkeletons(categoryPath); 20674 }); 20675 } 20676 20677 // Inject thumbnails into the DOM 20678 function injectThumbnails(categoryPath, products) { 20679 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20680 links.forEach(link => { 20681 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20682 if (!thumbsContainer) return; 20683 20684 const thumbsHtml = products.slice(0, 2).map(p => 20685 `<img src="${esc(p.img)}" alt="${esc(p.name)}" class="mm2-thumb" title="${esc(p.name)}" loading="lazy">` 20686 ).join(''); 20687 20688 thumbsContainer.innerHTML = thumbsHtml; 20689 thumbsContainer.removeAttribute('data-loading'); 20690 }); 20691 } 20692 20693 // Remove skeleton loaders when no products 20694 function removeThumbSkeletons(categoryPath) { 20695 const links = detailView.querySelectorAll(`[data-cat-path="${categoryPath}"]`); 20696 links.forEach(link => { 20697 const thumbsContainer = link.querySelector('.mm2-thumbs'); 20698 if (thumbsContainer) { 20699 thumbsContainer.remove(); 20700 } 20701 }); 20702 } 20703 20704 function showDetail(node){ 20705 const title = node.label.split('/')[0]; 20706 const children = Array.isArray(node.children) ? node.children : []; 20707 const hasChildren = children.length > 0; 20708 20709 const subnav = hasChildren ? ` 20710 <nav class="mm2-subnav" role="tablist"> 20711 ${children.map((c2,i)=>{ 20712 const t = c2.label.split('/').slice(-1)[0]; 20713 return `<button class="mm2-subitem${i===0?' active':''}" role="tab" data-idx="${i}" aria-selected="${i===0?'true':'false'}">${esc(t)}</button>`; 20714 }).join('')} 20715 </nav>` : ''; 20716 20717 const subcontent = `<div class="mm2-subcontent" data-mm2="subcontent"></div>`; 20718 20719 detailView.innerHTML = ` 20720 <div class="mm2-detail"> 20721 <div class="mm2-detail-header"> 20722 <button class="mm2-back" type="button" data-mm2="back" aria-label="Kembali"> 20723 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"></polyline></svg> 20724 </button> 20725 <div class="mm2-detail-title">${esc(title)}</div> 20726 </div> 20727 20728 ${hasChildren ? ` 20729 <div class="mm2-detail-layout"> 20730 ${subnav} 20731 ${subcontent} 20732 </div>` : ` 20733 <div class="mm2-links"> 20734 <a class="mm2-link mm2-seeall" href="/product/kategori/?kategori_path=${encodeURIComponent(node.path)}" data-mm2="nav" data-path="${esc(node.path)}" data-label="${esc(node.label)}"> 20735 <strong>Lihat semua ${esc(title)}</strong> 20736 </a> 20737 </div> 20738 `} 20739 </div> 20740 `; 20741 20742 topView.classList.remove('is-active');
20743 detailView.classList.add('is-active'); 20744 20745 if (hasChildren) { 20746 const subEl = detailView.querySelector('.mm2-subnav'); 20747 const contEl = detailView.querySelector('[data-mm2="subcontent"]'); 20748 let activeIdx = 0; 20749 20750 const renderSubgrid = (idx) => { 20751 const c2 = children[idx]; if (!c2) { contEl.innerHTML=''; return; } 20752 const title2 = c2.label.split('/').slice(-1)[0]; 20753 const l3 = Array.isArray(c2.children) ? c2.children : []; 20754 const l3Html = l3.map(c3 => ` 20755 <a class="mm2-link" href="/product/kategori/?kategori_path=${encodeURIComponent(c3.path)}" data-mm2="nav" data-path="${esc(c3.path)}" data-label="${esc(c3.label)}" data-cat-path="${esc(c3.path)}"> 20756 <span class="mm2-link-text">${esc(c3.label.split('/').slice(-1)[0])}</span> 20757 <span class="mm2-thumbs" data-loading="true"> 20758 <span class="mm2-thumb-skeleton"></span> 20759 <span class="mm2-thumb-skeleton"></span> 20760 </span> 20761 </a>`).join(''); 20762 20763 contEl.innerHTML = ` 20764 <div class="mm2-links"> 20765 <a class="mm2-link mm2-seeall" href="/product/kategori/?kategori_path=${encodeURIComponent(c2.path)}" data-mm2="nav" data-path="${esc(c2.path)}" data-label="${esc(c2.label)}"> 20766 <strong>Lihat semua ${esc(title2)}</strong> 20767 </a> 20768 ${l3Html} 20769 </div> 20770 `; 20771 20772 // Fetch product thumbnails for each level-3 category (desktop only) 20773 if (isDesktop()) { 20774 setTimeout(() => { 20775 l3.forEach(c3 => { 20776 fetchCategoryThumbnails(c3.path); 20777 }); 20778 }, 100); 20779 } 20780 }; 20781 20782 renderSubgrid(activeIdx); 20783 20784 // Desktop: hover ganti konten; Mobile & Desktop: click pilih 20785 subEl.addEventListener('mousemove', (e)=>{ 20786 if (!isDesktop()) return; 20787 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20788 const idx = +it.dataset.idx; if (!Number.isInteger(idx) || idx===activeIdx) return; 20789 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20790 it.classList.add('active'); 20791 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20792 activeIdx = idx; 20793 renderSubgrid(activeIdx); 20794 }); 20795 20796 subEl.addEventListener('click', (e)=>{ 20797 const it = e.target.closest('.mm2-subitem'); if (!it) return; 20798 const idx = +it.dataset.idx; if (!Number.isInteger(idx)) return; 20799 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.classList.remove('active')); 20800 it.classList.add('active'); 20801 subEl.querySelectorAll('.mm2-subitem').forEach(el=>el.setAttribute('aria-selected', el===it ? 'true':'false')); 20802 activeIdx = idx; 20803 renderSubgrid(activeIdx); 20804 }); 20805 20806 // Delegate link clicks to add recent 20807 detailView.addEventListener('click', (e)=>{ 20808 const nav = e.target.closest('[data-mm2="nav"]'); 20809 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20810 }); 20811 } 20812 } 20813 20814 function buildTop(){ 20815 const html = data.map((n, idx) => { 20816 const title = n.label.split('/')[0]; 20817 const childCount = (n.children||[]).length; 20818 const productCount = countProducts(n); 20819 const gradient = categoryGradients[idx % categoryGradients.length]; 20820 20821 // Dynamic subtitle based on content - shorter for mobile 20822 let sub = ''; 20823 if (productCount > 0) { 20824 sub = `${productCount} produk`; 20825 if (childCount > 0) sub += ` · ${childCount} kat`; 20826 } else if (childCount > 0) { 20827 sub = `${childCount} kategori`; 20828 } else { 20829 sub = 'Lihat semua'; 20830 } 20831 20832 const iconStyle = `background: linear-gradient(135deg, ${gradient.from}, ${gradient.to}); border-color: ${gradient.border}; color: ${gradient.color};`; 20833 20834 return `<a href="/product/kategori/?kategori_path=${encodeURIComponent(n.path)}" class="mm2-card" data-mm2="card" data-path="${esc(n.path)}" data-label="${esc(n.label)}"> 20835 <span class="mm2-ico" style="${iconStyle}">${icon()}</span>
20836 <span style="flex: 1;"> 20837 <div class="mm2-title">${esc(title)}</div> 20838 <div class="mm2-sub">${esc(sub)}</div> 20839 </span> 20840 </a>`; 20841 }).join(''); 20842 const closeBtnHtml = !isDesktop() ? mobileCloseBtn() : ''; 20843 topView.innerHTML = `<div class="mm2-top">${closeBtnHtml}<div class="mm2-grid">${html}</div></div>`; 20844 } 20845 20846 // Open/close with safe-area 20847 trigger.addEventListener('click', (e)=>{ e.preventDefault(); document.body.classList.contains('mm2-open') ? close() : open(); }); 20848 trigger.addEventListener('pointerenter', ()=>{ if (isDesktop()) { clearTimeout(closeTimer); hoverTimer=setTimeout(open, 80); }}); 20849 trigger.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) { clearTimeout(hoverTimer); closeTimer=setTimeout(close, 200); }}); 20850 panel.addEventListener('pointerenter', ()=>{ if (isDesktop()) clearTimeout(closeTimer); }); 20851 panel.addEventListener('pointerleave', (e)=>{ if (isDesktop() && !isToSafeArea(e)) closeTimer=setTimeout(close, 200); }); 20852 backdrop?.addEventListener('pointerenter', ()=>{ clearTimeout(closeTimer); }); 20853 backdrop?.addEventListener('click', close); 20854 document.addEventListener('keydown', (e)=>{ if (e.key==='Escape') close(); }); 20855 20856 // Card click â open detail (unless cmd/ctrl to open link) 20857 panel.addEventListener('click', (e)=>{ 20858 const closeBtn = e.target.closest('[data-mm2="close"]'); if (closeBtn) { e.preventDefault(); close(); return; } 20859 const back = e.target.closest('[data-mm2="back"]'); if (back) { e.preventDefault(); showTop(); return; } 20860 const card = e.target.closest('[data-mm2="card"]'); 20861 if (card && !e.metaKey && !e.ctrlKey) { 20862 e.preventDefault(); 20863 const n = data.find(d => d.path === card.dataset.path); 20864 if (n) showDetail(n); 20865 } 20866 const nav = e.target.closest('[data-mm2="nav"]'); 20867 if (nav) { saveRecent(nav.dataset.path, nav.dataset.label); } 20868 }); 20869 20870 // Swipe-down gesture for mobile (bottom sheet close) 20871 if (!isDesktop()) { 20872 let startY = 0, currentY = 0, isDragging = false, startTime = 0; 20873 const SWIPE_THRESHOLD = 80; 20874 const VELOCITY_THRESHOLD = 0.3; 20875 20876 panel.addEventListener('touchstart', (e) => { 20877 if (!document.body.classList.contains('mm2-open')) return; 20878 const touch = e.touches[0]; 20879 const scrollableContent = e.target.closest('.mm2-top, .mm2-detail'); 20880 20881 // Prevent drag if content is scrolled 20882 if (scrollableContent && scrollableContent.scrollTop > 5) return; 20883 20884 // Only start drag from top area or handle 20885 const rect = panel.getBoundingClientRect(); 20886 if (touch.clientY - rect.top > 60) return; 20887 20888 startY = touch.clientY; 20889 currentY = startY; 20890 startTime = Date.now(); 20891 isDragging = true; 20892 }, { passive: true }); 20893 20894 panel.addEventListener('touchmove', (e) => { 20895 if (!isDragging) return; 20896 const touch = e.touches[0]; 20897 currentY = touch.clientY; 20898 const deltaY = currentY - startY; 20899 20900 // Only allow downward swipe 20901 if (deltaY > 0) { 20902 const opacity = Math.max(0.4, 1 - (deltaY / 300)); 20903 panel.style.transform = `translateX(0) translateY(${deltaY}px)`; 20904 panel.style.opacity = opacity; 20905 } 20906 }, { passive: true }); 20907 20908 const endDrag = () => { 20909 if (!isDragging) return; 20910 const deltaY = currentY - startY; 20911 const deltaTime = Date.now() - startTime; 20912 const velocity = deltaY / deltaTime; 20913 20914 // Close if swiped down enough or fast swipe 20915 if (deltaY > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { 20916 close(); 20917 } 20918 20919 // Reset position 20920 panel.style.transform = ''; 20921 panel.style.opacity = ''; 20922 isDragging = false; 20923 }; 20924 20925 panel.addEventListener('touchend', endDrag, { passive: true }); 20926 panel.addEventListener('touchcancel', endDrag, { passive: true }); 20927 } 20928 20929 // Fetch data with loading state 20930 if (!isLoading) { 20931 isLoading = true; 20932 showLoading(); 20933 20934 fetch(apiUrl, { headers: { 'Accept': 'application/json' }}) 20935 .then(r => { 20936 if (!r.ok) throw new Error('Network response was not ok'); 20937 return r.json(); 20938 }) 20939 .then(j => { 20940 data = Array.isArray(j.data) ? j.data : []; 20941 if (!data.length) { 20942 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20943 <p>Tidak ada kategori tersedia</p> 20944 </div></div>`; 20945 return; 20946 }
20947 buildTop(); 20948 isLoading = false; 20949 }) 20950 .catch(err => { 20951 console.error('Failed to load categories:', err); 20952 topView.innerHTML = `<div class="mm2-top"><div style="text-align:center;padding:40px;color:var(--mm2-fg-muted);"> 20953 <p>Gagal memuat kategori. Silakan coba lagi.</p> 20954 </div></div>`; 20955 isLoading = false; 20956 }); 20957 } 20958})(); 20959 20960// Add skeleton animation styles 20961const style = document.createElement('style'); 20962style.textContent = ` 20963.mm2-skeleton { 20964 pointer-events: none; 20965 animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; 20966} 20967.skeleton-box, .skeleton-line { 20968 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 20969 background-size: 200% 100%; 20970 animation: shimmer 1.5s infinite; 20971 border-radius: 8px; 20972} 20973@keyframes pulse { 20974 0%, 100% { opacity: 1; } 20975 50% { opacity: 0.6; } 20976} 20977@keyframes shimmer { 20978 0% { background-position: 200% 0; } 20979 100% { background-position: -200% 0; } 20980} 20981`; 20982document.head.appendChild(style); 20983</script>
20983 20984
20984<script data-inlined="benchmark.js"> 20985/** 20986 * Benchmark Data Loader 20987 * Handles AJAX fetch and display of benchmark data 20988 */ 20989 20990(function() { 20991 'use strict'; 20992 20993 // Wait for DOM ready 20994 if (document.readyState === 'loading') { 20995 document.addEventListener('DOMContentLoaded', initBenchmark); 20996 } else { 20997 initBenchmark(); 20998 } 20999 21000 function initBenchmark() { 21001 const section = document.querySelector('.pd-benchmark-section'); 21002 if (!section) return; 21003 21004 const button = document.getElementById('btn-load-benchmark'); 21005 const content = document.getElementById('benchmark-content'); 21006 const productId = section.dataset.productId; 21007 21008 if (!button || !content || !productId) return; 21009 21010 let isLoading = false; 21011 let dataLoaded = false; 21012 21013 button.addEventListener('click', function() { 21014 if (isLoading || dataLoaded) return; 21015 21016 loadBenchmarkData(productId, button, content); 21017 }); 21018 } 21019 21020 function loadBenchmarkData(productId, button, container) { 21021 // Update button state 21022 button.disabled = true; 21023 button.classList.add('loading'); 21024 const originalText = button.querySelector('.btn-text').textContent; 21025 button.querySelector('.btn-text').textContent = 'Memuat data...'; 21026 21027 // Show loading state 21028 container.style.display = 'block'; 21029 container.innerHTML = ` 21030 <div class="benchmark-loading"> 21031 <div class="benchmark-loading-spinner"></div> 21032 <p>Mengambil data benchmark...</p> 21033 </div> 21034 `; 21035 21036 // Fetch data 21037 fetch('/api/benchmark/fetch.php?product_id=' + productId) 21038 .then(function(response) { 21039 if (!response.ok) { 21040 throw new Error('HTTP error ' + response.status); 21041 } 21042 return response.json(); 21043 }) 21044 .then(function(result) { 21045 if (result.success && result.data) { 21046 renderBenchmarkData(result.data, container); 21047 button.style.display = 'none'; // Hide button after successful load 21048 } else { 21049 throw new Error(result.error || 'Data tidak tersedia'); 21050 } 21051 }) 21052 .catch(function(error) { 21053 console.error('Benchmark fetch error:', error); 21054 container.innerHTML = ` 21055 <div class="benchmark-error"> 21056 <p><strong>â ï¸ Data Benchmark Tidak Tersedia</strong></p> 21057 <p style="font-size: 14px; margin-top: 8px;"> 21058 ${error.message || 'Terjadi kesalahan saat memuat data'} 21059 </p> 21060 <button type="button" onclick="location.reload()" 21061 style="margin-top: 16px; padding: 8px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer;"> 21062 Coba Lagi 21063 </button> 21064 </div> 21065 `; 21066 21067 // Reset button 21068 button.disabled = false; 21069 button.classList.remove('loading'); 21070 button.querySelector('.btn-text').textContent = originalText; 21071 }); 21072 } 21073 21074 function renderBenchmarkData(data, container) { 21075 let html = '<div class="benchmark-data-wrapper">'; 21076 21077 // Chipset info if available 21078 if (data.chipset) { 21079 html += ` 21080 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 10px 16px; border-radius: 8px; margin-bottom: 12px;"> 21081 <div style="font-size: 11px; opacity: 0.85; margin-bottom: 2px;">Chipset</div> 21082 <div style="font-size: 15px; font-weight: 600;">${escapeHtml(data.chipset)}</div> 21083 </div> 21084 `; 21085 } 21086 21087 // Main benchmark scores 21088 if (data.antutu || data.geekbench_multi || data.overall_score) { 21089 html += '<div class="benchmark-data-grid">'; 21090 21091 if (data.antutu) { 21092 html += ` 21093 <div class="benchmark-card"> 21094 <div class="benchmark-card-label">ANTUTU</div> 21095 <div class="benchmark-card-value large">${formatNumber(data.antutu)}</div> 21096 ${getScoreBadge(data.antutu, 'antutu')} 21097 </div> 21098 `; 21099 } 21100 21101 if (data.geekbench_multi) { 21102 html += ` 21103 <div class="benchmark-card"> 21104 <div class="benchmark-card-label">GEEKBENCH MULTI</div> 21105 <div class="benchmark-card-value">${formatNumber(data.geekbench_multi)}</div> 21106 ${data.geekbench_single ? '<div class="benchmark-card-subtitle">Single: ' + formatNumber(data.geekbench_single) + '</div>' : ''} 21107 </div> 21108 `; 21109 } 21110 21111 if (data.gaming_score) { 21112 html += ` 21113 <div class="benchmark-card"> 21114 <div class="benchmark-card-label">GAMING</div> 21115 <div class="benchmark-card-value">${data.gaming_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21116 </div> 21117 `; 21118 } 21119 21120 if (data.battery_score) { 21121 html += ` 21122 <div class="benchmark-card"> 21123 <div class="benchmark-card-label">BATTERY</div> 21124 <div class="benchmark-card-value">${data.battery_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21125 </div> 21126 `; 21127 } 21128 21129 if (data.overall_score) { 21130 html += ` 21131 <div class="benchmark-card"> 21132 <div class="benchmark-card-label">OVERALL</div> 21133 <div class="benchmark-card-value">${data.overall_score}<span style="font-size: 14px; opacity: 0.6;">/100</span></div> 21134 </div> 21135 `; 21136 } 21137 21138 html += '</div>'; 21139 } 21140 21141 // AI Performance Description 21142 if (data.notes) { 21143 html += ` 21144 <div style="background: #f8f9fa; border-left: 3px solid #667eea; padding: 10px 14px; border-radius: 6px; margin-top: 12px;"> 21145 <div style="font-size: 11px; font-weight: 600; color: #667eea; margin-bottom: 6px;">ð¡ Analisis Performa</div> 21146 <div style="font-size: 13px; color: #4a5568; line-height: 1.5;">${escapeHtml(data.notes)}</div> 21147 </div> 21148 `; 21149 } 21150 21151 // Attribution 21152 html += ` 21153 <div class="benchmark-attribution"> 21154 <p style="font-size: 11px; margin: 8px 0 0 0; color: #888;">Data benchmark dianalisis menggunakan AI dari spesifikasi produk</p> 21155 ${data.fetched_at ? '<p style="font-size: 10px; margin: 4px 0 0 0; color: #aaa;">Terakhir diperbarui: ' + formatDate(data.fetched_at) + '</p>' : ''} 21156 </div> 21157 `; 21158 21159 html += '</div>'; 21160 21161 container.innerHTML = html; 21162 } 21163 21164 function formatNumber(num) { 21165 if (!num) return '-'; 21166 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 21167 } 21168 21169 function escapeHtml(text) { 21170 if (!text) return ''; 21171 const div = document.createElement('div'); 21172 div.textContent = text; 21173 return div.innerHTML; 21174 } 21175 21176 function formatDate(dateStr) { 21177 try { 21178 const date = new Date(dateStr); 21179 const options = { year: 'numeric', month: 'long', day: 'numeric' }; 21180 return date.toLocaleDateString('id-ID', options); 21181 } catch (e) { 21182 return dateStr; 21183 } 21184 } 21185 21186 function getScoreBadge(score, type) { 21187 let category = ''; 21188 let label = ''; 21189 21190 if (type === 'antutu') { 21191 if (score >= 500000) { 21192 category = 'excellent'; 21193 label = 'Flagship'; 21194 } else if (score >= 350000) { 21195 category = 'good'; 21196 label = 'High-End'; 21197 } else if (score >= 200000) { 21198 category = 'average'; 21199 label = 'Mid-Range'; 21200 } else { 21201 category = 'average'; 21202 label = 'Entry-Level'; 21203 } 21204 } 21205 21206 if (label) { 21207 return '<div class="benchmark-score-badge ' + category + '">' + label + '</div>'; 21208 } 21209 21210 return ''; 21211 } 21212})(); 21213 21214</script>
21214 21215
21215<script data-inlined="product-compare.js"> 21216(function() { 21217 'use strict'; 21218 21219 // State management 21220 let compareList = []; 21221 let isAnalyzing = false; 21222 let currentAnalysis = null; 21223 21224 // DOM elements 21225 const compareBtn = document.getElementById('compareBtn'); 21226 const compareSidebar = document.getElementById('compareSidebar'); 21227 const compareOverlay = document.getElementById('compareOverlay'); 21228 const compareClose = document.getElementById('compareClose'); 21229 const compareModal = document.getElementById('aiCompareModal'); 21230 const compareModalClose = document.getElementById('aiCompareModalClose'); 21231 21232 // Initialize 21233 document.addEventListener('DOMContentLoaded', async function() { 21234 await initializeCompare(); 21235 setupEventListeners(); 21236 }); 21237 21238 /** 21239 * Initialize compare functionality 21240 */ 21241 async function initializeCompare() { 21242 // Load compare list first before setting up buttons 21243 await loadCompareList(); 21244 21245 // Setup product detail compare button if exists 21246 const productCompareBtn = document.getElementById('pd-compare-btn'); 21247 21248 if (productCompareBtn) { 21249 setupProductCompareButton(productCompareBtn); 21250 } 21251 21252 // Check for existing compare list and update UI 21253 if (compareList.length > 0) { 21254 // Only update badge and buttons, no sticky bar 21255 } 21256 } 21257 21258 /** 21259 * Setup event listeners 21260 */ 21261 function setupEventListeners() { 21262 // Navigation compare button 21263 if (compareBtn) { 21264 compareBtn.addEventListener('click', toggleCompareSidebar); 21265 } 21266 21267 // Sidebar controls 21268 if (compareClose) { 21269 compareClose.addEventListener('click', closeCompareSidebar); 21270 } 21271 21272 if (compareOverlay) { 21273 compareOverlay.addEventListener('click', closeCompareSidebar); 21274 } 21275 21276 // Sidebar actions 21277 const clearAllBtn = document.getElementById('compareClearAll'); 21278 if (clearAllBtn) { 21279 clearAllBtn.addEventListener('click', clearAllProducts); 21280 } 21281 21282 const analyzeBtn = document.getElementById('compareAnalyze'); 21283 if (analyzeBtn) { 21284 analyzeBtn.addEventListener('click', startAIAnalysis); 21285 } 21286 21287 // Modal controls 21288 if (compareModalClose) { 21289 compareModalClose.addEventListener('click', closeModal); 21290 } 21291 21292 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 21293 if (modalBackdrop) { 21294 modalBackdrop.addEventListener('click', closeModal); 21295 } 21296 21297 // Modal actions 21298 const retryBtn = document.getElementById('aiRetry'); 21299 if (retryBtn) { 21300 retryBtn.addEventListener('click', startAIAnalysis); 21301 } 21302 21303 // Removed share and add to cart buttons - replaced with AI disclaimer 21304 } 21305 21306 /** 21307 * Setup product detail compare button 21308 */ 21309 function setupProductCompareButton(button) { 21310 const productId = parseInt(button.dataset.productId); 21311 if (!productId) return; 21312 21313 // Check if product is already in compare list 21314 updateProductCompareButtonState(button, productId); 21315 21316 button.addEventListener('click', async function() { 21317 // Get current variant ID (if any) 21318 const variantIdElement = document.getElementById('pd-variant-id'); 21319 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 21320 21321 // Update button's variant data for current selection 21322 button.dataset.variantId = variantId.toString(); 21323 21324 await toggleProductInCompare(productId, button, variantId); 21325 }); 21326 } 21327 21328 /** 21329 * Toggle product in compare list 21330 */ 21331 async function toggleProductInCompare(productId, button, variantId = 0) { 21332 // Check by product ID (variants of same product treated as same item) 21333 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21334 21335 try { 21336 if (isInCompare) { 21337 await removeFromCompare(productId); 21338 showToast('Produk dihapus dari perbandingan', 'success'); 21339 } else { 21340 await addToCompare(productId, variantId); 21341 showToast('Produk ditambahkan ke perbandingan', 'success'); 21342 21343 // Trigger fly-to-compare animation 21344 triggerFlyToCompareAnimation(button); 21345 } 21346 21347 updateProductCompareButtonState(button, productId); 21348 updateCompareBadge(); 21349 updateAllCompareButtons(); 21350 21351 } catch (error) { 21352 showToast(error.message || 'Terjadi kesalahan', 'error'); 21353 } 21354 } 21355 21356 /** 21357 * Update product compare button state 21358 */ 21359 function updateProductCompareButtonState(button, productId) { 21360 // Ensure both IDs are compared as integers 21361 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 21362 button.classList.toggle('added', isInCompare); 21363 21364 // Handle visibility of text elements 21365 const compareText = button.querySelector('.compare-text'); 21366 const compareRemove = button.querySelector('.compare-remove'); 21367 const compareNote = document.getElementById('compare-note'); 21368 21369 if (compareText) { 21370 compareText.style.display = isInCompare ? 'none' : 'inline'; 21371 } 21372 21373 if (compareRemove) { 21374 if (isInCompare) { 21375 compareRemove.removeAttribute('hidden'); 21376 compareRemove.style.display = 'flex'; 21377 } else { 21378 compareRemove.setAttribute('hidden', ''); 21379 compareRemove.style.display = 'none'; 21380 } 21381 } 21382 21383 // Update info note 21384 if (compareNote) { 21385 if (isInCompare) { 21386 const currentCount = compareList.length; 21387 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 21388 } else { 21389 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 21390 } 21391 } 21392 21393 // Inline compare preview removed - keeping it simple 21394 } 21395 21396 /** 21397 * Add product to compare list 21398 */ 21399 async function addToCompare(productId, variantId = 0) { 21400 const requestData = { product_id: productId }; 21401 if (variantId > 0) { 21402 requestData.variant_id = variantId; 21403 } 21404 21405 const response = await fetch('/api/compare/add.php', { 21406 method: 'POST', 21407 headers: { 21408 'Content-Type': 'application/json', 21409 }, 21410 body: JSON.stringify(requestData), 21411 credentials: 'same-origin' 21412 }); 21413 21414 const result = await response.json(); 21415 21416 if (!result.success) { 21417 throw new Error(result.error || 'Gagal menambahkan produk'); 21418 } 21419 21420 // Send Google Analytics event for add to compare 21421 if (typeof gtag === 'function') { 21422 gtag('event', 'add_to_compare', { 21423 event_category: 'Product Compare', 21424 event_label: 'Product Added to Compare', 21425 product_id: productId, 21426 variant_id: variantId || 0, 21427 custom_map: { 21428 custom_parameter_1: 'compare_add_product' 21429 } 21430 }); 21431 } 21432
21433 await loadCompareList(); 21434 return result; 21435 } 21436 21437 /** 21438 * Remove product from compare list 21439 */ 21440 async function removeFromCompare(productId) { 21441 const response = await fetch('/api/compare/remove.php', { 21442 method: 'POST', 21443 headers: { 21444 'Content-Type': 'application/json', 21445 }, 21446 body: JSON.stringify({ product_id: productId }), 21447 credentials: 'same-origin' 21448 }); 21449 21450 const result = await response.json(); 21451 21452 if (!result.success) { 21453 throw new Error(result.error || 'Gagal menghapus produk'); 21454 } 21455 21456 // Send Google Analytics event for remove from compare 21457 if (typeof gtag === 'function') { 21458 gtag('event', 'remove_from_compare', { 21459 event_category: 'Product Compare', 21460 event_label: 'Product Removed from Compare', 21461 product_id: productId, 21462 custom_map: { 21463 custom_parameter_1: 'compare_remove_product' 21464 } 21465 }); 21466 } 21467 21468 await loadCompareList(); 21469 return result; 21470 } 21471 21472 /** 21473 * Clear all products from compare list 21474 */ 21475 async function clearAllProducts() { 21476 if (compareList.length === 0) return; 21477 21478 // Show confirmation modal 21479 const confirmed = await showConfirmModal({ 21480 title: 'Hapus Semua Produk', 21481 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 21482 confirmText: 'Hapus Semua', 21483 cancelText: 'Batal', 21484 type: 'danger' 21485 }); 21486 21487 if (!confirmed) return; 21488 21489 try { 21490 const response = await fetch('/api/compare/clear.php', { 21491 method: 'POST', 21492 credentials: 'same-origin' 21493 }); 21494 21495 const result = await response.json(); 21496 21497 if (!result.success) { 21498 throw new Error(result.error || 'Gagal menghapus produk'); 21499 } 21500 21501 // Send Google Analytics event for clear all products 21502 if (typeof gtag === 'function') { 21503 gtag('event', 'compare_clear_all', { 21504 event_category: 'Product Compare', 21505 event_label: 'Clear All Products', 21506 products_count: compareList.length, 21507 custom_map: { 21508 custom_parameter_1: 'compare_clear_action' 21509 } 21510 }); 21511 } 21512 21513 await loadCompareList(); 21514 showToast('Semua produk dihapus dari perbandingan', 'success'); 21515 21516 } catch (error) { 21517 showToast(error.message || 'Terjadi kesalahan', 'error'); 21518 } 21519 } 21520 21521 /** 21522 * Load compare list from server 21523 */ 21524 async function loadCompareList() { 21525 try { 21526 const response = await fetch('/api/compare/get.php', { 21527 credentials: 'same-origin' 21528 }); 21529 21530 const result = await response.json(); 21531 21532 if (result.success) { 21533 compareList = result.products || []; 21534 updateCompareSidebar(); 21535 updateCompareBadge(); 21536 updateAllCompareButtons(); 21537 } 21538 21539 } catch (error) { 21540 // Silently handle error - compare functionality will work with empty list 21541 } 21542 } 21543 21544 /** 21545 * Update compare sidebar content 21546 */ 21547 function updateCompareSidebar() { 21548 const emptyState = document.getElementById('compareEmpty'); 21549 const listContainer = document.getElementById('compareList'); 21550 const actionsContainer = document.getElementById('compareActions'); 21551 const countElement = document.getElementById('compareCountSidebar'); 21552 21553 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 21554 21555 // Update count 21556 countElement.textContent = `(${compareList.length})`; 21557 21558 if (compareList.length === 0) { 21559 emptyState.style.display = 'block'; 21560 listContainer.style.display = 'none'; 21561 actionsContainer.style.display = 'none'; 21562 } else { 21563 emptyState.style.display = 'none'; 21564 listContainer.style.display = 'block'; 21565 actionsContainer.style.display = 'flex'; 21566 21567 // Render product list 21568 listContainer.innerHTML = compareList.map(product => { 21569 // Display variant name if available 21570 let displayName = product.name; 21571 if (product.combination_name) { 21572 displayName += ` (${product.combination_name})`; 21573 } 21574 21575 // Use variant price if available 21576 let displayPrice = product.price; 21577 if (product.variant_price !== undefined && product.variant_price !== null) { 21578 displayPrice = product.variant_price; 21579 } 21580 21581 return ` 21582 <div class="compare-item" data-product-id="${product.id}"> 21583 <div class="compare-item-image"> 21584 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 21585 </div> 21586 <div class="compare-item-info"> 21587 <div class="compare-item-name">${escapeHtml(displayName)}</div> 21588 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 21589 </div> 21590 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
21591 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 21592 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 21593 </svg> 21594 </button> 21595 </div> 21596 `; 21597 }).join(''); 21598 21599 // Setup event listeners for remove buttons 21600 setupRemoveButtons(); 21601 21602 // Update analyze button state 21603 const analyzeBtn = document.getElementById('compareAnalyze'); 21604 if (analyzeBtn) { 21605 analyzeBtn.disabled = compareList.length < 2; 21606 } 21607 } 21608 } 21609 21610 /** 21611 * Setup remove buttons event listeners 21612 */ 21613 function setupRemoveButtons() { 21614 const removeButtons = document.querySelectorAll('.compare-item-remove'); 21615 removeButtons.forEach(button => { 21616 button.addEventListener('click', async (e) => { 21617 e.preventDefault(); 21618 const productId = parseInt(button.dataset.productId); 21619 if (productId) { 21620 await removeProduct(productId); 21621 } 21622 }); 21623 }); 21624 } 21625 21626 /** 21627 * Update all compare buttons on the page 21628 */ 21629 function updateAllCompareButtons() { 21630 // Update product detail compare button 21631 const productBtn = document.getElementById('pd-compare-btn'); 21632 if (productBtn) { 21633 const productId = parseInt(productBtn.dataset.productId); 21634 if (productId) { 21635 updateProductCompareButtonState(productBtn, productId); 21636 } 21637 } 21638 21639 // Update any other compare buttons on the page 21640 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 21641 allCompareButtons.forEach(button => { 21642 const productId = parseInt(button.dataset.productId); 21643 if (productId && button.id !== 'pd-compare-btn') { 21644 updateProductCompareButtonState(button, productId); 21645 } 21646 }); 21647 } 21648 21649 /** 21650 * Update compare badge 21651 */ 21652 function updateCompareBadge() { 21653 const badge = document.getElementById('compareCount'); 21654 if (!badge) return; 21655 21656 const oldCount = parseInt(badge.textContent) || 0; 21657 const newCount = compareList.length; 21658 21659 badge.textContent = newCount; 21660 badge.classList.toggle('active', newCount > 0); 21661 21662 // Animate badge if count increased 21663 if (newCount > oldCount) { 21664 animateCompareBadge(); 21665 highlightCompareButton(); 21666 } 21667 21668 // Sticky bar removed - keeping only fly-to-navbar animation 21669 } 21670 21671 /** 21672 * Animate compare badge when new product added 21673 */ 21674 function animateCompareBadge() { 21675 const badge = document.getElementById('compareCount'); 21676 if (!badge) return; 21677 21678 badge.style.transform = 'scale(1.3)'; 21679 badge.style.transition = 'transform 0.3s ease'; 21680 21681 setTimeout(() => { 21682 badge.style.transform = 'scale(1)'; 21683 }, 300); 21684 } 21685 21686 /** 21687 * Highlight compare button temporarily 21688 */ 21689 function highlightCompareButton() { 21690 const compareBtn = document.getElementById('compareBtn'); 21691 if (!compareBtn) return; 21692 21693 compareBtn.classList.add('pulse-highlight'); 21694 21695 setTimeout(() => { 21696 compareBtn.classList.remove('pulse-highlight'); 21697 }, 3000); 21698 } 21699 21700 /** 21701 * Trigger fly-to-compare animation 21702 */ 21703 function triggerFlyToCompareAnimation(sourceButton) { 21704 if (!sourceButton) { 21705 return; 21706 } 21707 21708 // Check if mobile view (bottom nav) or desktop (top nav) 21709 const isMobile = window.innerWidth <= 768; 21710 21711 // Get source position 21712 const sourceRect = sourceButton.getBoundingClientRect(); 21713 21714 // Calculate target position 21715 let targetTop, targetLeft; 21716 21717 if (isMobile) { 21718 // Mobile: fly to fixed bottom position (bottom nav area) 21719 // Use fixed coordinates regardless of bottom nav visibility 21720 targetTop = window.innerHeight - 80; // Just above bottom nav area 21721 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 21722 } else { 21723 // Desktop: fly to top navbar compare button 21724 const targetElement = document.getElementById('compareBtn'); 21725 if (!targetElement) { 21726 return; 21727 } 21728 const targetRect = targetElement.getBoundingClientRect(); 21729 targetTop = targetRect.top + 10; 21730 targetLeft = targetRect.left + 10; 21731 } 21732 21733 // Get product image for animation - try multiple selectors 21734 let productImage = document.querySelector('#pd-main-img, .product-gallery img, .pd-image img, .product-image img, .product-main-image img, img[alt*="produk"], img[src*="product"]'); 21735 21736 // Create animated element 21737 const flyingElement = document.createElement('div'); 21738 flyingElement.className = 'flying-product'; 21739 21740 if (productImage) { 21741 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 21742 flyingElement.querySelector('img').style.cssText = `
21743 width: 100%; 21744 height: 100%; 21745 object-fit: cover; 21746 border-radius: 8px; 21747 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21748 `; 21749 } else { 21750 // Fallback: colored div with icon 21751 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 21752 flyingElement.querySelector('.flying-fallback').style.cssText = ` 21753 width: 100%; 21754 height: 100%; 21755 background: #3b82f6; 21756 border-radius: 8px; 21757 display: flex; 21758 align-items: center; 21759 justify-content: center; 21760 font-size: 24px; 21761 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 21762 `; 21763 } 21764 21765 flyingElement.style.cssText = ` 21766 position: fixed; 21767 top: ${sourceRect.top}px; 21768 left: ${sourceRect.left}px; 21769 width: 60px; 21770 height: 60px; 21771 z-index: 9999; 21772 pointer-events: none; 21773 opacity: 1; 21774 transform: scale(1); 21775 `; 21776 21777 document.body.appendChild(flyingElement); 21778 21779 // Use Web Animations API for more reliable animation 21780 flyingElement.animate([ 21781 // Keyframe 1: Initial state 21782 { 21783 top: `${sourceRect.top}px`, 21784 left: `${sourceRect.left}px`, 21785 transform: 'scale(1)', 21786 opacity: 1 21787 }, 21788 // Keyframe 2: Final state 21789 { 21790 top: `${targetTop}px`, 21791 left: `${targetLeft}px`, 21792 transform: 'scale(0.3)', 21793 opacity: 0 21794 } 21795 ], { 21796 duration: 800, 21797 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 21798 fill: 'forwards' 21799 }); 21800 21801 // Clean up after animation completes 21802 setTimeout(() => { 21803 if (flyingElement.parentNode) { 21804 flyingElement.parentNode.removeChild(flyingElement); 21805 } 21806 }, 850); 21807 } 21808 21809 // Sticky compare bar functions removed - using only fly-to-navbar animation 21810 21811 /** 21812 * Toggle compare sidebar 21813 */ 21814 function toggleCompareSidebar() { 21815 if (!compareSidebar) return; 21816 21817 const isOpen = compareSidebar.classList.contains('open'); 21818 21819 if (isOpen) { 21820 closeCompareSidebar(); 21821 } else { 21822 openCompareSidebar(); 21823 } 21824 } 21825 21826 /** 21827 * Open compare sidebar 21828 */ 21829 function openCompareSidebar() { 21830 if (!compareSidebar || !compareOverlay) return; 21831 21832 compareSidebar.classList.add('open'); 21833 compareOverlay.classList.add('active'); 21834 document.body.style.overflow = 'hidden'; 21835 21836 // Send Google Analytics event for compare sidebar open 21837 if (typeof gtag === 'function') { 21838 gtag('event', 'compare_sidebar_open', { 21839 event_category: 'Product Compare', 21840 event_label: 'Compare Sidebar Opened', 21841 products_count: compareList.length, 21842 custom_map: { 21843 custom_parameter_1: 'compare_sidebar_view' 21844 } 21845 }); 21846 } 21847 } 21848 21849 /** 21850 * Close compare sidebar 21851 */ 21852 function closeCompareSidebar() { 21853 if (!compareSidebar || !compareOverlay) return; 21854 21855 compareSidebar.classList.remove('open'); 21856 compareOverlay.classList.remove('active'); 21857 document.body.style.overflow = ''; 21858 } 21859 21860 /** 21861 * Start AI analysis 21862 */ 21863 async function startAIAnalysis() { 21864 if (compareList.length < 2) { 21865 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 21866 return; 21867 } 21868 21869 if (isAnalyzing) return; 21870 21871 // Send Google Analytics event for AI Analysis start 21872 if (typeof gtag === 'function') { 21873 gtag('event', 'ai_analysis_start', { 21874 event_category: 'Product Compare', 21875 event_label: 'AI Analysis Button Click', 21876 products_count: compareList.length, 21877 product_ids: compareList.map(p => p.id).join(','), 21878 custom_map: { 21879 custom_parameter_1: 'compare_ai_analysis' 21880 } 21881 }); 21882 } 21883 21884 // Check if products are in similar categories 21885 const mismatchInfo = await checkProductCategoryMismatch(); 21886 if (mismatchInfo.shouldWarn) { 21887 const categoryList = Array.from(mismatchInfo.categories).join(', '); 21888 const confirmed = await showConfirmModal({ 21889 title: 'Produk Berbeda Kategori', 21890 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 21891 confirmText: 'Ya, Analisis Tetap', 21892 cancelText: 'Batal', 21893 type: 'warning' 21894 }); 21895 21896 if (!confirmed) return; 21897 } 21898 21899 openModal(); 21900 showLoadingState(); 21901 isAnalyzing = true; 21902 21903 try { 21904 const response = await fetch('/api/compare/ai-analysis.php', { 21905 method: 'POST', 21906 credentials: 'same-origin' 21907 }); 21908 21909 const result = await response.json(); 21910 21911 if (!result.success) { 21912 throw new Error(result.error || 'Analisis AI gagal'); 21913 } 21914 21915 currentAnalysis = result; 21916 showAnalysisResults(result); 21917 21918 // Send Google Analytics event for successful AI Analysis 21919 if (typeof gtag === 'function') { 21920 gtag('event', 'ai_analysis_success', { 21921 event_category: 'Product Compare', 21922 event_label: 'AI Analysis Completed', 21923 products_count: compareList.length, 21924 analysis_type: 'product_comparison', 21925 custom_map: { 21926 custom_parameter_1: 'compare_ai_success' 21927 } 21928 }); 21929 } 21930 21931 } catch (error) { 21932 showErrorState(error.message); 21933 21934 // Send Google Analytics event for failed AI Analysis 21935 if (typeof gtag === 'function') { 21936 gtag('event', 'ai_analysis_error', { 21937 event_category: 'Product Compare', 21938 event_label: 'AI Analysis Failed', 21939 products_count: compareList.length, 21940 error_message: error.message, 21941 custom_map: { 21942 custom_parameter_1: 'compare_ai_error' 21943 } 21944 }); 21945 } 21946 } finally { 21947 isAnalyzing = false;
21948 } 21949 } 21950 21951 /** 21952 * Show modal 21953 */ 21954 function openModal() { 21955 if (!compareModal) return; 21956 compareModal.classList.add('open'); 21957 document.body.style.overflow = 'hidden'; 21958 } 21959 21960 /** 21961 * Close modal 21962 */ 21963 function closeModal() { 21964 if (!compareModal) return; 21965 compareModal.classList.remove('open'); 21966 document.body.style.overflow = ''; 21967 } 21968 21969 /** 21970 * Show loading state 21971 */ 21972 function showLoadingState() { 21973 const loading = document.getElementById('aiLoading'); 21974 const error = document.getElementById('aiError'); 21975 const results = document.getElementById('aiResults'); 21976 21977 if (loading) loading.style.display = 'block'; 21978 if (error) error.hidden = true; 21979 if (results) results.hidden = true; 21980 } 21981 21982 /** 21983 * Show error state 21984 */ 21985 function showErrorState(message) { 21986 const loading = document.getElementById('aiLoading'); 21987 const error = document.getElementById('aiError'); 21988 const results = document.getElementById('aiResults'); 21989 const errorMessage = document.getElementById('aiErrorMessage'); 21990 21991 if (loading) loading.style.display = 'none'; 21992 if (error) error.hidden = false; 21993 if (results) results.hidden = true; 21994 if (errorMessage) errorMessage.textContent = message; 21995 } 21996 21997 /** 21998 * Show analysis results 21999 */ 22000 function showAnalysisResults(analysis) { 22001 const loading = document.getElementById('aiLoading'); 22002 const error = document.getElementById('aiError'); 22003 const results = document.getElementById('aiResults'); 22004 22005 if (loading) loading.style.display = 'none'; 22006 if (error) error.hidden = true; 22007 if (results) results.hidden = false; 22008 22009 const data = analysis.data; 22010 22011 // Update summary 22012 const summaryElement = document.getElementById('aiSummary'); 22013 if (summaryElement) summaryElement.textContent = data.summary || ''; 22014 22015 const winnerProduct = document.getElementById('winnerProduct'); 22016 const winnerReason = document.getElementById('winnerReason'); 22017 if (winnerProduct && winnerReason && data.winner_overall) { 22018 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 22019 winnerProduct.textContent = winner ? winner.name : 'Tidak diketahui'; 22020 winnerReason.textContent = data.winner_reason || ''; 22021 } 22022 22023 // Update comparison table 22024 updateComparisonTable(data.comparison_table); 22025 22026 // Update detailed analysis 22027 updateDetailedAnalysis(data.detailed_analysis); 22028 22029 // Update recommendations 22030 updateRecommendations(data.recommendations); 22031 22032 // Update buying tips 22033 updateBuyingTips(data.buying_tips); 22034 } 22035 22036 /** 22037 * Update comparison table 22038 */ 22039 function updateComparisonTable(comparisonData) { 22040 const container = document.getElementById('comparisonTable'); 22041 if (!container || !comparisonData?.categories) return; 22042 22043 // First, create product header row with names 22044 const productHeaderHtml = ` 22045 <div class="comparison-category"> 22046 <div class="category-header">Produk</div> 22047 <div class="category-products"> 22048 ${compareList.map(product => ` 22049 <div class="product-score"> 22050 <div class="product-name-header"> 22051 <strong>${escapeHtml(product.name)}</strong> 22052 </div> 22053 <div class="product-price"> 22054 ${formatPrice(product.price)} 22055 </div> 22056 </div> 22057 `).join('')} 22058 </div> 22059 </div> 22060 `; 22061 22062 // Then create comparison categories 22063 const categoriesHtml = comparisonData.categories.map(category => ` 22064 <div class="comparison-category"> 22065 <div class="category-header">${escapeHtml(category.name)}</div> 22066 <div class="category-products"> 22067 ${compareList.map(product => { 22068 const productData = category.products?.[product.id]; 22069 return ` 22070 <div class="product-score"> 22071 <div class="score-value"> 22072 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 22073 <span>${productData?.score || 0}/5</span> 22074 </div> 22075 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 22076 </div> 22077 `; 22078 }).join('')} 22079 </div> 22080 </div> 22081 `).join(''); 22082 22083 container.innerHTML = productHeaderHtml + categoriesHtml; 22084 } 22085 22086 /** 22087 * Update detailed analysis 22088 */ 22089 function updateDetailedAnalysis(analysisData) { 22090 const container = document.getElementById('detailedAnalysis'); 22091 if (!container || !analysisData) return; 22092 22093 container.innerHTML = compareList.map(product => { 22094 const productAnalysis = analysisData[product.id]; 22095 if (!productAnalysis) return ''; 22096 22097 // Use product name from API response if available, fallback to local name 22098 const productName = productAnalysis.product_name || product.name; 22099 22100 return ` 22101 <div class="product-analysis"> 22102 <h5>${escapeHtml(productName)}</h5> 22103 22104 ${productAnalysis.strengths ? ` 22105 <div class="analysis-section"> 22106 <h6 class="strengths">â Kelebihan</h6> 22107 <ul> 22108 ${productAnalysis.strengths.map(strength => 22109 `<li>${escapeHtml(strength)}</li>` 22110 ).join('')} 22111 </ul> 22112 </div> 22113 ` : ''} 22114 22115 ${productAnalysis.weaknesses ? ` 22116 <div class="analysis-section"> 22117 <h6 class="weaknesses">â Kekurangan</h6> 22118 <ul> 22119 ${productAnalysis.weaknesses.map(weakness => 22120 `<li>${escapeHtml(weakness)}</li>` 22121 ).join('')} 22122 </ul> 22123 </div> 22124 ` : ''} 22125 22126 ${productAnalysis.best_for ? ` 22127 <div class="analysis-section"> 22128 <h6 class="best-for">ð¯ Cocok Untuk</h6> 22129 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 22130 </div> 22131 ` : ''} 22132 </div> 22133 `; 22134 }
22134).filter(Boolean).join(''); 22135 } 22136 22137 /** 22138 * Update recommendations 22139 */ 22140 function updateRecommendations(recommendations) { 22141 if (!recommendations) return; 22142 22143 const budgetEl = document.getElementById('budgetRecommendation'); 22144 const performanceEl = document.getElementById('performanceRecommendation'); 22145 const balancedEl = document.getElementById('balancedRecommendation'); 22146 22147 if (budgetEl && recommendations.budget_conscious) { 22148 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 22149 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 22150 } 22151 22152 if (performanceEl && recommendations.performance_focused) { 22153 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 22154 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 22155 } 22156 22157 if (balancedEl && recommendations.balanced) { 22158 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 22159 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 22160 } 22161 } 22162 22163 /** 22164 * Check if products are from different categories 22165 */ 22166 async function checkProductCategoryMismatch() { 22167 if (compareList.length < 2) { 22168 return { shouldWarn: false, categories: new Set() }; 22169 } 22170 22171 // Get the root categories from each product 22172 const rootCategories = new Set(); 22173 const productCategories = []; 22174 22175 for (const product of compareList) { 22176 // Extract root category from the product's category_path if available 22177 const rootCategory = await getProductRootCategory(product); 22178 if (rootCategory) { 22179 rootCategories.add(rootCategory); 22180 productCategories.push({ 22181 product: product.name, 22182 category: rootCategory 22183 }); 22184 } 22185 } 22186 22187 // If we have more than 1 different root categories, show warning 22188 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 22189 22190 return { 22191 shouldWarn: shouldWarn, 22192 categories: rootCategories, 22193 productCategories: productCategories 22194 }; 22195 } 22196 22197 /** 22198 * Get product root category from category_path or fallback to name detection 22199 */ 22200 async function getProductRootCategory(product) { 22201 // First try to get from category_path if available 22202 if (product.category_path) { 22203 const rootCategory = product.category_path.split('/')[0]; 22204 return rootCategory; 22205 } 22206 22207 // If no category_path, try to fetch from product detail or use name detection 22208 const detectedCategory = detectProductCategory(product.name); 22209 return detectedCategory; 22210 } 22211 22212 /** 22213 * Detect product category from name (fallback method) 22214 */ 22215 function detectProductCategory(productName) { 22216 const name = productName.toLowerCase(); 22217 22218 // Map keywords to actual root categories from the API structure 22219 const categoryMappings = { 22220 // Komputer & Laptop 22221 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 22222 22223 // Printer & Scanner 22224 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 22225 22226 // Gadget 22227 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 22228 22229 // Periferal 22230 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 22231 22232 // Audio 22233 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 22234 22235 // Jaringan 22236 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 22237 22238 // Penyimpanan 22239 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 22240 22241 // Komponen PC 22242 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 22243 22244 // UPS & Power 22245 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 22246 22247 // Proyektor 22248 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 22249 22250 // Security System 22251 'Security System': ['cctv', 'security camera', 'surveillance'], 22252 22253 // Aksesoris 22254 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 22255 22256 // Software 22257 'Software': ['software', 'lisensi', 'license', 'antivirus'], 22258
22259 // Perlengkapan Kantor 22260 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 22261 }; 22262 22263 // Check which root category matches 22264 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 22265 for (const keyword of keywords) { 22266 if (name.includes(keyword)) { 22267 return rootCategory; 22268 } 22269 } 22270 } 22271 22272 return null; 22273 } 22274 22275 /** 22276 * Update buying tips 22277 */ 22278 function updateBuyingTips(tips) { 22279 const container = document.getElementById('buyingTips'); 22280 if (!container || !Array.isArray(tips)) return; 22281 22282 container.innerHTML = tips.map(tip => 22283 `<li>${escapeHtml(tip)}</li>` 22284 ).join(''); 22285 } 22286 22287 // Removed share and cart functions - replaced with AI disclaimer in UI 22288 22289 /** 22290 * Remove product from compare (public method) 22291 */ 22292 async function removeProduct(productId) { 22293 try { 22294 await removeFromCompare(productId); 22295 showToast('Produk dihapus dari perbandingan', 'success'); 22296 22297 // Update all compare buttons 22298 updateAllCompareButtons(); 22299 22300 } catch (error) { 22301 showToast(error.message || 'Terjadi kesalahan', 'error'); 22302 } 22303 } 22304 22305 /** 22306 * Utility functions 22307 */ 22308 function escapeHtml(text) { 22309 const div = document.createElement('div'); 22310 div.textContent = text; 22311 return div.innerHTML; 22312 } 22313 22314 function formatPrice(price) { 22315 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 22316 } 22317 22318 /** 22319 * Show confirmation modal using the existing modal.css design 22320 */ 22321 function showConfirmModal(options = {}) { 22322 return new Promise((resolve) => { 22323 const { 22324 title = 'Konfirmasi', 22325 message = 'Apakah Anda yakin?', 22326 confirmText = 'Ya', 22327 cancelText = 'Tidak', 22328 type = 'warning' 22329 } = options; 22330 22331 // Create modal HTML 22332 const modalHTML = ` 22333 <div class="modal-overlay" id="confirmModal"> 22334 <div class="modal-dialog"> 22335 <div class="modal-header"> 22336 <h3 class="modal-title"> 22337 <div class="modal-icon ${type}"> 22338 ${getModalIcon(type)} 22339 </div> 22340 ${escapeHtml(title)} 22341 </h3> 22342 </div> 22343 <div class="modal-body"> 22344 <p class="modal-message">${escapeHtml(message)}</p> 22345 <div class="modal-actions"> 22346 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 22347 ${escapeHtml(cancelText)} 22348 </button> 22349 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 22350 ${escapeHtml(confirmText)} 22351 </button> 22352 </div> 22353 </div> 22354 </div> 22355 </div> 22356 `; 22357 22358 // Add to DOM 22359 document.body.insertAdjacentHTML('beforeend', modalHTML); 22360 const modal = document.getElementById('confirmModal'); 22361 const cancelBtn = document.getElementById('confirmCancel'); 22362 const okBtn = document.getElementById('confirmOk'); 22363 22364 // Show modal 22365 requestAnimationFrame(() => { 22366 modal.classList.add('show'); 22367 }); 22368 22369 // Handle events 22370 function cleanup() { 22371 modal.remove(); 22372 } 22373 22374 function handleCancel() { 22375 modal.classList.remove('show'); 22376 setTimeout(() => { 22377 cleanup(); 22378 resolve(false); 22379 }, 200); 22380 } 22381 22382 function handleConfirm() { 22383 modal.classList.remove('show'); 22384 setTimeout(() => { 22385 cleanup(); 22386 resolve(true); 22387 }, 200); 22388 } 22389 22390 cancelBtn.addEventListener('click', handleCancel); 22391 okBtn.addEventListener('click', handleConfirm); 22392 22393 // Close on overlay click 22394 modal.addEventListener('click', (e) => { 22395 if (e.target === modal) { 22396 handleCancel(); 22397 } 22398 }); 22399 22400 // Close on escape 22401 document.addEventListener('keydown', function escapeHandler(e) { 22402 if (e.key === 'Escape') { 22403 document.removeEventListener('keydown', escapeHandler); 22404 handleCancel(); 22405 } 22406 }); 22407 22408 // Focus on confirm button 22409 setTimeout(() => okBtn.focus(), 100); 22410 }); 22411 } 22412 22413 /** 22414 * Get icon for modal type 22415 */ 22416 function getModalIcon(type) { 22417 const icons = { 22418 danger: 'ð¨ ', 22419 warning: 'â ï¸', 22420 success: 'â ', 22421 info: 'â¹ï¸' 22422 }; 22423 return icons[type] || icons.info; 22424 } 22425 22426 22427 function showToast(message, type = 'info') { 22428 // Integration with existing toast system or create simple one 22429 22430 // Simple alert fallback - replace with proper toast system 22431 if (type === 'error') { 22432 alert('Error: ' + message); 22433 } 22434 } 22435 22436 // Inline compare preview functions removed - keeping it simple and clean 22437 22438 /** 22439 * Get current product ID from page 22440 */ 22441 function getCurrentProductId() { 22442 const button = document.getElementById('pd-compare-btn'); 22443 return button ? parseInt(button.dataset.productId) : null; 22444 } 22445 22446 // hideStickyCompareBar function removed - no longer using sticky bar 22447 22448 // Public API
22449 window.productCompare = { 22450 removeProduct: removeProduct, 22451 addToCompare: addToCompare, 22452 loadCompareList: loadCompareList, 22453 getCompareList: function() { return compareList; }, 22454 openSidebar: openCompareSidebar, 22455 startAnalysis: startAIAnalysis, 22456 // Helper function to add from external sources with variant support 22457 addProductWithVariant: async function(productId, variantId = 0) { 22458 return await addToCompare(productId, variantId); 22459 } 22460 }; 22461 22462})(); 22463 22464 22465 22466</script>
vendor: 1 bytes, line 22466
22466
22467<script data-inlined="product-card-actions.js"> 22468/** 22469 * Product Card Quick Actions 22470 * Bottom sheet menu for quick add to cart & compare 22471 * Integrates with existing product-compare.js and cart.js 22472 */ 22473 22474(function() { 22475 'use strict'; 22476 22477 let activeSheet = null; 22478 let activeProductData = null; 22479 22480 // DOM elements (will be created dynamically) 22481 let overlay = null; 22482 let bottomSheet = null; 22483 22484 /** 22485 * Initialize on DOM ready 22486 */ 22487 document.addEventListener('DOMContentLoaded', function() { 22488 createBottomSheet(); 22489 attachEventListeners(); 22490 }); 22491 22492 /** 22493 * Create bottom sheet HTML structure 22494 */ 22495 function createBottomSheet() { 22496 // Create overlay 22497 overlay = document.createElement('div'); 22498 overlay.className = 'pca-overlay'; 22499 overlay.addEventListener('click', closeBottomSheet); 22500 22501 // Create bottom sheet 22502 bottomSheet = document.createElement('div'); 22503 bottomSheet.className = 'pca-bottom-sheet'; 22504 bottomSheet.innerHTML = ` 22505 <div class="pca-sheet-handle"></div> 22506 <div class="pca-sheet-product" id="pca-product-preview"> 22507 <!-- Product info will be inserted here --> 22508 </div> 22509 <div class="pca-sheet-actions"> 22510 <button type="button" class="pca-action-item cart" id="pca-add-cart"> 22511 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22512 <path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/> 22513 </svg> 22514 <div class="pca-action-content"> 22515 <div class="pca-action-label">Tambah ke Keranjang</div> 22516 <div class="pca-action-desc">Lanjut belanja produk lain</div> 22517 </div> 22518 </button> 22519 22520 <button type="button" class="pca-action-item compare" id="pca-add-compare"> 22521 <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor"> 22522 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 22523 </svg> 22524 <div class="pca-action-content"> 22525 <div class="pca-action-label">Bandingkan Produk</div> 22526 <div class="pca-action-desc">Analisis dengan AI</div> 22527 </div> 22528 <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span> 22529 </button> 22530 </div> 22531 `; 22532 22533 // Append to body 22534 document.body.appendChild(overlay); 22535 document.body.appendChild(bottomSheet); 22536 22537 // Setup action buttons 22538 document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart); 22539 document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare); 22540 22541 // Prevent clicks inside bottom sheet from closing it 22542 bottomSheet.addEventListener('click', function(e) { 22543 e.stopPropagation(); 22544 }); 22545 } 22546 22547 /** 22548 * Attach event listeners to all 3-dot buttons 22549 */ 22550 function attachEventListeners() { 22551 // Use event delegation for dynamically loaded product cards 22552 document.addEventListener('click', function(e) { 22553 const btn = e.target.closest('.prod-card-actions-btn'); 22554 if (btn) { 22555 e.preventDefault(); 22556 e.stopPropagation(); 22557 handleActionsButtonClick(btn); 22558 } 22559 }); 22560 22561 // Handle ESC key 22562 document.addEventListener('keydown', function(e) { 22563 if (e.key === 'Escape' && activeSheet) { 22564 closeBottomSheet(); 22565 } 22566 }); 22567 } 22568 22569 /** 22570 * Handle 3-dot button click 22571 */ 22572 function handleActionsButtonClick(button) { 22573 // Get product ID directly from button 22574 const productId = parseInt(button.dataset.productId) || 0; 22575 if (!productId) { 22576 console.error('Product ID not found on button:', button); 22577 showToast('Data produk tidak ditemukan', 'error'); 22578 return; 22579 } 22580 22581 // Find parent card for additional data 22582 const card = button.closest('.prod-card, .pa-card'); 22583 if (!card) { 22584 console.error('Product card not found'); 22585 showToast('Data produk tidak ditemukan', 'error'); 22586 return; 22587 } 22588 22589 // Extract product data from card 22590 const productData = extractProductData(card, productId); 22591 if (!productData) { 22592 showToast('Data produk tidak ditemukan', 'error'); 22593 return; 22594 } 22595 22596 activeProductData = productData; 22597 openBottomSheet(productData); 22598 } 22599 22600 /** 22601 * Extract product data from card element 22602 */ 22603 function extractProductData(card, productId) { 22604 try { 22605 // Product ID already passed from button 22606 if (!productId) { 22607 console.error('Product ID not provided'); 22608 return null; 22609 } 22610 // Product ID already passed from button 22611 if (!productId) { 22612 console.error('Product ID not provided'); 22613 return null; 22614 } 22615 22616 const id = productId; 22617 22618 // Get product name 22619 const nameEl = card.querySelector('.prod-name, .title'); 22620 const name = nameEl ? nameEl.textContent.trim() : 'Produk'; 22621 22622 // Get product image 22623 const imgEl = card.querySelector('.thumb img, img'); 22624 const image = imgEl ? imgEl.src : '/assets/img/placeholder.png'; 22625 22626 // Get product price and detect variants 22627 const priceEl = card.querySelector('.prod-price-final, .final, .prod-price'); 22628 let price = 0; 22629 let hasVariants = false;
22630 let priceDisplay = ''; 22631 22632 if (priceEl) { 22633 // Get text content and clean it 22634 let priceText = priceEl.textContent.trim(); 22635 priceDisplay = priceText; 22636 22637 // Check if it's a range (indicates product has variants) 22638 if (priceText.includes('-')) { 22639 hasVariants = true; 22640 // Take the first price for calculation 22641 priceText = priceText.split('-')[0].trim(); 22642 } 22643 22644 // Remove all non-numeric characters 22645 priceText = priceText.replace(/[^0-9]/g, ''); 22646 price = parseInt(priceText) || 0; 22647 } 22648 22649 // Get stock status 22650 const stockDot = card.querySelector('.stock-dot'); 22651 let stockStatus = 'unknown'; 22652 let stockLabel = 'Cek stok'; 22653 22654 if (stockDot) { 22655 if (stockDot.classList.contains('ready')) { 22656 stockStatus = 'in'; 22657 stockLabel = 'Stok tersedia'; 22658 } else if (stockDot.classList.contains('low')) { 22659 stockStatus = 'low'; 22660 stockLabel = 'Stok terbatas'; 22661 } else if (stockDot.classList.contains('out')) { 22662 stockStatus = 'out'; 22663 stockLabel = 'Stok habis'; 22664 } 22665 } 22666 22667 // Get category path (if available) 22668 const categoryPath = card.dataset.category || ''; 22669 22670 // Get URL 22671 let url = '#'; 22672 if (card.tagName === 'A') { 22673 url = card.href; 22674 } else { 22675 const link = card.querySelector('a[href*="/product/"]'); 22676 if (link) url = link.href; 22677 } 22678 22679 return { 22680 id: id, 22681 name: name, 22682 image: image, 22683 price: price, 22684 priceDisplay: priceDisplay, 22685 hasVariants: hasVariants, 22686 stockStatus: stockStatus, 22687 stockLabel: stockLabel, 22688 categoryPath: categoryPath, 22689 url: url 22690 }; 22691 } catch (error) { 22692 console.error('Error extracting product data:', error); 22693 return null; 22694 } 22695 } 22696 22697 /** 22698 * Open bottom sheet 22699 */ 22700 function openBottomSheet(productData) { 22701 if (!overlay || !bottomSheet) return; 22702 22703 // Update product preview 22704 updateProductPreview(productData); 22705 22706 // Update cart button based on variant status 22707 updateCartButtonForVariant(productData); 22708 22709 // Update compare button state 22710 updateCompareButtonState(productData.id); 22711 22712 // Show overlay and sheet 22713 overlay.classList.add('active'); 22714 bottomSheet.classList.add('active'); 22715 document.body.style.overflow = 'hidden'; 22716 22717 activeSheet = true; 22718 22719 // Google Analytics 22720 if (typeof gtag === 'function') { 22721 gtag('event', 'quick_actions_open', { 22722 event_category: 'Product Card', 22723 event_label: 'Quick Actions Menu Opened', 22724 product_id: productData.id, 22725 product_name: productData.name 22726 }); 22727 } 22728 } 22729 22730 /** 22731 * Close bottom sheet 22732 */ 22733 function closeBottomSheet() { 22734 if (!overlay || !bottomSheet) return; 22735 22736 overlay.classList.remove('active'); 22737 bottomSheet.classList.remove('active'); 22738 document.body.style.overflow = ''; 22739 22740 activeSheet = false;
22741 activeProductData = null; 22742 } 22743 22744 /** 22745 * Update product preview in bottom sheet 22746 */ 22747 function updateProductPreview(productData) { 22748 const preview = document.getElementById('pca-product-preview'); 22749 if (!preview) return; 22750 22751 // Use full price display (shows range for variants) 22752 const priceDisplay = productData.priceDisplay || formatPrice(productData.price); 22753 22754 preview.innerHTML = ` 22755 <img src="${escapeHtml(productData.image)}" 22756 alt="${escapeHtml(productData.name)}" 22757 class="pca-product-image" 22758 loading="lazy"> 22759 <div class="pca-product-info"> 22760 <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4> 22761 <p class="pca-product-price">${escapeHtml(priceDisplay)}</p> 22762 <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p> 22763 </div> 22764 `; 22765 } 22766 22767 /** 22768 * Update cart button for variant products 22769 */ 22770 function updateCartButtonForVariant(productData) { 22771 const cartBtn = document.getElementById('pca-add-cart'); 22772 if (!cartBtn) return; 22773 22774 if (productData.hasVariants) { 22775 // Product has variants - change to "View Detail" 22776 cartBtn.dataset.hasVariants = 'true'; 22777 cartBtn.dataset.productUrl = productData.url; 22778 22779 // Update icon to eye/external 22780 const iconSvg = cartBtn.querySelector('svg'); 22781 if (iconSvg) { 22782 iconSvg.innerHTML = '<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z"/>'; 22783 } 22784 22785 // Update text 22786 const label = cartBtn.querySelector('.pca-action-label'); 22787 const desc = cartBtn.querySelector('.pca-action-desc'); 22788 if (label) label.textContent = 'Lihat Detail Produk'; 22789 if (desc) desc.textContent = 'Pilih varian terlebih dahulu'; 22790 22791 } else { 22792 // Single product - normal cart button 22793 cartBtn.dataset.hasVariants = 'false'; 22794 22795 // Restore cart icon 22796 const iconSvg = cartBtn.querySelector('svg'); 22797 if (iconSvg) { 22798 iconSvg.innerHTML = '<path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/>'; 22799 } 22800 22801 // Restore text 22802 const label = cartBtn.querySelector('.pca-action-label'); 22803 const desc = cartBtn.querySelector('.pca-action-desc'); 22804 if (label) label.textContent = 'Tambah ke Keranjang'; 22805 if (desc) desc.textContent = 'Lanjut belanja produk lain'; 22806 } 22807 } 22808 22809 /** 22810 * Update compare button state 22811 */ 22812 function updateCompareButtonState(productId) { 22813 const compareBtn = document.getElementById('pca-add-compare'); 22814 const compareBadge = document.getElementById('pca-compare-count'); 22815 22816 if (!compareBtn) return; 22817 22818 // Check if product is in compare list (using global compareList if available) 22819 let isInCompare = false; 22820 let compareCount = 0; 22821 22822 // Try to get compare list from product-compare.js via window.productCompare 22823 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') { 22824 const compareList = window.productCompare.getCompareList(); 22825 compareCount = compareList.length; 22826 isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 22827 } else { 22828 // Fallback: check badge count 22829 const mainBadge = document.getElementById('compareCount'); 22830 if (mainBadge) { 22831 compareCount = parseInt(mainBadge.textContent) || 0; 22832 } 22833 } 22834 22835 // Update button state 22836 if (isInCompare) { 22837 compareBtn.classList.add('active'); 22838 compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan'; 22839 compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar'; 22840 } else { 22841 compareBtn.classList.remove('active'); 22842 compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk'; 22843 compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI'; 22844 } 22845 22846 // Update badge 22847 if (compareBadge && compareCount > 0) { 22848 compareBadge.textContent = compareCount; 22849 compareBadge.style.display = 'inline-flex'; 22850 } else if (compareBadge) { 22851 compareBadge.style.display = 'none'; 22852 } 22853 } 22854 22855 /** 22856 * Handle add to cart 22857 */ 22858 async function handleAddToCart() { 22859 if (!activeProductData) return; 22860 22861 const button = document.getElementById('pca-add-cart'); 22862 if (!button) return; 22863 22864 // If product has variants, redirect to detail page 22865 if (activeProductData.hasVariants) { 22866 window.location.href = activeProductData.url; 22867 return; 22868 } 22869
22870 // Check stock 22871 if (activeProductData.stockStatus === 'out') { 22872 showToast('Produk sedang habis', 'warning'); 22873 return; 22874 } 22875 22876 // Show loading state 22877 button.classList.add('loading'); 22878 button.disabled = true; 22879 22880 // CSRF Token Helper 22881 function getCsrfToken() { 22882 return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 22883 } 22884 22885 try { 22886 // Call cart API with proper format 22887 const response = await fetch('/api/cart/add.php', { 22888 method: 'POST', 22889 headers: { 22890 'Content-Type': 'application/json', 22891 'X-CSRF-Token': getCsrfToken() 22892 }, 22893 body: JSON.stringify({ 22894 csrf_token: getCsrfToken(), 22895 product_id: activeProductData.id, 22896 qty: 1, // Use 'qty' not 'quantity' 22897 variant_id: 0, // Default variant 22898 meta: { 22899 name: activeProductData.name, 22900 image_url: activeProductData.image 22901 } 22902 }), 22903 credentials: 'same-origin' 22904 }); 22905 22906 const result = await response.json(); 22907 22908 // Check both response status and result.ok 22909 if (!response.ok || !result.ok) { 22910 throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang'); 22911 } 22912 22913 // Success animation 22914 button.classList.remove('loading'); 22915 button.classList.add('success'); 22916 22917 // Update cart badge 22918 updateCartBadge(); 22919 22920 // PlazaAnalytics tracking 22921 if (window.PlazaAnalytics) { 22922 const _slug = (activeProductData.url || '').replace(/.*\/product\//, ''); 22923 window.PlazaAnalytics.track('add_to_cart', { slug: _slug }); 22924 } 22925 22926 // Show success toast 22927 showToast('Produk ditambahkan ke keranjang', 'success'); 22928 22929 // Google Analytics 22930 if (typeof gtag === 'function') { 22931 gtag('event', 'add_to_cart', { 22932 event_category: 'Ecommerce', 22933 event_label: 'Quick Add to Cart', 22934 items: [{ 22935 item_id: activeProductData.id, 22936 item_name: activeProductData.name, 22937 price: activeProductData.price, 22938 quantity: 1 22939 }] 22940 }); 22941 } 22942 22943 // Close sheet after short delay 22944 setTimeout(() => { 22945 closeBottomSheet(); 22946 button.classList.remove('success'); 22947 button.disabled = false; 22948 }, 1000); 22949 22950 } catch (error) { 22951 console.error('Add to cart error:', error); 22952 button.classList.remove('loading'); 22953 button.disabled = false; 22954 showToast(error.message || 'Terjadi kesalahan', 'error'); 22955 } 22956 } 22957 22958 /** 22959 * Handle add to compare 22960 */ 22961 async function handleAddToCompare() { 22962 if (!activeProductData) return; 22963 22964 const button = document.getElementById('pca-add-compare'); 22965 if (!button) return; 22966 22967 // Check if already in compare 22968 const isActive = button.classList.contains('active'); 22969 22970 // Check 5-product limit before adding 22971 if (!isActive) { 22972 const mainBadge = document.getElementById('compareCount'); 22973 const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0; 22974 22975 if (currentCount >= 5) { 22976 showToast('Maksimal 5 produk untuk perbandingan', 'warning'); 22977 return; 22978 } 22979 } 22980 22981 // Show loading state 22982 button.classList.add('loading'); 22983 button.disabled = true; 22984 22985 try { 22986 if (isActive) { 22987 // Remove from compare 22988 await removeFromCompare(activeProductData.id); 22989 showToast('Produk dihapus dari perbandingan', 'success'); 22990 } else { 22991 // Add to compare 22992 await addToCompare(activeProductData.id); 22993 showToast('Produk ditambahkan ke perbandingan', 'success'); 22994 22995 // Trigger fly animation 22996 triggerFlyToCompareAnimation(); 22997 } 22998 22999 // Success animation 23000 button.classList.remove('loading'); 23001 button.classList.add('success'); 23002 23003 // Update compare button state 23004 updateCompareButtonState(activeProductData.id); 23005 23006 // Close sheet after short delay 23007 setTimeout(() => { 23008 closeBottomSheet(); 23009 button.classList.remove('success'); 23010 button.disabled = false;
23011 }, 800); 23012 23013 } catch (error) { 23014 console.error('Compare error:', error); 23015 button.classList.remove('loading'); 23016 button.disabled = false; 23017 showToast(error.message || 'Terjadi kesalahan', 'error'); 23018 } 23019 } 23020 23021 /** 23022 * Add to compare (calls existing API) 23023 */ 23024 async function addToCompare(productId) { 23025 const response = await fetch('/api/compare/add.php', { 23026 method: 'POST', 23027 headers: { 23028 'Content-Type': 'application/json', 23029 }, 23030 body: JSON.stringify({ 23031 product_id: productId, 23032 variant_id: 0 23033 }), 23034 credentials: 'same-origin' 23035 }); 23036 23037 const result = await response.json(); 23038 23039 if (!result.success) { 23040 throw new Error(result.error || 'Gagal menambahkan produk'); 23041 } 23042 23043 // Trigger compare list reload if function exists 23044 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23045 await window.productCompare.loadCompareList(); 23046 } 23047 23048 return result; 23049 } 23050 23051 /** 23052 * Remove from compare (calls existing API) 23053 */ 23054 async function removeFromCompare(productId) { 23055 const response = await fetch('/api/compare/remove.php', { 23056 method: 'POST', 23057 headers: { 23058 'Content-Type': 'application/json', 23059 }, 23060 body: JSON.stringify({ 23061 product_id: productId 23062 }), 23063 credentials: 'same-origin' 23064 }); 23065 23066 const result = await response.json(); 23067 23068 if (!result.success) { 23069 throw new Error(result.error || 'Gagal menghapus produk'); 23070 } 23071 23072 // Trigger compare list reload if function exists 23073 if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') { 23074 await window.productCompare.loadCompareList(); 23075 } 23076 23077 return result; 23078 } 23079 23080 /** 23081 * Update cart badge 23082 */ 23083 function updateCartBadge() { 23084 // Try to call global cart update function if exists 23085 if (typeof window.updateCartBadge === 'function') { 23086 window.updateCartBadge(); 23087 } else { 23088 // Fallback: increment badge manually 23089 const badge = document.getElementById('cartCount'); 23090 if (badge) { 23091 const currentCount = parseInt(badge.textContent) || 0; 23092 badge.textContent = currentCount + 1; 23093 badge.classList.add('active'); 23094 } 23095 } 23096 } 23097 23098 /** 23099 * Trigger fly to compare animation 23100 */ 23101 function triggerFlyToCompareAnimation() { 23102 const compareBtn = document.getElementById('compareBtn'); 23103 if (!compareBtn || !activeProductData) return; 23104 23105 // Create flying element 23106 const flyingElement = document.createElement('div'); 23107 flyingElement.className = 'flying-product'; 23108 flyingElement.style.cssText = ` 23109 position: fixed; 23110 width: 60px; 23111 height: 60px; 23112 z-index: 99999; 23113 pointer-events: none; 23114 `; 23115 23116 flyingElement.innerHTML = ` 23117 <img src="${activeProductData.image}" 23118 style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);"> 23119 `; 23120 23121 // Position at bottom sheet 23122 const sheetRect = bottomSheet.getBoundingClientRect(); 23123 const targetRect = compareBtn.getBoundingClientRect(); 23124 23125 flyingElement.style.top = `${sheetRect.top + 20}px`; 23126 flyingElement.style.left = `${sheetRect.left + 20}px`; 23127 23128 document.body.appendChild(flyingElement); 23129 23130 // Animate to target 23131 requestAnimationFrame(() => { 23132 flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; 23133 flyingElement.style.top = `${targetRect.top + 10}px`; 23134 flyingElement.style.left = `${targetRect.left + 10}px`; 23135 flyingElement.style.transform = 'scale(0.3)'; 23136 flyingElement.style.opacity = '0'; 23137 }); 23138 23139 // Clean up 23140 setTimeout(() => { 23141 if (flyingElement.parentNode) { 23142 flyingElement.parentNode.removeChild(flyingElement); 23143 } 23144 }, 800); 23145 } 23146 23147 /** 23148 * Format price as Rupiah 23149 */ 23150 function formatPrice(price) { 23151 if (!price || price === 0) return 'Rp 0'; 23152 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 23153 } 23154 23155 /** 23156 * Escape HTML 23157 */ 23158 function escapeHtml(text) { 23159 const div = document.createElement('div'); 23160 div.textContent = text; 23161 return div.innerHTML; 23162 } 23163 23164 /** 23165 * Show toast notification (uses global showToast if available) 23166 */ 23167 function showToast(message, type = 'info') { 23168 if (typeof window.showToast === 'function') { 23169 window.showToast({ 23170 title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info', 23171 message: message, 23172 type: type, 23173 duration: 3000 23174 }); 23175 } else { 23176 // Fallback: console log 23177 console.log(`[${type.toUpperCase()}] ${message}`); 23178 } 23179 } 23180 23181 /** 23182 * Expose functions for external access 23183 */ 23184 window.ProductCardActions = {
23185 openSheet: openBottomSheet, 23186 closeSheet: closeBottomSheet, 23187 updateCompareState: updateCompareButtonState 23188 }; 23189 23190})(); 23191 23192</script>
23192 23193
23193<script data-inlined="product-reviews.js"> 23194/** 23195 * Product Reviews - Frontend JavaScript 23196 * Handles review display, submission, and helpful voting 23197 */ 23198 23199const ProductReviews = (function() { 23200 'use strict'; 23201 23202 let currentPage = 1; 23203 let currentSort = 'helpful'; 23204 let productId = null; 23205 let isLoading = false; 23206 23207 /** 23208 * Public API: Initialize reviews for a product 23209 */ 23210 function init(pid) { 23211 productId = pid; 23212 23213 if (!productId) { 23214 console.warn('Product ID not provided to ProductReviews.init()'); 23215 return; 23216 } 23217 23218 initializeReviewTab(); 23219 loadReviews(); 23220 } 23221 23222 /** 23223 * Initialize review tab functionality 23224 */ 23225 function initializeReviewTab() { 23226 // Tab switching 23227 const tabButtons = document.querySelectorAll('.tab-btn'); 23228 tabButtons.forEach(btn => { 23229 btn.addEventListener('click', function() { 23230 const tabName = this.dataset.tab; 23231 switchTab(tabName); 23232 }); 23233 }); 23234 23235 // Sort dropdown 23236 const sortSelect = document.getElementById('review-sort'); 23237 if (sortSelect) { 23238 sortSelect.addEventListener('change', function() { 23239 currentSort = this.value; 23240 currentPage = 1; 23241 loadReviews(); 23242 }); 23243 } 23244 23245 // Load more button
23246 const loadMoreBtn = document.getElementById('load-more-reviews'); 23247 if (loadMoreBtn) { 23248 loadMoreBtn.addEventListener('click', function() { 23249 currentPage++; 23250 loadReviews(true); // Append mode 23251 }); 23252 } 23253 23254 // Write review button 23255 const writeReviewBtn = document.getElementById('write-review-btn'); 23256 if (writeReviewBtn) { 23257 writeReviewBtn.addEventListener('click', showReviewModal); 23258 } 23259 } 23260 23261 /** 23262 * Switch between product tabs 23263 */ 23264 function switchTab(tabName) { 23265 // Update button states 23266 document.querySelectorAll('.tab-btn').forEach(btn => { 23267 btn.classList.toggle('active', btn.dataset.tab === tabName); 23268 }); 23269 23270 // Update content visibility 23271 document.querySelectorAll('.tab-content').forEach(content => { 23272 content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none'; 23273 }); 23274 } 23275 23276 /** 23277 * Load reviews from API 23278 */ 23279 async function loadReviews(append = false) { 23280 if (isLoading) return; 23281 isLoading = true; 23282 23283 const container = document.getElementById('reviews-list');
23284 const loadMoreBtn = document.getElementById('load-more-reviews'); 23285 23286 // Check if review section exists (product might not have reviews section) 23287 if (!container) { 23288 console.log('Review section not found - product may not have reviews yet'); 23289 isLoading = false; 23290 return; 23291 } 23292 23293 try { 23294 if (!append) { 23295 container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>'; 23296 } else { 23297 loadMoreBtn.disabled = true; 23298 loadMoreBtn.textContent = 'Memuat...'; 23299 } 23300 23301 const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`); 23302 const data = await response.json(); 23303 23304 if (!data.success) { 23305 throw new Error(data.message || 'Gagal memuat ulasan'); 23306 } 23307 23308 // Update statistics (only on initial load) 23309 if (!append && data.stats) { 23310 updateReviewStats(data.stats); 23311 } 23312 23313 // Render reviews 23314 if (!append) { 23315 container.innerHTML = ''; 23316 } 23317 23318 const reviews = data.reviews || []; 23319 23320 if (reviews.length === 0 && !append) { 23321 container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>'; 23322 } else { 23323 reviews.forEach(review => { 23324 container.appendChild(createReviewElement(review)); 23325 }); 23326 } 23327 23328 // Update load more button 23329 if (loadMoreBtn) { 23330 if (data.pagination.has_more) { 23331 loadMoreBtn.style.display = 'block'; 23332 loadMoreBtn.disabled = false; 23333 loadMoreBtn.textContent = 'Muat Lebih Banyak'; 23334 } else { 23335 loadMoreBtn.style.display = 'none'; 23336 } 23337 } 23338 23339 } catch (error) { 23340 console.error('Load reviews error:', error); 23341 container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>'; 23342 } finally { 23343 isLoading = false; 23344 } 23345 } 23346 23347 /** 23348 * Update review statistics display 23349 */ 23350 function updateReviewStats(stats) { 23351 const avgRatingEl = document.getElementById('avg-rating'); 23352 const totalReviewsEl = document.getElementById('total-reviews'); 23353 const ratingBarsContainer = document.getElementById('rating-bars'); 23354 23355 if (avgRatingEl) { 23356 avgRatingEl.textContent = stats.average_rating.toFixed(1); 23357 } 23358 23359 if (totalReviewsEl) { 23360 totalReviewsEl.textContent = `${stats.total_reviews} ulasan`; 23361 } 23362 23363 // Update rating breakdown bars 23364 if (ratingBarsContainer) { 23365 ratingBarsContainer.innerHTML = ''; 23366 stats.rating_breakdown.forEach(item => { 23367 const bar = document.createElement('div'); 23368 bar.className = 'rating-bar'; 23369 bar.innerHTML = ` 23370 <span class="rating-label">${item.rating}â </span> 23371 <div class="bar-container"> 23372 <div class="bar-fill" style="width: ${item.percentage}%"></div> 23373 </div> 23374 <span class="rating-count">${item.count}</span> 23375 `; 23376 ratingBarsContainer.appendChild(bar); 23377 }); 23378 } 23379 23380 // Update tab count 23381 const reviewTab = document.querySelector('[data-tab="reviews"]'); 23382 if (reviewTab && stats.total_reviews > 0) { 23383 reviewTab.textContent = `Ulasan (${stats.total_reviews})`; 23384 } 23385 } 23386 23387 /** 23388 * Create review element 23389 */ 23390 function createReviewElement(review) { 23391 const div = document.createElement('div'); 23392 div.className = 'review-item'; 23393 div.dataset.reviewId = review.ID; 23394 23395 // Build stars 23396 const stars = 'â '.repeat(review.RATING) + 'â'.repeat(5 - review.RATING); 23397 23398 // Build images if available 23399 let imagesHtml = ''; 23400 if (review.images && review.images.length > 0) { 23401 imagesHtml = '<div class="review-images">'; 23402 review.images.forEach(img => { 23403 imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`; 23404 }); 23405 imagesHtml += '</div>'; 23406 } 23407 23408 // Verified purchase badge 23409 const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 23410 ? '<span class="verified-badge">â Pembelian Terverifikasi</span>' 23411 : ''; 23412 23413 // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME 23414 const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous'; 23415 const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 23416 ? '<span class="anonymous-badge" title="Nama pengguna disamark
23416an untuk privasi">ð</span>' 23417 : ''; 23418 23419 div.innerHTML = ` 23420 <div class="review-header"> 23421 <div class="review-author"> 23422 <strong>${escapeHtml(displayName)}</strong> 23423 ${anonymousIndicator} 23424 ${verifiedBadge} 23425 </div> 23426 <div class="review-rating"> 23427 <span class="stars">${stars}</span> 23428 </div> 23429 </div> 23430 <div class="review-date">${review.created_ago}</div> 23431 <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div> 23432 ${imagesHtml} 23433 <div class="review-actions"> 23434 <button class="btn-helpful" data-review-id="${review.ID}"> 23435 <span class="helpful-icon">ð</span> 23436 <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span> 23437 </button> 23438 </div> 23439 `; 23440 23441 // Add helpful button listener 23442 const helpfulBtn = div.querySelector('.btn-helpful'); 23443 helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID)); 23444 23445 return div; 23446 } 23447 23448 /** 23449 * Get CSRF token from meta tag 23450 */ 23451 function getCsrfToken() { 23452 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23453 return metaTag ? metaTag.getAttribute('content') : ''; 23454 } 23455 23456 /** 23457 * Mark review as helpful 23458 */ 23459 async function markAsHelpful(reviewId) { 23460 // Check if logged in 23461 const isLoggedIn = document.body.classList.contains('logged-in') || 23462 document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 23463 23464 if (!isLoggedIn) { 23465 showModal({ 23466 title: 'Login Diperlukan', 23467 message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.', 23468 icon: 'info', 23469 buttons: [ 23470 { text: 'Batal', className: 'modal-btn-secondary' }, 23471 { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' } 23472 ] 23473 }); 23474 return; 23475 } 23476 23477 // Get current button element 23478 const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`); 23479 const helpfulBtn = reviewItem?.querySelector('.btn-helpful'); 23480 23481 if (!helpfulBtn) return; 23482 23483 // Disable button during request 23484 helpfulBtn.disabled = true; 23485 const originalText = helpfulBtn.querySelector('.helpful-text').textContent; 23486 23487 try { 23488 const response = await fetch('/api/reviews/helpful.php', { 23489 method: 'POST', 23490 headers: { 23491 'Content-Type': 'application/json', 23492 'X-CSRF-Token': getCsrfToken() 23493 }, 23494 credentials: 'same-origin', 23495 body: JSON.stringify({ 23496 csrf_token: getCsrfToken(), 23497 review_id: reviewId 23498 }) 23499 }); 23500 23501 const data = await response.json(); 23502 23503 if (data.success) { 23504 // Update button count immediately based on action 23505 const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0); 23506 const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1; 23507 helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`; 23508 23509 // Show success message 23510 showToast(data.message, 'success'); 23511 23512 // Re-enable button 23513 helpfulBtn.disabled = false; 23514 } else { 23515 showToast(data.message, 'error'); 23516 helpfulBtn.disabled = false; 23517 } 23518 23519 } catch (error) { 23520 console.error('Mark helpful error:', error); 23521 showToast('Gagal memberikan vote. Silakan coba lagi.', 'error'); 23522 helpfulBtn.disabled = false; 23523 } 23524 } 23525 23526 /** 23527 * Show review submission modal (placeholder) 23528 */ 23529 function showReviewModal() { 23530 // TODO: Implement review modal 23531 // For now, redirect to account page 23532 window.location.href = '/account/orders'; 23533 } 23534 23535 /** 23536 * Show toast notification 23537 */ 23538 function showToast(message, type = 'info') { 23539 const toast = document.createElement('div'); 23540 toast.className = `review-toast toast-${type}`; 23541 toast.textContent = message; 23542 23543 document.body.appendChild(toast); 23544 23545 setTimeout(() => { 23546 toast.classList.add('show'); 23547 }, 100); 23548 23549 setTimeout(() => { 23550 toast.classList.remove('show'); 23551 setTimeout(() => toast.remove(), 300); 23552 }, 3000); 23553 } 23554 23555 /** 23556 * Show modal dialog 23557 * @param {Object} options - Modal options 23558 * @param {string} options.title - Modal title 23559 * @param {string} options.message - Modal message 23560 * @param {string} options.icon - Icon type (info, success, warning, danger) 23561 * @param {Array} options.buttons - Array of button objects {text, className, callback} 23562 */ 23563 function showModal(options) { 23564 const {
23565 title = 'Konfirmasi', 23566 message = '', 23567 icon = 'info', 23568 buttons = [{ text: 'OK', className: 'modal-btn-primary' }] 23569 } = options; 23570 23571 // Icon emoji mapping 23572 const iconEmojis = { 23573 info: 'â¹ï¸', 23574 success: 'â', 23575 warning: 'â ï¸', 23576 danger: 'â' 23577 }; 23578 23579 // Create modal HTML 23580 const modalHTML = ` 23581 <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;"> 23582 <div class="modal-dialog"> 23583 <div class="modal-header"> 23584 <h3 class="modal-title"> 23585 <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span> 23586 ${escapeHtml(title)} 23587 </h3> 23588 </div> 23589 <div class="modal-body"> 23590 <p class="modal-message">${escapeHtml(message)}</p> 23591 <div class="modal-actions"> 23592 ${buttons.map((btn, index) => 23593 `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>` 23594 ).join('')} 23595 </div> 23596 </div> 23597 </div> 23598 </div> 23599 `; 23600 23601 // Append to body 23602 const tempDiv = document.createElement('div'); 23603 tempDiv.innerHTML = modalHTML; 23604 const modal = tempDiv.firstElementChild; 23605 document.body.appendChild(modal); 23606 23607 // Add event listeners to buttons 23608 buttons.forEach((btn, index) => { 23609 const btnElement = modal.querySelector(`[data-index="${index}"]`); 23610 btnElement.addEventListener('click', () => { 23611 closeModal(modal); 23612 if (btn.callback && typeof btn.callback === 'function') { 23613 btn.callback(); 23614 } 23615 }); 23616 }); 23617 23618 // Close on overlay click 23619 modal.addEventListener('click', (e) => { 23620 if (e.target === modal) { 23621 closeModal(modal); 23622 } 23623 }); 23624 23625 // Show modal with animation 23626 setTimeout(() => modal.classList.add('show'), 10); 23627 } 23628 23629 /** 23630 * Close modal dialog 23631 */ 23632 function closeModal(modal) { 23633 modal.classList.remove('show'); 23634 setTimeout(() => modal.remove(), 200); 23635 } 23636 23637 /** 23638 * Escape HTML for XSS protection 23639 */ 23640 function escapeHtml(text) { 23641 const div = document.createElement('div'); 23642 div.textContent = text; 23643 return div.innerHTML; 23644 } 23645 23646 /** 23647 * Get product ID from URL 23648 */ 23649 function getProductIdFromUrl() { 23650 const match = window.location.pathname.match(/\/product\/(\d+)/); 23651 return match ? match[1] : null; 23652 } 23653 23654 // Public API 23655 return { 23656 init: init 23657 }; 23658 23659})(); 23660 23661</script>
23661 23662
23662<script data-inlined="review-submission.js"> 23663/** 23664 * Review Submission Modal & Handler 23665 * Handles review form display, validation, and submission 23666 */ 23667 23668(function() { 23669 'use strict'; 23670 23671 let currentProductId = null; 23672 let currentOrderId = null; 23673 let currentSoRefId = null; 23674 let selectedRating = 0; 23675 let uploadedImages = []; 23676 23677 // Get CSRF token from meta tag 23678 function getCsrfToken() { 23679 const metaTag = document.querySelector('meta[name="csrf-token"]'); 23680 return metaTag ? metaTag.getAttribute('content') : ''; 23681 } 23682 23683 // Initialize 23684 function init() { 23685 createReviewModal(); 23686 attachEventListeners(); 23687 loadReviewableOrders(); 23688 } 23689 23690 // Create review modal HTML 23691 function createReviewModal() { 23692 const modalHTML = ` 23693 <div class="modal-overlay" id="review-modal-overlay" style="display:none;"> 23694 <div class="modal-content review-modal"> 23695 <div class="modal-header"> 23696 <h3 class="modal-title">Tulis Ulasan</h3> 23697 <button type="button" class="modal-close" id="review-modal-close" aria-label="Tutup"> 23698 <svg width="20" height="20" viewBox="0 0 24 24"> 23699 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 23700 </svg> 23701 </button> 23702 </div> 23703 23704 <div class="modal-body"> 23705 <form id="review-form" class="review-form"> 23706 <!-- Product Info --> 23707 <div class="review-product-info" id="review-product-info"> 23708 <img id="review-product-image" src="" alt="" class="review-product-thumb"> 23709 <div class="review-product-details"> 23710 <h4 id="review-product-name"></h4> 23711 <p id="review-product-sku"></p> 23712 </div> 23713 </div> 23714 23715 <!-- Rating --> 23716 <div class="form-group"> 23717 <label class="form-label">Rating <span style="color:#ef4444;">*</span></label> 23718 <div class="star-rating-input" id="star-rating-input"> 23719 <button type="button" class="star-btn" data-rating="1">â </button> 23720 <button type="button" class="star-btn" data-rating="2">â </button> 23721 <button type="button" class="star-btn" data-rating="3">â </button> 23722 <button type="button" class="star-btn" data-rating="4">â </button> 23723 <button type="button" class="star-btn" data-rating="5">â </button> 23724 </div> 23725 <p class="rating-label" id="rating-label">Pilih rating Anda</p> 23726 </div> 23727 23728 <!-- Review Text --> 23729 <div class="form-group"> 23730 <label class="form-label" for="review-text">Ulasan Anda <span style="color:#ef4444;">*</span></label> 23731 <textarea 23732 id="review-text" 23733 name="review_text" 23734 class="form-control" 23735 rows="6" 23736 placeholder="Ceritakan pengalaman Anda dengan produk ini..." 23737 minlength="20" 23738 maxlength="5000" 23739 required></textarea> 23740 <p class="form-hint"> 23741 <span id="review-char-count">0</span>/5000 karakter 23742 <span style="color:#059669;font-weight:500;"> 23743 ð° +1,000 point untuk ulasan ⥠100 karakter 23744 </span> 23745 </p> 23746 </div> 23747 23748 <!-- Image Upload --> 23749 <div class="form-group"> 23750 <label class="form-label">Upload Foto (Opsional)</label> 23751 <div class="image-upload-container"> 23752 <input type="file" 23753 id="review-images" 23754 name="images[]" 23755 accept="image/jpeg,image/jpg,image/png,image/webp" 23756 multiple 23757 style="display:none;"> 23758 <button type="button" class="btn-upload-trigger" id="upload-trigger"> 23759 <svg width="24" height="24" viewBox="0 0 24 24">
23760 <path fill="currentColor" d="M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z"/> 23761 </svg> 23762 <span>Pilih Foto</span> 23763 </button> 23764 <p class="form-hint"> 23765 Max 5 foto, format JPG/PNG/WebP, max 2MB per foto 23766 <span style="color:#059669;font-weight:500;"> 23767 ð° +2,000 point untuk ulasan dengan foto 23768 </span> 23769 </p> 23770 </div> 23771 <div class="image-preview-grid" id="image-preview-grid"></div> 23772 </div> 23773 23774 <!-- Point Reward Info --> 23775 <div class="point-reward-info"> 23776 <div class="pri-header"> 23777 <span>ð</span> 23778 <span>Dapatkan Point Reward</span> 23779 </div> 23780 <ul class="pri-list"> 23781 <li>â Ulasan dasar: <strong>5,000 point</strong></li> 23782 <li id="pri-long-text" style="opacity:0.5;">ð Ulasan panjang (â¥100 kata): <strong>+1,000 point</strong></li> 23783 <li id="pri-images" style="opacity:0.5;">ð¸ Dengan foto: <strong>+2,000 point</strong></li> 23784 </ul> 23785 <div class="pri-total"> 23786 Total Reward: <strong id="pri-total-points">5,000 point</strong> 23787 </div> 23788 </div> 23789 23790 <!-- Privacy Option --> 23791 <div class="form-group" style="margin-top:20px;padding:15px;background:#f8fafc;border-radius:8px;"> 23792 <label class="checkbox-label" style="display:flex;align-items:center;cursor:pointer;margin:0;"> 23793 <input type="checkbox" 23794 id="review-anonymous" 23795 name="is_anonymous" 23796 style="margin-right:8px;width:18px;height:18px;cursor:pointer;"> 23797 <span style="font-size:14px;"> 23798 <strong>ð Sembunyikan nama saya</strong> 23799 <span style="color:#64748b;display:block;font-size:13px;margin-top:4px;" id="review-anonymous-preview"> 23800 Nama Anda akan ditampilkan dengan disamarkan untuk menjaga privasi 23801 </span> 23802 </span> 23803 </label> 23804 </div> 23805 23806 <!-- Error Alert --> 23807 <div class="form-error" id="review-form-error" style="display:none;"></div> 23808 23809 <!-- Submit Buttons --> 23810 <div class="modal-footer"> 23811 <button type="button" class="btn btn-secondary" id="review-cancel">Batal</button> 23812 <button type="submit" class="btn btn-primary" id="review-submit"> 23813 Kirim Ulasan 23814 </button> 23815 </div> 23816 </form> 23817 </div> 23818 </div> 23819 </div> 23820 `; 23821 23822 document.body.insertAdjacentHTML('beforeend', modalHTML); 23823 } 23824 23825 // Attach event listeners 23826 function attachEventListeners() { 23827 // Modal triggers - event delegation 23828 document.addEventListener('click', function(e) { 23829 const btn = e.target.closest('.btn-write-review'); 23830 if (btn) { 23831 e.preventDefault(); 23832 openReviewModal( 23833 btn.dataset.productId, 23834 btn.dataset.orderId, 23835 btn.dataset.productName, 23836 btn.dataset.productImage, 23837 btn.dataset.productSku, 23838 btn.dataset.soRefId || null 23839 ); 23840 } 23841 }); 23842 23843 // Close modal 23844 setTimeout(() => { 23845 const closeBtn = document.getElementById('review-modal-close'); 23846 const cancelBtn = document.getElementById('review-cancel');
23847 const overlay = document.getElementById('review-modal-overlay'); 23848 23849 if (closeBtn) { 23850 closeBtn.addEventListener('click', closeReviewModal); 23851 } 23852 if (cancelBtn) { 23853 cancelBtn.addEventListener('click', closeReviewModal); 23854 } 23855 if (overlay) { 23856 overlay.addEventListener('click', function(e) { 23857 if (e.target === overlay) closeReviewModal(); 23858 }); 23859 } 23860 }, 100); 23861 23862 // Star rating 23863 setTimeout(() => { 23864 const starButtons = document.querySelectorAll('.star-btn'); 23865 starButtons.forEach(btn => { 23866 btn.addEventListener('click', function(e) { 23867 e.preventDefault(); 23868 const rating = parseInt(this.dataset.rating); 23869 setRating(rating); 23870 }); 23871 }); 23872 }, 100); 23873 23874 // Character count 23875 setTimeout(() => { 23876 const textarea = document.getElementById('review-text'); 23877 if (textarea) { 23878 textarea.addEventListener('input', function() { 23879 updateCharCount(); 23880 calculatePotentialPoints(); 23881 }); 23882 } 23883 }, 100); 23884 23885 // Image upload 23886 setTimeout(() => { 23887 const uploadTrigger = document.getElementById('upload-trigger'); 23888 const fileInput = document.getElementById('review-images'); 23889 23890 if (uploadTrigger && fileInput) { 23891 uploadTrigger.addEventListener('click', () => fileInput.click()); 23892 fileInput.addEventListener('change', handleImageSelection); 23893 } 23894 }, 100); 23895 23896 // Form submit 23897 setTimeout(() => { 23898 const form = document.getElementById('review-form'); 23899 if (form) { 23900 form.addEventListener('submit', handleSubmit); 23901 } 23902 }, 100); 23903 } 23904 23905 // Open review modal 23906 function openReviewModal(productId, orderId, productName, productImage, productSku, soRefId = null) { 23907 const overlay = document.getElementById('review-modal-overlay'); 23908 if (!overlay) { 23909 console.error('Modal overlay not found!'); 23910 return; 23911 } 23912 23913 currentProductId = productId; 23914 currentOrderId = orderId; 23915 currentSoRefId = soRefId; 23916 selectedRating = 0; 23917 uploadedImages = []; 23918 23919 // Set product info 23920 const nameEl = document.getElementById('review-product-name'); 23921 const imageEl = document.getElementById('review-product-image'); 23922 const skuEl = document.getElementById('review-product-sku'); 23923 23924 if (nameEl) nameEl.textContent = productName; 23925 if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; 23926 if (skuEl) skuEl.textContent = 'SKU: ' + productSku; 23927 23928 // Update privacy preview with actual user name 23929 updatePrivacyPreview(); 23930 23931 // Reset form 23932 const form = document.getElementById('review-form'); 23933 const previewGrid = document.getElementById('image-preview-grid'); 23934 const errorEl = document.getElementById('review-form-error'); 23935 23936 if (form) form.reset(); 23937 if (previewGrid) previewGrid.innerHTML = ''; 23938 if (errorEl) errorEl.style.display = 'none'; 23939 23940 setRating(0); 23941 updateCharCount(); 23942 calculatePotentialPoints(); 23943 23944 // Show modal 23945 overlay.style.display = 'flex'; 23946 overlay.classList.add('show'); 23947 document.body.style.overflow = 'hidden'; 23948 } 23949 23950 // Close review modal 23951 function closeReviewModal() { 23952 const overlay = document.getElementById('review-modal-overlay'); 23953 if (overlay) { 23954 overlay.classList.remove('show'); 23955 setTimeout(() => { 23956 overlay.style.display = 'none'; 23957 }, 200); // Wait for fade animation 23958 } 23959 document.body.style.overflow = ''; 23960 } 23961 23962 // Set star rating 23963 function setRating(rating) { 23964 selectedRating = rating; 23965 const stars = document.querySelectorAll('.star-btn'); 23966 const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; 23967
23968 stars.forEach((star, index) => { 23969 if (index < rating) { 23970 star.classList.add('active'); 23971 } else { 23972 star.classList.remove('active'); 23973 } 23974 }); 23975 23976 document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; 23977 } 23978 23979 // Update character count 23980 function updateCharCount() { 23981 const textarea = document.getElementById('review-text'); 23982 const count = textarea.value.length; 23983 document.getElementById('review-char-count').textContent = count; 23984 } 23985 23986 // Calculate potential points 23987 function calculatePotentialPoints() { 23988 const textarea = document.getElementById('review-text'); 23989 if (!textarea) return; 23990 23991 const charCount = textarea.value.trim().length; 23992 const hasImages = uploadedImages.length > 0; 23993 23994 let totalPoints = 5000; 23995 let longTextActive = charCount >= 100; 23996 let imagesActive = hasImages; 23997 23998 if (longTextActive) totalPoints += 1000; 23999 if (imagesActive) totalPoints += 2000; 24000 24001 // Update UI 24002 const longTextEl = document.getElementById('pri-long-text'); 24003 const imagesEl = document.getElementById('pri-images'); 24004 const totalEl = document.getElementById('pri-total-points'); 24005 24006 if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; 24007 if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; 24008 if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; 24009 } 24010 24011 // Handle image selection 24012 function handleImageSelection(e) { 24013 const files = Array.from(e.target.files); 24014 24015 if (files.length > 5) { 24016 showError('Maksimal 5 foto yang dapat diupload'); 24017 e.target.value = ''; // Reset input 24018 return; 24019 } 24020 24021 uploadedImages = []; 24022 const previewGrid = document.getElementById('image-preview-grid'); 24023 previewGrid.innerHTML = ''; 24024 24025 files.forEach((file, index) => { 24026 // Validate file type 24027 if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { 24028 showError('Format file harus JPG, PNG, atau WebP'); 24029 return; 24030 } 24031 24032 // Validate file size (2MB) 24033 if (file.size > 2 * 1024 * 1024) { 24034 showError('Ukuran file maksimal 2MB'); 24035 return; 24036 } 24037 24038 uploadedImages.push(file); 24039 24040 // Create preview 24041 const reader = new FileReader(); 24042 reader.onload = function(e) { 24043 const previewHTML = ` 24044 <div class="image-preview-item"> 24045 <img src="${e.target.result}" alt="Preview ${index + 1}"> 24046 <button type="button" class="remove-image" data-index="${index}" aria-label="Hapus"> 24047 <svg width="16" height="16" viewBox="0 0 24 24"> 24048 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 24049 </svg> 24050 </button> 24051 </div> 24052 `; 24053 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24054 }; 24055 reader.readAsDataURL(file); 24056 }); 24057 24058 calculatePotentialPoints(); 24059 24060 // Attach remove handlers 24061 setTimeout(() => { 24062 document.querySelectorAll('.remove-image').forEach(btn => { 24063 btn.addEventListener('click', function() { 24064 const index = parseInt(this.dataset.index); 24065 removeImage(index); 24066 }); 24067 }); 24068 }, 100); 24069 } 24070 24071 // Remove image 24072 function removeImage(index) { 24073 uploadedImages.splice(index, 1); 24074 24075 const previewGrid = document.getElementById('image-preview-grid'); 24076 previewGrid.innerHTML = ''; 24077 24078 uploadedImages.forEach((file, idx) => { 24079 const reader = new FileReader(); 24080 reader.onload = function(e) { 24081 const previewHTML = ` 24082 <div class="image-preview-item"> 24083 <img src="${e.target.result}" alt="Preview ${idx + 1}"> 24084 <button type="button" class="remove-image" data-index="${idx}" aria-label="Hapus">
24085 <svg width="16" height="16" viewBox="0 0 24 24"> 24086 <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/> 24087 </svg> 24088 </button> 24089 </div> 24090 `; 24091 previewGrid.insertAdjacentHTML('beforeend', previewHTML); 24092 }; 24093 reader.readAsDataURL(file); 24094 }); 24095 24096 calculatePotentialPoints(); 24097 24098 setTimeout(() => { 24099 document.querySelectorAll('.remove-image').forEach(btn => { 24100 btn.addEventListener('click', function() { 24101 const idx = parseInt(this.dataset.index); 24102 removeImage(idx); 24103 }); 24104 }); 24105 }, 100); 24106 } 24107 24108 // Handle form submission 24109 async function handleSubmit(e) { 24110 e.preventDefault(); 24111 24112 // Validate 24113 if (selectedRating === 0) { 24114 showError('Silakan pilih rating terlebih dahulu'); 24115 return; 24116 } 24117 24118 const reviewText = document.getElementById('review-text').value.trim(); 24119 if (reviewText.length < 20) { 24120 showError('Ulasan minimal 20 karakter'); 24121 return; 24122 } 24123 24124 const submitBtn = document.getElementById('review-submit'); 24125 submitBtn.disabled = true; 24126 24127 try { 24128 // Step 1: Upload images if any 24129 let imageUrls = []; 24130 if (uploadedImages.length > 0) { 24131 submitBtn.textContent = 'Mengupload foto...'; 24132 imageUrls = await uploadImages(); 24133 if (!imageUrls) { 24134 // Upload failed 24135 submitBtn.disabled = false; 24136 submitBtn.textContent = 'Kirim Ulasan'; 24137 return; 24138 } 24139 } 24140 24141 // Step 2: Submit review with image URLs 24142 submitBtn.textContent = 'Mengirim ulasan...'; 24143 24144 const payload = { 24145 csrf_token: getCsrfToken(), 24146 product_id: currentProductId, 24147 order_id: currentOrderId || 0, 24148 so_ref_id: currentSoRefId || undefined, 24149 rating: selectedRating, 24150 review_text: reviewText, 24151 is_anonymous: document.getElementById('review-anonymous').checked, 24152 images: imageUrls 24153 }; 24154 24155 const response = await fetch('/api/reviews/submit.php', { 24156 method: 'POST', 24157 headers: { 24158 'Content-Type': 'application/json', 24159 'X-CSRF-Token': getCsrfToken() 24160 }, 24161 body: JSON.stringify(payload) 24162 }); 24163 24164 const result = await response.json(); 24165 24166 if (result.success) { 24167 showSuccess(result.message || 'Ulasan berhasil dikirim!'); 24168 closeReviewModal(); 24169 24170 // Refresh reviewable orders if on account page 24171 if (typeof loadReviewableOrders === 'function') { 24172 setTimeout(loadReviewableOrders, 500); 24173 } 24174 } else { 24175 showError(result.message || 'Gagal mengirim ulasan'); 24176 } 24177 } catch (error) { 24178 console.error('Submit error:', error); 24179 showError('Terjadi kesalahan saat mengirim ulasan'); 24180 } finally { 24181 submitBtn.disabled = false; 24182 submitBtn.textContent = 'Kirim Ulasan'; 24183 } 24184 } 24185 24186 /** 24187 * Upload images to server 24188 * @returns {Promise<Array|null>} Array of image URLs or null if failed 24189 */ 24190 async function uploadImages() { 24191 if (uploadedImages.length === 0) { 24192 return []; 24193 } 24194 24195 const formData = new FormData(); 24196 uploadedImages.forEach(file => { 24197 formData.append('images[]', file); 24198 }); 24199 24200 try { 24201 const response = await fetch('/api/reviews/upload_images.php', { 24202 method: 'POST', 24203 headers: { 24204 'X-CSRF-Token': getCsrfToken() 24205 }, 24206 body: formData 24207 }); 24208 24209 // Check if response is JSON 24210 const contentType = response.headers.get('content-type'); 24211 if (!contentType || !contentType.includes('application/json')) { 24212 const text = await response.text(); 24213 console.error('Non-JSON response:', text); 24214 showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); 24215 return null; 24216 } 24217 24218 const result = await response.json(); 24219 24220 if (result.success && result.images) { 24221 // Extract URLs from uploaded images 24222 return result.images.map(img => img.url); 24223 } else { 24224 showError(result.message || 'Gagal mengupload foto'); 24225 return null; 24226 } 24227 } catch (error) { 24228 console.error('Upload images error:', error); 24229 showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); 24230 return null; 24231 } 24232 } 24233 24234 // Load reviewable orders (for account page) 24235 async function loadReviewableOrders() { 24236 // Only run if on account/profile page 24237 const container = document.getElementById('reviewable-orders-list'); 24238 const loading = document.getElementById('reviewable-orders-loading'); 24239 const empty = document.getElementById('reviewable-orders-empty'); 24240 24241 if (!container) { 24242 return; 24243 } 24244 24245 // Show loading 24246 if (loading) loading.style.display = 'block'; 24247 if (empty) empty.style.display = 'none'; 24248 container.innerHTML = ''; 24249 24250 try { 24251 const response = await fetch('/api/reviews/reviewable.php'); 24252 const result = await response.json(); 24253 24254 if (loading) loading.style.display = 'none'; 24255 24256 if (result.success && result.data.length > 0) {
24257 renderReviewableOrders(result.data); 24258 if (empty) empty.style.display = 'none'; 24259 } else { 24260 if (empty) empty.style.display = 'block'; 24261 } 24262 } catch (error) { 24263 console.error('Load reviewable orders error:', error); 24264 if (loading) loading.style.display = 'none'; 24265 if (empty) { 24266 empty.innerHTML = '<p>Gagal memuat data. Silakan refresh halaman.</p>'; 24267 empty.style.display = 'block'; 24268 } 24269 } 24270 } 24271 24272 // Render reviewable orders 24273 function renderReviewableOrders(orders) { 24274 const container = document.getElementById('reviewable-orders-list'); 24275 if (!container) return; 24276 24277 let html = ''; 24278 24279 orders.forEach(order => { 24280 html += ` 24281 <div class="reviewable-order-item"> 24282 <img src="${escapeHtml(order.product_image || '/assets/img/no-image.png')}" alt="${escapeHtml(order.product_name)}" class="roi-image"> 24283 <div class="roi-info"> 24284 <h4>${escapeHtml(order.product_name)}</h4> 24285 <p class="roi-meta">Order: ${escapeHtml(order.order_number)}</p> 24286 <p class="roi-meta">Dibeli: ${escapeHtml(order.order_date)}</p> 24287 </div> 24288 <button type="button" 24289 class="btn btn-primary btn-write-review" 24290 data-product-id="${order.product_id}" 24291 data-order-id="${order.order_id || ''}" 24292 data-so-ref-id="${escapeHtml(order.so_ref_id || '')}" 24293 data-product-name="${escapeHtml(order.product_name)}" 24294 data-product-image="${escapeHtml(order.product_image || '')}" 24295 data-product-sku="${escapeHtml(order.product_sku || '')}"> 24296 âï¸ Tulis Ulasan 24297 </button> 24298 </div> 24299 `; 24300 }); 24301 24302 container.innerHTML = html; 24303 } 24304 24305 // Escape HTML for XSS protection 24306 function escapeHtml(text) { 24307 const div = document.createElement('div'); 24308 div.textContent = text; 24309 return div.innerHTML; 24310 } 24311 24312 // Show error 24313 function showError(message) { 24314 const errorEl = document.getElementById('review-form-error'); 24315 errorEl.textContent = message; 24316 errorEl.style.display = 'block'; 24317 24318 setTimeout(() => { 24319 errorEl.style.display = 'none'; 24320 }, 5000); 24321 } 24322 24323 // Show success 24324 function showSuccess(message) { 24325 // Create toast notification 24326 const toast = document.createElement('div'); 24327 toast.className = 'toast-notification success'; 24328 toast.textContent = message; 24329 toast.style.cssText = 'position:fixed;top:20px;right:20px;background:#059669;color:white;padding:16px 24px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);z-index:10000;font-size:14px;font-weight:500;'; 24330 24331 document.body.appendChild(toast); 24332 24333 setTimeout(() => { 24334 toast.style.opacity = '0'; 24335 toast.style.transition = 'opacity 0.3s'; 24336 setTimeout(() => toast.remove(), 300); 24337 }, 3000); 24338 } 24339 24340 // Mask user name for privacy preview (like PHP maskReviewerName) 24341 function maskUserName(fullName) { 24342 if (!fullName || fullName.trim() === '') { 24343 return 'Anonymous'; 24344 } 24345 24346 const words = fullName.trim().split(/\s+/); 24347 const maskedWords = words.map(word => { 24348 const len = word.length; 24349 if (len <= 1) return word; 24350 return word.charAt(0) + '*'.repeat(len - 1); 24351 }); 24352 24353 return maskedWords.join(' '); 24354 } 24355 24356 // Update privacy preview text with user's actual masked name 24357 function updatePrivacyPreview() { 24358 const previewEl = document.getElementById('review-anonymous-preview'); 24359 if (!previewEl) return; 24360 24361 const userName = window.userFullName || ''; 24362 if (userName) { 24363 const maskedName = maskUserName(userName); 24364 previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; 24365 } else { 24366 previewEl.textContent = 'Nama Anda akan ditampilkan de
24366ngan disamarkan untuk menjaga privasi'; 24367 } 24368 } 24369 24370 // Initialize when DOM ready 24371 if (document.readyState === 'loading') { 24372 document.addEventListener('DOMContentLoaded', init); 24373 } else { 24374 init(); 24375 } 24376 24377 // Export openReviewModal to global scope for use in other scripts 24378 window.openReviewModal = openReviewModal; 24379 24380})(); 24381 24382</script>
24382 24383 <style> 24384/* ========================================================= 24385 â¡ FIXED FOOTER â RED ELEGANT MODE 24386========================================================= */ 24387 24388.sbo-top-fixed-footer{ 24389 position: fixed; 24390 left: 0; 24391 right: 0; 24392 bottom: 0; 24393 z-index: 999; 24394 display: flex; 24395 align-items: center; 24396 justify-content: space-around; 24397 gap: 8px; 24398 padding: 11px 12px calc(11px + env(safe-area-inset-bottom)); 24399 overflow: hidden; 24400 background: radial-gradient(circle at 50% 0%, #000000, rgb(0 0 0 / 92%) 58%, rgb(0 0 0 / 98%)), linear-gradient(180deg, rgba(25, 0, 0, 0.92), rgb(0 0 0 / 98%)); 24401 backdrop-filter: blur(12px); 24402 border-top: 1px solid rgb(143 1 1); 24403 border-radius: 18px 18px 0 0; 24404 box-shadow: 0 -12px 30px #410000, inset 0 1px 0 rgba(255, 255, 255, .05); 24405 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 24406} 24407 24408/* scanline */ 24409.sbo-top-fixed-footer::before{ 24410 content:""; 24411 position:absolute; 24412 inset:0; 24413 pointer-events:none; 24414 background: 24415 repeating-linear-gradient( 24416 0deg, 24417 rgba(0,0,0,.015), 24418 rgba(0,0,0,.015) 1px, 24419 transparent 1px, 24420 transparent 6px 24421 ); 24422 opacity:.85; 24423 mix-blend-mode:overlay; 24424} 24425 24426/* shine sweep */ 24427.sbo-top-fixed-footer::after{ 24428 content:""; 24429 position:absolute; 24430 top:0; 24431 left:-160%; 24432 width:60%; 24433 height:100%; 24434 background: linear-gradient(120deg, transparent, rgba(255, 90, 90, 0.25), transparent); 24435 transform: skewX(-25deg); 24436 animation: footerFinalShine 3.8s infinite; 24437 mix-blend-mode: screen; 24438 pointer-events:none; 24439} 24440 24441@keyframes footerFinalShine{ 24442 0%{left:-160%;} 24443 68%{left:160%;} 24444 100%{left:160%;} 24445} 24446 24447/* ========================================================= 24448 ITEM 24449========================================================= */ 24450 24451.sbo-top-fixed-footer a{ 24452 flex:1; 24453 max-width:110px; 24454 24455 display:flex; 24456 flex-direction:column; 24457 align-items:center; 24458 justify-content:center; 24459 gap:6px; 24460 24461 padding: 8px 6px; 24462 24463 text-decoration:none; 24464 text-align:center; 24465 24466 color: rgba(171 0 0); 24467 font-size: 12px; 24468 font-weight: 800; 24469 letter-spacing: .25px; 24470 24471 border-radius: 14px; 24472 position:relative; 24473 z-index:1; 24474 24475 transition: all .18s ease; 24476} 24477 24478.sbo-top-fixed-footer a:hover{ 24479 color:#ffffff; 24480 background: rgba(255, 80, 80, 0.08); 24481 filter: drop-shadow(0 0 12px rgba(255, 80, 80, 0.15)); 24482 transform: translateY(-1px); 24483} 24484 24485.sbo-top-fixed-footer a:active{ 24486 transform: translateY(0) scale(.99); 24487} 24488 24489/* ========================================================= 24490 ICON 24491========================================================= */ 24492 24493.sbo-top-fixed-footer img{ 24494 width:26px; 24495 height:26px; 24496 transition: .18s ease; 24497 filter: drop-shadow(0 0 8px rgba(255, 80, 80, 0.35)); 24498} 24499 24500.sbo-top-fixed-footer a:hover img{ 24501 transform: scale(1.08); 24502 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.6)); 24503} 24504 24505/* ========================================================= 24506 HUD CORNERS 24507========================================================= */ 24508 24509.sbo-top-fixed-footer .hud-corner{ 24510 position:absolute; 24511 width:18px;height:18px; 24512 pointer-events:none; 24513 border: 1px solid rgba(255, 90, 90, 0.5); 24514 filter: drop-shadow(0 0 10px rgba(255, 80, 80, 0.2)); 24515 opacity:.85; 24516} 24517 24518.sbo-top-fixed-footer .hud-tl{top:8px;left:10px;border-right:none;border-bottom:none;border-radius:10px 0 0 0;} 24519.sbo-top-fixed-footer .hud-tr{top:8px;right:10px;border-left:none;border-bottom:none;border-radius:0 10px 0 0;} 24520.sbo-top-fixed-footer .hud-bl{bottom:8px;left:10px;border-right:none;border-top:none;border-radius:0 0 0 10px;} 24521.sbo-top-fixed-footer .hud-br{bottom:8px;right:10px;border-left:none;border-top:none;border-radius:0 0 10px 0;} 24522 24523/* ========================================================= 24524 CENTER CTA 24525========================================================= */ 24526 24527.sbo-top-fixed-footer .tada{ 24528 color: #ffffff; 24529 background: radial-gradient(circle at 50% 0%, #f52b2b, rgba(0, 0, 0, 0) 70%), rgba(255, 60, 60, 0.08); 24530 border: 1px solid rgba(255, 90, 90, 0.35); 24531 box-shadow: 0 0 18px #f52b2b, inset 0 0 16px rgba(255, 60, 60, 0.15); 24532} 24533 24534.sbo-top-fixed-footer .tada img{ 24535 width:30px; 24536 height:30px; 24537 filter: drop-shadow(0 0 16px rgba(255, 80, 80, 0.65)); 24538} 24539 24540/* pulse */ 24541.sbo-top-fixed-footer .tada::before{ 24542 content:""; 24543 position:absolute; 24544 inset:-12px; 24545 border-radius:999px; 24546 background: 24547 radial-gradient(circle, #000000, transparent 70%), 24548 radial-gradient(circle, rgba(255, 120, 120, 0.08), transparent 72%); 24549 filter: blur(12px); 24550 opacity:.55; 24551 animation: corePulse 2.9s ease-in-out infinite; 24552 z-index:-1; 24553} 24554 24555@keyframes corePulse{ 24556 0%,100%{opacity:.35; transform:scale(.99);} 24557 50%{opacity:.75; transform:scale(1.02);} 24558} 24559 24560/* ACTIVE */ 24561.sbo-top-fixed-footer a.is-active{ 24562 color:#ffffff; 24563 background: rgba(255, 80, 80, 0.1); 24564 box-shadow: inset 0 0 0 1px rgba(255, 90, 90, 0.25); 24565} 24566 24567.sbo-top-fixed-footer a.is-active img{ 24568 filter: drop-shadow(0 0 18px rgba(255, 80, 80, 0.8)); 24569} 24570 24571/* RESPONSIVE */ 24572@media (max-width:500px){ 24573 .sbo-top-fixed-footer{ 24574 padding: 10px 10px calc(10px + env(safe-area-inset-bottom)); 24575 border-radius: 16px 16px 0 0; 24576 gap:6px; 24577 } 24578 .sbo-top-fixed-footer a{ 24579 font-size:11px; 24580 padding:7px 5px; 24581 gap:5px; 24582 max-width:104px; 24583 } 24584 .sbo-top-fixed-footer img{width:22px;height:22px;} 24585 .sbo-top-fixed-footer .tada img{width:26px;height:26px;} 24586} 24587</style> 24588<div class="sbo-top-fixed-footer"> 24589 <a href="https://slot-gacor.loklokpro.com/" target="_blank" rel="nofollow noopener"> 24590 <img src="https://imgmahasuhu.io/images/2026/05/13/iconbonus.png" alt="BONUS IWANTOGEL"> 24591 Promosi 24592 </a> 24593 <a href="https://slot-gacor.loklokpro.com/" target="_blank" rel="nofollow noopener"> 24594 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlogin.png" alt="LOGIN IWANTOGEL"> 24595 Login 24596 </a> 24597 <a href="https://slot-gacor.loklokpro.com/" target="_blank" rel="nofollow noopener" class="tada"> 24598 <img src="https://imgmahasuhu.io/images/2026/05/13/icondaftar.webp" alt="DAFTAR IWANTOGEL"> 24599 Daftar 24600 </a> 24601 <a href="https://slot-gacor.loklokpro.com/" target="_blank" rel="nofollow noopener"> 24602 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlinkalternatif.png" alt="LINK ALTERNATIF IWANTOGEL"> 24603 Alternatif 24604 </a> 24605 <a href="https://slot-gacor.loklokpro.com/" target="_blank" rel="nofollow noopener"> 24606 <img src="https://imgmahasuhu.io/images/2026/05/13/iconlivechat.png" alt="LIVE CHAT IWANTOGEL"> 24607 Live Chat
24608 </a> 24609</div>
24610<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24610
24610
24611<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24611
24611
24612<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24612
24612
24613<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24613
24613
24614<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"51df3b17a746455f940c0b561e8c0c0b","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24614
24614
24615<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"f2ab780bc9a44e979f5cc2ae3699b18e","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24615
24615
24616<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"5b1c76696dc94f7db08316e2401cd14e","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24616
24616
24617<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"870082c1b60640b78980af04df5a944d","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24617
24617
24618<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"9633684236f448248e0f65d50ee7e4af","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24618
24618
24619<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"31d3901e3a0a40c687e8d44f28060815","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24619
24619
24620<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"28460a7e2fdb467898d7a1cc07bfbc86","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24620
24620
24621<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"ca8a0c1b183e4b2b9814a6d6944e175f","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24621
24621
24622<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"0fa09fdd1e6248a88a5f9fcc43d3662c","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24622
24622
24623<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v3d52b47920f24c319d37e2661827c42b1787588026925" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon='{"version":"2024.11.0","token":"660e3e1485aa4d0bb4239656395758ff","r":1}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24623
24623
24624<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"3980ac8b91ca47b4838c0f3feb47c836","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24624
24624
24625<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"85fdc56030fd427cacac1d2d2af9360b","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24625
24625
24626<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"ee5eae759d7945b7a73a25243248a01d","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24626
24626
24627<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"45ca011ed7314b63a70a1eaf042a5365","r":1,"spa":2}' crossorigin="anonymous"></script>
vendor: 1 bytes, line 24627
24627
24628<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"45ca011ed7314b63a70a1eaf042a5365","r":1,"spa":2}' crossorigin="anonymous"></script>
24628 24629</body> 24630</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.