1 2 3 4<!DOCTYPE html> 5<html lang="id"> 6<head> 7 <meta charset="UTF-8"> 8 <title>NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif</title> 9 <meta name="description" content="Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern."> 10 <meta name="robots" content="index,follow"> 11 <link rel="canonical" href="https://sunnypork.com/"> 12 <link rel="amphtml" href="https://sunny-401.sunnypork.com/"> 13 <link rel="alternate" href="https://sunny-401.sunnypork.com/" hreflang="id"> 14 <link rel="alternate" href="https://sunny-401.sunnypork.com/" hreflang="en"> 15 <link rel="alternate" href="https://sunny-401.sunnypork.com/" hreflang="x-default"> 16 <link rel="alternate" hreflang="id-ID" href="https://sunny-401.sunnypork.com/" /> 17 <link rel="alternate" hreflang="en-US" href="https://sunny-401.sunnypork.com/" /> 18 <link rel="alternate" hreflang="en-SG" href="https://sunny-401.sunnypork.com/" /> 19 <link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>
vendor: 67 bytes, lines 19-20
19 20 <script async src="https://www.googletagmanager.com/gtag/js?id=
20G-0LSM1YJSPZ
vendor: 48 bytes, lines 20-21
20" nonce="se3iyzEHMc9FQEB4wSyd0w=="></script> 21
21<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>
21 22 <meta property="og:locale" content="id_ID"> 23 <meta property="og:site_name" content="NAGARP"> 24 <meta property="og:type" content="product"> 25 <meta property="og:title" content="NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif"> 26 <meta property="og:description" content="Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern."> 27 <meta property="og:url" content="https://sunnypork.com/"> 28 <meta property="og:image" content="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg"> 29 <meta property="og:image:alt" content="NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif"> 30<meta property="product:price:amount" content="5000"> 31<meta property="product:price:currency" content="IDR"> 32<meta property="product:availability" content="out of stock"> 33<meta name="twitter:card" content="summary_large_image"> 34<meta name="twitter:title" content="NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif"> 35<meta name="twitter:description" content="Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern."> 36<meta name="twitter:image" content="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg"> 37
38<script type="application/ld+json"> 39 { 40 "@context": "http:\/\/schema.org", 41 "@type": "BreadcrumbList", 42 "itemListElement": [ 43 { 44 "@type": "ListItem", 45 "item": { 46 "@id": "https://sunnypork.com/", 47 "name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif" 48 }, 49 "position": 1 50 } 51 ] 52 } 53 </script>
53 54
54<script type="application/ld+json"> 55 { 56 "@context": "http:\/\/schema.org", 57 "@type": "Product", 58 "name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif", 59 "description": "Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern.", 60 "image": "https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg", 61 "offers": { 62 "@type": "http:\/\/schema.org\/Offer", 63 "price": 9999, 64 "url": "https://sunnypork.com/", 65 "priceCurrency": "IDR", 66 "availability": "http:\/\/schema.org\/InStock" 67 } 68 } 69 </script>
69 70
70<script type="application/ld+json"> 71 { 72 "@context": "http:\/\/schema.org\/", 73 "@type": "WebPage", 74 "speakable": { 75 "@type": "SpeakableSpecification", 76 "cssSelector": [".description"], 77 "xpath": ["\/html\/head\/title"] 78 } 79 } 80 </script>
80 81 <!-- ZS --> 82 <!-- Schema JSON untuk SBO -->
83<script type="application/ld+json"> 84{ 85"@context": "https://schema.org/", 86"@type": "Product", 87"name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif", 88"url": "https://sunnypork.com/", 89"sku": "NAGARP SITUS", 90"image": [ 91 "https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" 92], 93"description": "Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern.", 94"brand": { 95 "@type": "Organization", 96 "name": "NAGARP ONLINE" 97}, 98"offers": { 99 "@type": "Offer", 100 "name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif", 101 "sku": "NAGARP ONLINE", 102 "price": "9999", 103 "priceCurrency": "IDR", 104 "availability": "https://schema.org/InStock" 105} 106} 107</script>
107 108
109<script type="application/ld+json"> 110{ 111"@context": "https://schema.org/", 112"@type": "Product", 113"name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif", 114"image": "https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg", 115"description": "Perubahan Teknologi Digital pada NAGARP X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern.", 116"sku": "NAGARP ONLINE", 117"offers": { 118 "@type": "Offer", 119 "priceCurrency": "IDR", 120 "url": "https://sunnypork.com/", 121 "availability": "https://schema.org/InStock", 122 "price": "9999.00" 123} 124} 125</script>
125 126
127<script type="application/ld+json" id="bv-jsonld-bvloader-summary"> 128{ 129"@context": "https://schema.org/", 130"@id": "https://sunnypork.com/", 131"aggregateRating": { 132 "@type": "AggregateRating", 133 "itemReviewed": { 134 "@type": "IndividualProduct", 135 "name": "NAGARP SITUS" 136 }, 137 "ratingValue": "5.0", 138 "reviewCount": 954638, 139 "bestRating": 5 140} 141} 142</script>
142 143
144<script type="application/ld+json"> 145{ 146"@context": "https://schema.org", 147"@type": "BreadcrumbList", 148"itemListElement": [ 149 { 150 "@type": "ListItem", 151 "position": 1, 152 "item": { 153 "@id": "https://sunnypork.com/", 154 "name": "NAGARP SITUS" 155 } 156 }, 157 { 158 "@type": "ListItem", 159 "position": 2, 160 "item": { 161 "@id": "https://sunnypork.com/", 162 "name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif" 163 } 164 }, 165 { 166 "@type": "ListItem", 167 "position": 3, 168 "item": { 169 "@id": "https://sunnypork.com/", 170 "name": "NAGARP ONLINE" 171 } 172 }, 173 { 174 "@type": "ListItem", 175 "position": 4, 176 "item": { 177 "@id": "https://sunnypork.com/", 178 "name": "NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif" 179 } 180 } 181] 182} 183</script>
183 184
185<script type="application/ld+json" id="bv-jsonld-reviews-data"> 186{ 187 "@context": "https://schema.org", 188 "@type": "Organization", 189 "@id": "https://sunnypork.com/", 190 "name": "NAGARP SITUS ONLINE", 191 "alternateName": "NAGARP SITUS ONLINE", 192 "url": "https://sunnypork.com/", 193 "review": [ 194 { 195 "@type": "Review", 196 "dateCreated": "2026-03-31T12:28:35+00:00", 197 "datePublished": "2026-03-31T12:28:35+00:00", 198 "headline": "NAGARP ONLINE", 199 "reviewBody": "NAGARP X Sunny Pork berhasil menghadirkan nuansa baru lewat integrasi teknologi digital yang membuat platform interaktif terasa lebih hidup dan mudah digunakan.", 200 "reviewRating": { 201 "@type": "Rating", 202 "ratingValue": 5, 203 "bestRating": 5, 204 "worstRating": 1 205 }, 206 "author": { 207 "@type": "Person", 208 "name": "Perubahan Teknologi Digital â Surabaya" 209 } 210 }, 211 { 212 "@type": "Review", 213 "dateCreated": "2026-04--02T18:28:21+00:00", 214 "datePublished": "2026-04-03T18:28:21+00:00", 215 "headline": "NAGARP ONLINE", 216 "reviewBody": "Saya cukup impressed dengan cara NAGARP X Sunny Pork membentuk pengalaman platform interaktif menggunakan teknologi digital terkini tanpa ribet.", 217 "reviewRating": { 218 "@type": "Rating", 219 "ratingValue": 5, 220 "bestRating": 5, 221 "worstRating": 1 222 }, 223 "author": { 224 "@type": "Person", 225 "name": "Pengalaman Platform Interaktif â Semarang" 226 } 227 }, 228 { 229 "@type": "Review", 230 "dateCreated": "2026-04-05T19:28:18+00:00", 231 "datePublished": "2026-04-05T19:28:18+00:00", 232 "headline": "NAGARP ONLINE", 233 "reviewBody": "Platform NAGARP X Sunny Pork terasa modern berkat perubahan teknologi digital yang diterapkan secara konsisten dan user-friendly.", 234 "reviewRating": { 235 "@type": "Rating", 236 "ratingValue": 5, 237 "bestRating": 5, 238 "worstRating": 1 239 }, 240 "author": { 241 "@type": "Person", 242 "name": "Teknologi Digital â Makassar" 243 } 244 } 245 ] 246} 247</script>
vendor: 2 bytes, line 247
247
248<script type="application/ld+json"> 249{ 250 "@context": "https://schema.org", 251 "@type": "FAQPage", 252 "mainEntity": [ 253 { 254 "@type": "Question", 255 "name": "Apa dampak perubahan teknologi digital pada platform interaktif NAGARP X Sunny Pork?", 256 "acceptedAnswer": { 257 "@type": "Answer", 258 "text": "Perubahan teknologi digital membantu NAGARP X Sunny Pork menciptakan pengalaman platform interaktif yang lebih responsif dan engaging bagi pengguna." 259 } 260}, 261{ 262 "@type": "Question", 263 "name": "Bagaimana NAGARP X Sunny Pork memanfaatkan teknologi digital untuk platformnya?", 264 "acceptedAnswer": { 265 "@type": "Answer", 266 "text": "NAGARP X Sunny Pork mengintegrasikan teknologi digital terkini untuk membentuk pengalaman platform interaktif yang dinamis dan mudah diakses." 267 } 268}, 269{ 270 "@type": "Question", 271 "name": "Apakah NAGARP X Sunny Pork menawarkan fitur interaktif berbasis teknologi digital?", 272 "acceptedAnswer": { 273 "@type": "Answer", 274 "text": "Ya, NAGARP X Sunny Pork fokus pada perubahan teknologi digital yang membentuk pengalaman platform interaktif dengan elemen visual dan navigasi canggih." 275 } 276}, 277{ 278 "@type": "Question", 279 "name": "Mengapa pengalaman platform interaktif penting di NAGARP X Sunny Pork?", 280 "acceptedAnswer": { 281 "@type": "Answer", 282 "text": "Pengalaman platform interaktif di NAGARP X Sunny Pork menjadi prioritas karena perubahan teknologi digital memungkinkan interaksi yang lebih personal dan efisien." 283 } 284}, 285{ 286 "@type": "Question", 287 "name": "Apa keunggulan NAGARP X Sunny Pork dalam hal teknologi digital?", 288 "acceptedAnswer": { 289 "@type": "Answer", 290 "text": "Keunggulan NAGARP X Sunny Pork terletak pada kemampuannya memanfaatkan perubahan teknologi digital untuk menghadirkan pengalaman platform interaktif yang seamless." 291 } 292} 293 ] 294} 295</script>
295 296 <!-- User Session Data --> 297 <meta name="csrf-token" content="9ce6e4a17f7cc189893df9e47789b13e98829450d516a0cae5ca7b5f43e5dadc"> 298 <meta name="user-logged-in" content="false"> 299 <meta name="user-id" content="0"> 300 301 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> 302 303 <link rel="shortcut icon" href="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" type="image/x-icon"> 304 <style data-inlined="style.css"> 305@font-face { 306 font-family: 'Segoe UI'; 307 src: url('/assets/fonts/segoeui.woff2') format('woff2'), 308 url('/assets/fonts/segoeui.woff') format('woff'); 309 font-weight: 400; 310 font-style: normal; 311} 312 313:root { 314 --brand-50:#A61016; 315 --brand-100:#335DD9; 316 --brand-500:#C10DFF; 317 --brand-600:#C10DFF; 318 --gray-900:#A61016; 319 --gray-700:#335DD9; 320 --gray-500:#6F737B; 321 --gray-300:#C5C9CF; 322 --gray-100:#335DD9; 323 --border:#E4E7EB; 324 --radius:8px; 325 --transition: .18s ease; 326 /* font-family: "Inter", system-ui, sans-serif; */ 327 font-family: 'Segoe UI', sans-serif; 328} 329 330* { box-sizing: border-box; } 331body { 332 margin:0; 333 padding-bottom: 0; /* Reset for desktop */ 334 color:var(--gray-900); 335 background:#fff; 336} 337img { max-width:100%; display:block; } 338 339.container { 340 width:100%; 341 max-width:1280px; 342 margin:0 auto; 343 padding:10px 0; 344} 345 346.topbar { 347 background:#111; 348 color:#fff; 349 font-size:13px; 350} 351.topbar-inner { 352 display:flex; 353 justify-content:space-between; 354 align-items:center; 355 min-height:36px; 356} 357.topbar a { color:#fff; text-decoration:none; margin-left:12px; } 358 359/* ========== Desktop base (Glassmorphism Modern with Warm Gradient) ========== */ 360.navbar { 361 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 362 backdrop-filter:blur(12px); 363 -webkit-backdrop-filter:blur(12px); 364 border-bottom:1px solid rgba(228,231,235,0.6); 365 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 366 position:sticky; 367 top:0; 368 z-index:500; 369 transition:all .3s ease; 370} 371 372/* Accent line gradient #d9a837 */ 373.navbar::after { 374 content:''; 375 position:absolute; 376 bottom:0; 377 left:0; 378 right:0; 379 height:2px; 380 background:linear-gradient(90deg,transparent 0%, var(--brand-500) 20%, var(--brand-600) 50%, var(--brand-500) 80%, transparent 100%); 381 opacity:0.5; 382} 383 384.nav-inner { 385 display:flex; 386 align-items:center; 387 gap:24px; 388 min-height:70px; 389} 390.nav-inner .search .m-search-ico { display: none; } 391 392/* Secara default (desktop) ikon login disembunyikan; teks muncul */ 393.login-btn .login-ico { display:none; } 394.navbar-logo { 395 display: none; 396 background:linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,245,230,0.85) 100%); 397 backdrop-filter:blur(12px); 398 -webkit-backdrop-filter:blur(12px); 399 border-bottom:1px solid rgba(228,231,235,0.6); 400 box-shadow:0 2px 16px -4px rgba(0,0,0,0.08); 401 position:sticky; 402 top:0; 403 z-index:500; 404} 405 406/* Hide bottom navigation on desktop - only show on mobile */ 407.bottom-nav { 408 display: none; 409} 410 411/* ============================================================== 412 MOBILE (<=640px) 413 ============================================================== */ 414@media (max-width:640px){ 415 .container { padding:10px 16px; } 416 417 .topbar { display:none !important; } 418 419 /* Add padding bottom for sticky bottom nav */ 420 body { 421 padding-bottom: 68px; /* Height of bottom nav + safe area */ 422 } 423 424 /* Mobile Top Navbar - Clean & Minimal */ 425 .navbar { 426 background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); 427 backdrop-filter:blur(16px); 428 -webkit-backdrop-filter:blur(16px); 429 box-shadow:0 2px 12px -3px rgba(0,0,0,0.08); 430 border: 0; 431 } 432 433 /* Simple grid layout - Logo, Search, Category */ 434 .nav-inner { 435 display:grid; 436 grid-template-columns:1fr; 437 gap:10px; 438 padding:12px 16px 0 12px; 439 min-height:auto; 440 } 441 442 /* Logo centered at top */ 443 .nav-inner .logo { 444 justify-self:center; 445 margin:0; 446 display:flex; 447 } 448 .nav-inner .logo img { height:36px; } 449 450 /* Search bar - full width */ 451 .nav-inner .search { 452 position:relative; 453 display:flex;
454 align-items:center; 455 gap:0; 456 width:100%; 457 height:44px; 458 background:#fff; 459 border:1.5px solid #e5e7eb; 460 border-radius:12px; 461 padding:0 12px; 462 box-shadow:0 1px 3px rgba(0,0,0,0.04); 463 transition:all .25s; 464 } 465 .nav-inner .search:focus-within { 466 border-color:#C10DFF; 467 box-shadow:0 0 0 3px rgba(245,202,86,0.1); 468 } 469 470 .nav-inner .search-icon-btn { 471 color:#9ca3af; 472 padding:4px; 473 margin-right:4px; 474 } 475 476 .nav-inner .search-icon-btn svg { 477 width:18px; 478 height:18px; 479 } 480 481 .nav-inner .search input { 482 flex:1; 483 min-width:0; 484 border:none; 485 outline:none; 486 background:transparent; 487 font-size:14px; 488 padding:0 8px; 489 height:100%; 490 } 491 492 .nav-inner .ai-bot-toggle { 493 padding:6px; 494 margin-left:0; 495 } 496 497 .nav-inner .ai-bot-toggle .bot-icon { 498 width:20px; 499 height:20px; 500 } 501 502 .nav-inner .search-suggest { 503 left:0; 504 right:0; 505 width:calc(100vw - 32px); 506 margin-top:6px; 507 border-radius:12px; 508 max-height:70vh; 509 } 510 511 /* Hide desktop nav actions but keep structure for userMenu */ 512 .nav-inner .nav-actions { 513 display:flex !important; /* Keep flex to allow userMenu positioning */ 514 visibility:hidden; /* Hide visually but keep in DOM */ 515 pointer-events:none; /* Disable interactions with hidden elements */ 516 position:absolute; /* Remove from flow */ 517 right:0; 518 top:0; 519 } 520 521 /* Re-enable userMenu for logged-in users */ 522 .nav-inner .nav-actions #userMenu { 523 visibility:visible !important; 524 pointer-events:auto !important; 525 } 526 527 .nav-inner .logo { display: none; } 528 .navbar-logo { 529 display: block; 530 /* background:linear-gradient(135deg, rgba(255,255,255,0.97) 0%, rgba(255,245,230,0.9) 100%); */ 531 backdrop-filter:blur(16px); 532 -webkit-backdrop-filter:blur(16px); 533 position: static; /* Remove sticky on mobile */ 534 box-shadow: none; 535 border-bottom: 0; 536 padding-top: .6rem; 537 } 538 .nav-inner .logo-mobile { display: block; } 539 540 /* Category menu wrapper will handle visibility */ 541 542 /* ============================================================== 543 BOTTOM NAVIGATION - Styles moved to bottom-nav.css 544 ============================================================== */ 545 /* All bottom-nav styles now in /assets/css/bottom-nav.css */ 546 547 /* Hide desktop elements that now moved to bottom nav */ 548 #wishlistBtn, 549 #notifBtn, 550 #loginBtn { display:none !important; } 551 552 /* Hide desktop user menu on mobile - use bottom sheet instead */ 553 .navbar #userMenu { 554 display:none !important; 555 } 556 557 .nav-actions .cart-indicator, 558 .nav-actions .compare-indicator { 559 display:none !important; 560 } 561 562 /* Hide category button from bottom nav */ 563 #bottomCategoryBtn { 564 display: none !important; 565 } 566 567 /* User menu button styling for mobile - scoped to navbar only */ 568 .navbar #userMenu .login-btn.logged-in { 569 width:40px; 570 height:40px; 571 padding:0; 572 border-radius:50%; 573 display:flex;
574 align-items:center; 575 justify-content:center; 576 background:rgba(255,255,255,0.95); 577 backdrop-filter:blur(12px); 578 -webkit-backdrop-filter:blur(12px); 579 border:1px solid rgba(228,231,235,0.6); 580 box-shadow:0 2px 12px -3px rgba(0,0,0,0.12); 581 } 582 583 .navbar #userMenu .login-btn.logged-in .login-text { 584 display:none; 585 } 586 587 .navbar #userMenu .login-btn.logged-in .login-ico { 588 display:inline; 589 font-size:20px; 590 line-height:1; 591 } 592 593 /* User dropdown positioning for mobile */ 594 .navbar #userMenu .user-dropdown { 595 position:fixed !important; 596 top:60px !important; 597 right:16px !important; 598 left:auto !important; 599 min-width:200px; 600 background:rgba(255,255,255,0.98); 601 backdrop-filter:blur(20px); 602 -webkit-backdrop-filter:blur(20px); 603 border:1px solid rgba(228,231,235,0.6); 604 box-shadow:0 8px 24px -8px rgba(0,0,0,0.2); 605 z-index:1002 !important; 606 border-radius:12px; 607 padding:8px 0; 608 } 609 610 /* Force visibility when not hidden attribute */ 611 .navbar #userMenu .user-dropdown:not([hidden]) { 612 display:block !important; 613 visibility:visible !important; 614 opacity:1 !important; 615 } 616 617 /* Container untuk kategori dan tombol kategori sejajar */ 618 .navbar .container:last-child { 619 display:flex !important; 620 align-items:center !important; 621 padding:4px 16px 6px !important; 622 gap:5px; 623 overflow:visible; 624 } 625 626 /* Wrapper untuk mega-cats dengan fade gradient (mobile only) */ 627 .mega-cats-wrapper { 628 position:relative; 629 flex:1; 630 overflow:hidden; 631 min-height:34px; /* Ensure content is visible */ 632 display:flex; /* Ensure wrapper is flex container */ 633 } 634 635 /* Fade gradient indicator di ujung kanan (mobile only) */ 636 .mega-cats-fade { 637 position:absolute; 638 top:0; 639 right:0; 640 bottom:0; 641 width:80px; /* Increased from 60px for better visibility */ 642 background:linear-gradient(to left, 643 rgba(255,245,230,1) 0%, 644 rgba(255,245,230,0.98) 15%, 645 rgba(255,245,230,0.85) 35%, 646 transparent 100%); 647 pointer-events:none; 648 z-index:5; /* Increased z-index */ 649 transition:opacity 0.3s ease; 650 } 651 652 /* Kategori menu di mobile */ 653 .mega-cats.container { 654 display:flex !important; 655 gap:12px; 656 overflow-x:auto; 657 overflow-y:visible; 658 -webkit-overflow-scrolling:touch; 659 scrollbar-width:none; 660 flex:1; 661 padding:0 !important; 662 margin:0 !important; 663 position:relative; /* For z-index stacking */ 664 z-index:1; /* Below fade gradient */ 665 } 666 .mega-cats.container::-webkit-scrollbar { display:none; } 667 668 /* Styling untuk menu items di mobile */ 669 .mega-cats a { 670 font-size:13px; 671 padding:6px 10px; 672 white-space:nowrap; 673 flex-shrink:0; 674 position:relative; /* Ensure proper stacking */ 675 } 676 .mega-cats a.active { 677 background:var(--brand-600); 678 color:#fff; 679 font-weight:600; 680 border-radius:6px; 681 } 682 683 /* Tombol kategori di mobile */ 684 .mm-wp { 685 flex-shrink:0; 686 } 687 .mm2-trigger { 688 display:flex;
689 align-items:center; 690 gap:6px; 691 padding:6px 12px; 692 background:var(--gray-100); 693 color:var(--gray-700); 694 text-decoration:none; 695 border-radius:6px; 696 font-size:13px; 697 white-space:nowrap; 698 } 699 700 .mm2-trigger .label-cat {display: none; } 701 /* Tombol more kategori */ 702 .mega-more-btn { 703 border:1px solid var(--border); 704 background:#fff; 705 border-radius:10px; 706 padding:6px 14px; 707 font-size:18px; 708 cursor:pointer; 709 } 710 .mega-cats-more.active > .mega-more-btn { 711 background:var(--brand-600); 712 color:#fff; 713 border-color:var(--brand-600); 714 } 715 .mega-more-dropdown { 716 top:calc(100% + 10px); 717 right:0; 718 min-width:85vw; 719 max-width:90vw; 720 border-radius:16px; 721 } 722 .mega-more-grid { 723 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 724 } 725 726 /* ============================================================== 727 MOBILE USER BOTTOM SHEET 728 ============================================================== */ 729 .mobile-user-sheet { 730 position:fixed; 731 top:0; 732 left:0; 733 right:0; 734 bottom:0; 735 z-index:10000; 736 pointer-events:none; 737 } 738 739 .mobile-user-sheet.active { 740 pointer-events:auto; 741 } 742 743 .mobile-user-sheet-backdrop { 744 position:absolute; 745 top:0; 746 left:0; 747 right:0; 748 bottom:0; 749 background:rgba(0,0,0,0.5); 750 opacity:0; 751 transition:opacity .3s ease; 752 } 753 754 .mobile-user-sheet.active .mobile-user-sheet-backdrop { 755 opacity:1; 756 } 757 758 .mobile-user-sheet-content { 759 position:absolute; 760 bottom:0; 761 left:0; 762 right:0; 763 background:#fff; 764 border-radius:20px 20px 0 0; 765 box-shadow:0 -4px 24px rgba(0,0,0,0.15); 766 transform:translateY(100%); 767 transition:transform .3s cubic-bezier(0.4, 0.0, 0.2, 1); 768 padding-bottom:calc(68px + env(safe-area-inset-bottom)); 769 } 770 771 .mobile-user-sheet.active .mobile-user-sheet-content { 772 transform:translateY(0); 773 } 774 775 .mobile-user-sheet-header { 776 padding:16px 20px 12px; 777 border-bottom:1px solid #e5e7eb; 778 } 779 780 .mobile-user-sheet-handle { 781 width:40px; 782 height:4px; 783 background:#d1d5db; 784 border-radius:2px; 785 margin:0 auto 16px; 786 } 787 788 .mobile-user-info { 789 display:flex;
790 align-items:center; 791 gap:12px; 792 } 793 794 .mobile-user-avatar { 795 width:48px; 796 height:48px; 797 border-radius:50%; 798 background:linear-gradient(135deg, #335DD9, #fff1cc); 799 display:flex; 800 align-items:center; 801 justify-content:center; 802 font-size:24px; 803 flex-shrink:0; 804 } 805 806 .mobile-user-details { 807 flex:1; 808 min-width:0; 809 } 810 811 .mobile-user-name { 812 font-size:16px; 813 font-weight:600; 814 color:#111827; 815 margin-bottom:2px; 816 white-space:nowrap; 817 overflow:hidden; 818 text-overflow:ellipsis; 819 } 820 821 .mobile-user-email { 822 font-size:13px; 823 color:#6b7280; 824 white-space:nowrap; 825 overflow:hidden; 826 text-overflow:ellipsis; 827 } 828 829 .mobile-user-sheet-body { 830 padding:8px 0; 831 } 832 833 .mobile-user-sheet-item { 834 display:flex; 835 align-items:center; 836 gap:16px; 837 padding:14px 20px; 838 text-decoration:none; 839 color:#374151; 840 font-size:15px; 841 transition:background .2s; 842 border:none; 843 background:none; 844 width:100%; 845 text-align:left; 846 cursor:pointer; 847 } 848 849 .mobile-user-sheet-item:active { 850 background:#f3f4f6; 851 } 852 853 .mobile-user-sheet-icon { 854 font-size:20px; 855 width:24px; 856 text-align:center; 857 flex-shrink:0; 858 } 859 860 .mobile-user-sheet-label { 861 flex:1; 862 font-weight:500; 863 } 864 865 .mobile-user-sheet-divider { 866 height:1px; 867 background:#e5e7eb; 868 margin:8px 0; 869 } 870} 871 872/* END NAVBAR */ 873 874.logo img { display:block; } 875.search { 876 flex:1; 877 position:relative; 878 display:flex; 879 align-items:center; 880 gap:0; 881 background:#fff; 882 border:1.5px solid #e5e7eb; 883 border-radius:12px; 884 padding:0 16px; 885 height:38px; 886 transition:all 0.3s ease; 887} 888.search:focus-within { 889 border-color:#C10DFF; 890 box-shadow:0 0 0 3px rgba(245,202,86,0.1); 891} 892.search input { 893 flex:1; 894 border:none; 895 outline:none; 896 background:transparent; 897 font-size:15px; 898 padding:0 12px; 899 height:100%; 900 color:#1f2937; 901} 902.search input::placeholder { 903 color:#9ca3af; 904} 905.search-icon-btn { 906 background:transparent; 907 border:none; 908 padding:0; 909 display:flex; 910 align-items:center; 911 justify-content:center; 912 cursor:pointer; 913 color:#6b7280; 914 transition:color 0.2s ease; 915 flex-shrink:0; 916} 917.search-icon-btn:hover { 918 color:#C10DFF; 919} 920.search-suggest { 921 position:absolute; 922 left:0; right:0; top:100%; 923 background:#fff; 924 border:1px solid var(--border); 925 border-radius:var(--radius); 926 padding:8px 0; 927 box-shadow:0 8px 20px rgba(0,0,0,.08); 928 margin-top:4px; 929 font-size:14px; 930 max-height:260px; 931 overflow:auto; 932} 933.search-suggest div { 934 padding:6px 12px; 935 cursor:pointer; 936} 937.search-suggest div:hover { background:var(--gray-100); } 938 939.nav-actions { display:flex; gap:10px; align-items:center; } 940.icon-btn, .login-btn { 941 background:linear-gradient(135deg, rgba(255,255,255,0.95), rgba(249,250,251,0.9)); 942 border:1px solid rgba(228,231,235,0.6); 943 padding:8px 12px; 944 border-radius:var(--radius); 945 cursor:pointer; 946 font-size:14px; 947 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 948 height: 38px; 949 position:relative; 950 box-shadow: 951 0 1px 3px rgba(0,0,0,0.06), 952 0 0 0 1px rgba(255,255,255,0.5) inset; 953 backdrop-filter:blur(8px); 954 -webkit-backdrop-filter:blur(8px); 955} 956.icon-btn:hover, .login-btn:hover { 957 background:linear-gradient(135deg, rgba(249,250,251,1), rgba(243,244,246,0.95)); 958 border-color:rgba(209,213,219,0.8); 959 box-shadow: 960 0 2px 8px rgba(0,0,0,0.08), 961 0 0 0 1px rgba(255,255,255,0.6) inset; 962 transform:translateY(-1px); 963} 964.cart-indicator, .compare-indicator { position:relative; } 965.badge { 966 background:linear-gradient(135deg, #e2a642, #e2a642); 967 color:#fff; 968 font-size:10.5px; 969 font-weight:600; 970 padding:2px 6px; 971 border-radius:999px; 972 position:absolute; 973 top:-6px; 974 right:-6px; 975 min-width:18px; 976 height:18px; 977 display:flex; 978 align-items:center; 979 justify-content:center; 980 box-shadow: 981 0 2px 8px rgba(226,166,66,0.4), 982 0 0 0 2px #fff, 983 0 0 12px -2px rgba(226,166,66,0.6); 984 transition:all 0.3s ease; 985} 986.icon-btn:hover .badge { 987 transform:scale(1.08); 988 box-shadow: 989 0 4px 12px rgba(226,166,66,0.5), 990 0 0 0 2px #fff, 991 0 0 16px -2px rgba(226,166,66,0.7); 992} 993.badge.is-empty { display:none; } 994 995/* User Menu Dropdown */ 996.user-menu { 997 position:relative; 998} 999.user-dropdown { 1000 position:absolute; 1001 top:calc(100% + 8px); 1002 right:0; 1003 background:#fff; 1004 border:1px solid var(--border); 1005 border-radius:12px; 1006 box-shadow:0 10px 40px rgba(0,0,0,0.12);
1007 min-width:200px; 1008 padding:8px 0; 1009 z-index:9999; 1010} 1011 1012/* Ensure dropdown is visible when not hidden */ 1013.user-dropdown:not([hidden]) { 1014 display:block !important; 1015 visibility:visible !important; 1016 opacity:1 !important; 1017} 1018 1019/* Mobile: Force user dropdown to be visible above everything */ 1020@media (max-width:640px) { 1021 .user-dropdown:not([hidden]) { 1022 position:fixed !important; 1023 z-index:9999 !important; 1024 top:60px !important; 1025 right:16px !important; 1026 } 1027} 1028 1029.dropdown-item { 1030 display:flex; 1031 align-items:center; 1032 gap:12px; 1033 padding:10px 16px; 1034 text-decoration:none; 1035 color:var(--gray-700); 1036 font-size:14px; 1037 transition:var(--transition); 1038 border:none; 1039 background:none; 1040 width:100%; 1041 text-align:left; 1042 cursor:pointer; 1043} 1044.dropdown-item:hover { 1045 background:var(--gray-100); 1046 color:var(--gray-900); 1047} 1048.dropdown-icon { 1049 font-size:16px; 1050 opacity:0.7; 1051} 1052.dropdown-separator { 1053 height:1px; 1054 background:var(--border); 1055 margin:8px 0; 1056} 1057.login-btn.logged-in { 1058 background:linear-gradient(135deg, rgba(255,245,230,0.8), rgba(255,231,204,0.6)); 1059 border:1px solid rgba(245,202,86,0.2); 1060 color:var(--brand-700); 1061 max-width:200px; /* Prevent button from getting too wide */ 1062 display:inline-flex; 1063 align-items:center; 1064 gap:10px; 1065 padding:4px 12px 4px 4px; 1066 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1067 position:relative; 1068 overflow:hidden; 1069 box-shadow: 1070 0 1px 3px rgba(245,202,86,0.08), 1071 0 0 0 1px rgba(255,255,255,0.5) inset, 1072 0 0 12px -6px rgba(245,202,86,0.1); 1073 backdrop-filter:blur(8px); 1074 -webkit-backdrop-filter:blur(8px); 1075} 1076/* Subtle background glow effect */ 1077.login-btn.logged-in::before { 1078 content:''; 1079 position:absolute; 1080 top:50%; 1081 left:0; 1082 width:32px; 1083 height:32px; 1084 background:radial-gradient(circle, rgba(245,202,86,0.15), transparent 70%); 1085 transform:translateY(-50%); 1086 opacity:0; 1087 transition:opacity 0.3s ease; 1088} 1089.login-btn.logged-in:hover::before { 1090 opacity:1; 1091} 1092.login-btn.logged-in .user-avatar { 1093 width:32px; 1094 height:32px; 1095 border-radius:50%; 1096 background:linear-gradient(135deg, #335DD9 0%, #fff1cc 50%, #C10DFF 100%); 1097 color:#C10DFF; 1098 font-size:11.5px; 1099 font-weight:700; 1100 display:flex; 1101 align-items:center; 1102 justify-content:center; 1103 flex-shrink:0; 1104 letter-spacing:0.3px; 1105 position:relative; 1106 box-shadow: 1107 0 2px 8px rgba(245,202,86,0.25), 1108 0 1px 3px rgba(0,0,0,0.12), 1109 inset 0 1px 0 rgba(255,255,255,0.5); 1110 transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 1111} 1112/* Ring gradient border */ 1113.login-btn.logged-in .user-avatar::before { 1114 content:''; 1115 position:absolute; 1116 inset:-2px; 1117 border-radius:50%; 1118 padding:2px; 1119 background:linear-gradient(135deg, #C10DFF, #fff1cc, #C10DFF); 1120 -webkit-mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1121 -webkit-mask-composite:xor; 1122 mask:linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); 1123 mask-composite:exclude; 1124 opacity:0; 1125 transition:opacity 0.3s ease; 1126} 1127.login-btn.logged-in:hover .user-avatar::before { 1128 opacity:1; 1129} 1130.login-btn.logged-in:hover .user-avatar { 1131 transform:scale(1.08) rotate(-3deg); 1132 box-shadow: 1133 0 4px 12px rgba(245,202,86,0.35), 1134 0 2px 6px rgba(0,0,0,0.15), 1135 inset 0 1px 0 rgba(255,255,255,0.6); 1136} 1137.login-btn.logged-in .login-text { 1138 overflow:hidden; 1139 text-overflow:ellipsis; 1140 white-space:nowrap; 1141 flex:1; 1142 min-width:0; 1143 font-weight:500; 1144 font-size:14px; 1145 transition:color 0.2s ease; 1146} 1147.login-btn.logged-in:hover .login-text { 1148 color:var(--brand-600); 1149} 1150.login-btn.logged-in:hover { 1151 background:linear-gradient(135deg, rgba(255,245,230,0.95), rgba(255,231,204,0.85)); 1152 border-color:rgba(245,202,86,0.3); 1153 box-shadow: 1154 0 4px 16px -4px rgba(245,202,86,0.25), 1155 0 0 0 1px rgba(245,202,86,0.1) inset, 1156 0 1px 2px rgba(0,0,0,0.05); 1157 transform:translateY(-1px); 1158} 1159 1160/* Desktop: hide wrapper elements, show menu normally */ 1161@media (min-width: 761px) { 1162 .mega-cats-wrapper { 1163 display:contents; /* Remove wrapper from layout */ 1164 } 1165 .mega-cats-fade { 1166 display:none !important; /* Hide fade on desktop */ 1167 } 1168} 1169 1170.mega-cats { 1171 display:flex; 1172 gap:5px; 1173 /* padding:6px 16px 10px; */ 1174 scrollbar-width:none; 1175 position: relative; 1176 z-index: 510; 1177 overflow-x:auto; 1178 -webkit-overflow-scrolling:touch;
1179 flex:1; 1180} 1181.mega-cats::-webkit-scrollbar { 1182 display:none; 1183} 1184.mega-cats a { 1185 text-decoration:none; 1186 font-size:14px; 1187 color:var(--gray-700); 1188 padding:6px 10px; 1189 border-radius:6px; 1190 white-space:nowrap; 1191 transition:var(--transition); 1192} 1193.mega-cats a:hover { background:var(--brand-100); color:var(--brand-600); } 1194/*.mega-cats::after { 1195 content:''; 1196 position:absolute; 1197 right:0; 1198 top:0; 1199 width:40px; 1200 height:100%; 1201 background:linear-gradient(to right,rgba(255,255,255,0),#fff); 1202 pointer-events:none; 1203}*/ 1204 1205/* Tambahan untuk tombol More & dropdown. 1206 Style dasar .mega-cats dan .mega-cats a tetap pakai definisi Anda. */ 1207 1208.mega-cats-more { 1209 position:relative; 1210 display:flex; 1211 align-items:center; 1212} 1213 1214.mega-cats-more.active > .mega-more-btn { 1215 background:var(--brand-600); 1216 color:#fff; 1217} 1218 1219.mega-more-btn { 1220 cursor:pointer; 1221 border:none; 1222 background:var(--gray-100,#f5f5f5); 1223 color:var(--gray-700,#333); 1224 padding:6px 12px; 1225 font-size:16px; 1226 line-height:1; 1227 border-radius:6px; 1228 display:flex; 1229 align-items:center; 1230 justify-content:center; 1231 transition:var(--transition); 1232} 1233.mega-more-btn:hover { 1234 background:var(--brand-100); 1235 color:var(--brand-600); 1236} 1237 1238.mega-more-dropdown { 1239 position:absolute; 1240 top:100%; 1241 right:0; 1242 margin-top:8px; 1243 background:#fff; 1244 border:1px solid var(--gray-200,#e3e6e9); 1245 border-radius:10px; 1246 padding:12px 14px 14px; 1247 min-width:400px; 1248 max-width:520px; 1249 max-height:420px; 1250 box-shadow:0 12px 32px -8px rgba(25,40,65,.25); 1251 z-index: 3000; 1252} 1253 1254.mega-more-grid { 1255 display:grid; 1256 grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); 1257 gap:8px 10px; 1258 overflow-x: hidden; 1259 max-height:380px; 1260 overscroll-behavior:contain; 1261 scrollbar-width:thin; 1262} 1263.mega-more-grid::-webkit-scrollbar { 1264 width:6px; 1265} 1266.mega-more-grid::-webkit-scrollbar-thumb { 1267 background:var(--gray-300,#c7ccd1); 1268 border-radius:3px; 1269} 1270 1271/* Link di dropdown mewarisi style .mega-cats a (warna, padding, radius). 1272 Kita tambahkan state active agar konsisten. */ 1273.mega-more-grid a.active { 1274 background:var(--brand-600); 1275 color:#fff; 1276 font-weight:600; 1277} 1278.hero { 1279 background:linear-gradient(135deg,var(--brand-50), #fff); 1280} 1281.hero-inner { 1282 display:grid; 1283 grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); 1284 gap:48px; 1285 align-items:center; 1286} 1287.hero-text h1 { 1288 font-size:48px; 1289 line-height:1.1; 1290 margin:0; 1291 font-weight:600; 1292 font-family:"Poppins",sans-serif; 1293} 1294.hero-text .text-brand { color:var(--brand-500); } 1295.lead { font-size:18px; color:var(--gray-700); margin-top:16px; } 1296.hero-cta { margin-top:28px; display:flex; gap:16px; } 1297.hero-bullets { margin-top:28px; display:flex; gap:28px; font-size:14px; color:var(--gray-600); } 1298.hero-box { 1299 background:#fff; 1300 padding:10px; 1301 border-radius:16px; 1302 box-shadow:0 4px 18px rgba(0,0,0,.08); 1303 /* min-height:320px; */ 1304 display:flex; 1305 align-items:center; 1306 justify-content:center; 1307} 1308.hero-box img {border-radius: 10px; } 1309 1310@media (max-width:680px) { 1311 .hero-inner { gap: 25px; } 1312} 1313 1314.usp-section { 1315 position:relative; 1316 background: 1317 linear-gradient(180deg,#FFFDFB 0%, #FFFFFF 60%) ; 1318 border-top:1px solid var(--border); 1319 border-bottom:1px solid var(--border); 1320 padding:10px 0 10px; 1321} 1322 1323.usp-list { 1324 list-style:none; 1325 margin:0; 1326 padding:0; 1327 display:grid; 1328 gap:26px; 1329} 1330 1331.usp-list.variant-soft { 1332 grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); 1333} 1334 1335.usp-item { 1336 display:flex; 1337 gap:16px; 1338 align-items:flex-start; 1339 background:#fff; 1340 padding:18px 18px 18px 16px; 1341 border:1px solid rgba(226,226,226,0.7); 1342 border-radius:18px; 1343 position:relative; 1344 overflow:hidden; 1345 isolation:isolate; 1346 box-shadow:0 2px 4px -1px rgba(0,0,0,.04), 0 6px 18px -6px rgba(0,0,0,.08); 1347 transition: .35s cubic-bezier(.16,.8,.32,1); 1348} 1349 1350.usp-item::before { 1351 content:""; 1352 position:absolute; 1353 inset:0; 1354 background:radial-gradient(circle at 18% 22%, rgba(245,202,86,.18), transparent 60%); 1355 opacity:.65; 1356 transition:.5s; 1357 z-index:-1; 1358} 1359 1360.usp-item:hover { 1361 transform:translateY(-6px); 1362 box-shadow:0 10px 26px -8px rgba(0,0,0,.15), 0 3px 8px -2px rgba(0,0,0,.08); 1363 border-color:rgba(245,202,86,.35); 1364} 1365 1366.usp-item:hover::before { 1367 background:radial-gradient(circle at 70% 40%, rgba(245,202,86,.22), transparent 65%); 1368} 1369 1370.usp-icon { 1371 flex:0 0 56px; 1372 width:56px; 1373 height:56px; 1374 border-radius:16px; 1375 display:flex; 1376 align-items:center; 1377 justify-content:center; 1378 background: 1379 linear-gradient(135deg,#335DD9,#fff1cc); 1380 position:relative; 1381 box-shadow:inset 0 0 0 1px rgba(255,255,255,.6), 0 4px 10px -3px rgba(0,0,0,.15); 1382 transition:.45s; 1383} 1384 1385.usp-item:hover .usp-icon { 1386 transform:rotate(-4deg) scale(1.06); 1387 box-shadow:inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 14px -4px rgba(0,0,0,.20); 1388} 1389 1390.usp-icon svg { 1391 width:28px; 1392 height:28px; 1393 display:block; 1394 filter:drop-shadow(0 2px 2px rgba(0,0,0,.18));
1395} 1396 1397.usp-text h3 { 1398 font-size:15px; 1399 font-weight:600; 1400 margin:2px 0 6px; 1401 letter-spacing:.2px; 1402} 1403 1404.usp-text p { 1405 font-size:13px; 1406 line-height:1.3; 1407 margin:0; 1408 color:var(--gray-600); 1409} 1410 1411@media (max-width:680px) { 1412 .usp-section { padding: 5px 0 5px; margin-bottom: 0.75rem;} 1413 .usp-item { padding:16px 14px; } 1414 .usp-icon { width:50px;height:50px;border-radius:14px; } 1415 .usp-text h3 { font-size:14px; margin-bottom:4px; } 1416 .usp-text p { font-size:12.5px; } 1417 .usp-list { gap: 10px; } 1418} 1419 1420.flash { padding:50px 0 30px; background:#fff; } 1421.flash-head { 1422 display:flex; align-items:center; 1423 justify-content:space-between; 1424 margin-bottom:20px; 1425} 1426.flash-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1427.flash .countdown { 1428 font-family:monospace; 1429 background:#111; 1430 color:#fff; 1431 padding:8px 14px; 1432 border-radius:8px; 1433 font-size:16px; 1434 letter-spacing:1px; 1435} 1436.flash-scroll { 1437 display:flex; 1438 gap:16px; 1439 overflow-x:auto; 1440 padding-bottom:6px; 1441 scrollbar-width:none; 1442} 1443.flash-scroll::-webkit-scrollbar { display:none; } 1444 1445.products { padding:60px 0; background:#fff; } 1446.products-head { 1447 display:flex; justify-content:space-between; align-items:center; 1448 margin-bottom:24px; 1449 flex-wrap:wrap; 1450 gap:16px; 1451} 1452.products-head h2 { margin:0; font-size:24px; font-family:"Poppins"; } 1453.product-filters input { 1454 padding:8px 12px; 1455 border:1px solid var(--border); 1456 border-radius:8px; 1457 font-size:14px; 1458 width:220px; 1459} 1460 1461.product-grid { 1462 display:grid; 1463 gap:24px; 1464 grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); 1465} 1466 1467.prod-card { 1468 background:#fff; 1469 border:1px solid var(--border); 1470 padding:12px; 1471 border-radius:12px; 1472 position:relative; 1473 display:flex; 1474 flex-direction:column; 1475 transition:var(--transition); 1476} 1477.prod-card.small { min-width:160px; } 1478.prod-card:hover { box-shadow:0 8px 20px rgba(0,0,0,.12); transform:translateY(-3px); } 1479.prod-card .img-box { 1480 background:var(--gray-100); 1481 border-radius:10px; 1482 aspect-ratio:1/1; 1483 display:flex; 1484 align-items:center; 1485 justify-content:center; 1486 overflow:hidden; 1487} 1488.prod-card .img-box img { width:100%; height:100%; object-fit:contain; transition:var(--transition); } 1489.prod-card:hover .img-box img { transform:scale(1.04); } 1490.prod-card .name { 1491 font-size:13.5px; 1492 font-weight:500; 1493 margin:10px 0 6px; 1494 min-height:36px; 1495 line-height:1.3; 1496} 1497.price { 1498 display:flex; 1499 gap:6px; 1500 align-items:center; 1501 flex-wrap:wrap; 1502} 1503.price strong { color:var(--brand-600); font-size:14px; } 1504.price .old { 1505 font-size:11px; 1506 color:var(--gray-500); 1507 text-decoration:line-through; 1508} 1509.tag.discount { 1510 position:absolute; 1511 top:8px; left:8px; 1512 background:#e2a642; 1513 color:#fff; 1514 font-size:11px; 1515 padding:4px 6px; 1516 border-radius:6px; 1517 font-weight:600; 1518} 1519.btn { 1520 border:none; 1521 cursor:pointer; 1522 font-weight:500; 1523 font-size:14px; 1524 padding:12px 22px; 1525 border-radius:10px; 1526 background:var(--gray-900); 1527 color:#fff; 1528 transition:var(--transition); 1529 text-decoration:none; 1530 text-align:center; 1531 display:inline-flex; 1532 align-items:center; 1533 justify-content:center; 1534 gap:6px; 1535} 1536.btn:hover { background:#000; } 1537.btn-primary { background:var(--brand-500); } 1538.btn-primary:hover { background:var(--brand-600); } 1539.btn-outline { 1540 background:#fff; 1541 color:var(--gray-900); 1542 border:1px solid var(--border); 1543} 1544.btn-outline:hover { background:var(--gray-100); } 1545.btn-sm { padding:8px 14px; font-size:13px; } 1546.btn-mini { padding:6px 10px; font-size:12px; } 1547.add-cart { margin-top:auto; background:var(--brand-500); color:#fff; } 1548.add-cart:hover { background:var(--brand-600); } 1549 1550/* ==== FOOTER COMPACT FIX PATCH (final) ==== */ 1551 1552/* 1. Pastikan grid aktif */ 1553 1554.site-footer.footer-compact .ft-grid { 1555 display:grid; 1556 grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); 1557 gap:30px 40px; 1558 align-items:start; 1559} 1560 1561/* 2. Spacing atas bawah dipadatkan */ 1562.site-footer.footer-compact .ft-top { 1563 padding:42px 0 26px; 1564} 1565 1566/* 3. Judul & teks */ 1567.site-footer.footer-compact .ft-title { 1568 margin:0 0 14px; 1569 font-size:14px; 1570 letter-spacing:.8px; 1571 text-transform:uppercase; 1572 font-weight:600;
1573 color:#82b1c9; 1574} 1575 1576.site-footer.footer-compact .ft-desc, 1577.site-footer.footer-compact .ft-text { 1578 margin:0 0 16px; 1579 font-size:13.5px; 1580 line-height:1.45; 1581 color:#c6d6e0; 1582 max-width:330px; 1583} 1584.site-footer.footer-compact .ft-desc.sm-gap { margin-top:18px; } 1585 1586/* 4. Reset ikon SVG agar tidak membesar */ 1587.site-footer.footer-compact svg { 1588 width:20px; 1589 height:20px; 1590 flex:0 0 20px; 1591 display:inline-block; 1592} 1593.site-footer.footer-compact .ft-social svg { width:18px; height:18px; } 1594.site-footer.footer-compact .ft-wa-btn svg { width:18px; height:18px; } 1595 1596/* Jika ada aturan global svg { width:100%; } â paksa override */ 1597svg { max-width:100%; } 1598.site-footer.footer-compact svg { 1599 width:20px !important; 1600 height:20px !important; 1601} 1602 1603/* 5. Link list */ 1604.site-footer.footer-compact .ft-links { 1605 list-style:none; 1606 margin:0 0 12px; 1607 padding:0; 1608 display:flex; 1609 flex-direction:column; 1610 gap:4px; 1611} 1612.site-footer.footer-compact .ft-links li { margin:0; } 1613.site-footer.footer-compact .ft-links a { 1614 display:flex; 1615 align-items:center; 1616 gap:8px; 1617 font-size:13.5px; 1618 padding:5px 2px; 1619 text-decoration:none; 1620 color:#c6d6e0; 1621 border-radius:8px; 1622 transition:.25s; 1623} 1624.site-footer.footer-compact .ft-links a:hover { 1625 background:rgba(255,255,255,.06); 1626 padding-left:6px; 1627 color:#fff; 1628} 1629 1630.site-footer.footer-compact .ico-s { 1631 width:24px; 1632 height:24px; 1633 background:#103346; 1634 color:#89b8cf; 1635 border-radius:6px; 1636 display:flex; 1637 align-items:center; 1638 justify-content:center; 1639 flex:0 0 24px; 1640 font-size:12px; 1641 transition:.3s; 1642} 1643.site-footer.footer-compact .ft-links a:hover .ico-s { 1644 background:#C10DFF; 1645 color:#fff; 1646 transform:rotate(-4deg); 1647} 1648 1649/* 6. WhatsApp button */ 1650.site-footer.footer-compact .ft-wa-btn { 1651 display:inline-flex; 1652 align-items:center; 1653 gap:8px; 1654 background:#fff; 1655 color:#062437; 1656 padding:8px 18px; 1657 font-size:13.5px; 1658 border-radius:22px;
1659 font-weight:500; 1660 text-decoration:none; 1661 box-shadow:0 3px 8px -3px rgba(0,0,0,.45); 1662 transition:.25s; 1663} 1664.site-footer.footer-compact .ft-wa-btn:hover { 1665 transform:translateY(-2px); 1666 box-shadow:0 6px 14px -6px rgba(0,0,0,.55); 1667} 1668 1669/* 7. Sosial */ 1670.site-footer.footer-compact .ft-social { 1671 display:flex; 1672 gap:12px; 1673 margin-top:4px; 1674} 1675.site-footer.footer-compact .ft-social a { 1676 width:34px; 1677 height:34px; 1678 background:rgba(255,255,255,.08); 1679 display:flex; 1680 align-items:center; 1681 justify-content:center; 1682 border-radius:10px; 1683 color:#c6d6e0; 1684 transition:.25s; 1685} 1686.site-footer.footer-compact .ft-social a:hover { 1687 background:#C10DFF; 1688 color:#fff; 1689} 1690 1691/* 8. Alamat */ 1692.site-footer.footer-compact .ft-address { 1693 font-size:12.5px; 1694 line-height:1.44; 1695 color:#8fa4b1; 1696 max-width:330px; 1697 margin-top:12px; 1698} 1699.site-footer.footer-compact .ft-address .company { 1700 margin:0 0 4px; 1701 font-weight:600; 1702 color:#e5f2f7; 1703 font-size:12.8px; 1704} 1705 1706/* 9. Payments kecil & rapi */ 1707.site-footer.footer-compact .ft-payments { 1708 margin-top:14px; 1709 display:flex; 1710 flex-direction:column; 1711 gap:8px; 1712 max-width:350px; 1713} 1714.site-footer.footer-compact .ft-payments .pay-row { 1715 display:flex; 1716 gap:8px; 1717 flex-wrap:wrap; 1718} 1719.site-footer.footer-compact .ft-payments img { 1720 width:46px; 1721 height:32px; 1722 padding:3px 5px; 1723 background:#fff; 1724 border-radius:7px; 1725 object-fit:contain; 1726 box-shadow:0 2px 6px -3px rgba(0,0,0,.45); 1727 transition:.25s; 1728} 1729.site-footer.footer-compact .ft-payments img:hover { 1730 transform:translateY(-3px); 1731 box-shadow:0 5px 12px -6px rgba(0,0,0,.55); 1732} 1733 1734/* 10. Bottom bar */ 1735.site-footer.footer-compact .ft-bottom { 1736 border-top:1px solid rgba(255,255,255,.08); 1737 background:#041f31; 1738 padding:12px 0 16px; 1739 margin-top:4px; 1740} 1741.site-footer.footer-compact .ft-copy { 1742 font-size:11.5px; 1743 color:#8fa7b5; 1744 display:flex; 1745 flex-wrap:wrap; 1746 gap:12px; 1747 justify-content:center; 1748 line-height:1.3; 1749 margin:0; 1750} 1751.site-footer.footer-compact .ft-copy span { 1752 position:relative; 1753 padding:0 10px; 1754} 1755.site-footer.footer-compact .ft-copy span+span:before { 1756 content:""; 1757 position:absolute; 1758 left:0; 1759 top:50%; 1760 width:1px; 1761 height:12px; 1762 background:rgba(255,255,255,.14); 1763 transform:translateY(-50%); 1764} 1765@media (max-width:580px){ 1766 .site-footer.footer-compact .ft-top { padding:34px 0 18px; } 1767 .site-footer.footer-compact .ft-grid { gap:26px 20px; } 1768 .site-footer.footer-compact .ft-copy span { padding:0; } 1769 .site-footer.footer-compact .ft-copy span+span:before { display:none; } 1770} 1771.copy { 1772 margin-top:40px; 1773 border-top:1px solid #222; 1774 text-align:center; 1775 padding:18px 0; 1776 font-size:12px; 1777 color:#666; 1778} 1779 1780@media (max-width:760px) { 1781 .hero-text h1 { font-size:38px; } 1782 .hero { padding:0px 0 30px; } 1783 .topbar-inner { flex-direction:column; gap:4px; padding:6px 0; } 1784 .nav-inner { flex-wrap:wrap; } 1785 .mega-cats { gap:10px; } 1786 .product-grid { gap:16px; } 1787 .prod-card { padding:10px; } 1788} 1789 1790/* === PLAZA IT DARK #d9a837 FOOTER THEME === */ 1791.site-footer.footer-compact { 1792 --ft-bg-0:#051722; 1793 --ft-bg-1:#072534; 1794 --ft-bg-2:#0B2F40; 1795 --ft-bg-3:#103A4E; 1796 --ft-orange:#C10DFF; 1797 --ft-orange-dark:#C10DFF; 1798 --ft-text:#E9F2F6; 1799 --ft-text-sec:#B8CAD3; 1800 --ft-text-muted:#8FA4B1; 1801 --ft-divider:rgba(255,255,255,0.08); 1802 --ft-glow:rgba(245,202,86,0.18); 1803 --ft-glow-fade:rgba(245,202,86,0); 1804 color:var(--ft-text); 1805} 1806 1807/* BASE TEXT / LINKS (override sebelumnya jika perlu) */ 1808.site-footer.footer-compact .ft-title { color:#9CC5D7; } 1809.site-footer.footer-compact .ft-links a { color:var(--ft-text-sec); } 1810.site-footer.footer-compact .ft-links a:hover { color:#fff; } 1811.site-footer.footer-compact .ft-address { color:var(--ft-text-muted); } 1812.site-footer.footer-compact .ft-copy { color:#9AB2BD; } 1813 1814/* ICON BACKGROUND (kubu gelap) */ 1815.site-footer.footer-compact .ico-s { 1816 background:#0F3647; 1817 color:#9FC7DA; 1818} 1819.site-footer.footer-compact .ft-links a:hover .ico-s { 1820 background:var(--ft-orange); 1821 color:#fff; 1822} 1823 1824/* WhatsApp button biarkan putih agar kontras */ 1825.site-footer.footer-compact .ft-wa-btn { 1826 box-shadow:0 4px 12px -5px rgba(0,0,0,.6), 0 0 0 3px rgba(255,255,255,0.05); 1827} 1828.site-footer.footer-compact .ft-wa-btn:hover { 1829 box-shadow:0 8px 20px -8px rgba(0,0,0,.65), 0 0 0 3px rgba(245,202,86,0.25); 1830} 1831 1832/* Payment cards border halus */ 1833.site-footer.footer-compact .ft-payments img { 1834 box-shadow:0 2px 6px -3px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,0.05); 1835} 1836.site-footer.footer-compact .ft-payments img:hover { 1837 box-shadow:0 6px 14px -6px rgba(0,0,0,.65), 0 0 0 1px rgba(245,202,86,0.4); 1838} 1839 1840/* ========== VARIANT A: DIAGONAL EMBER (AKTIF) ========== */ 1841.site-footer.footer-compact.variant-ember { 1842 background: 1843 radial-gradient(circle at 78% 82%, var(--ft-glow) 0%, var(--ft-glow-fade) 55%), 1844 linear-gradient(140deg,var(--ft-bg-1) 0%, var(--ft-bg-2) 55%, var(--ft-bg-0) 100%); 1845} 1846.site-footer.footer-compact.variant-ember .ft-bottom { 1847 background:rgba(0,0,0,0.25); 1848 backdrop-filter:blur(2px); 1849} 1850 1851/* ========== VARIANT B: TOP BAR #d9a837 ========== */ 1852.site-footer.footer-compact.variant-bar { 1853 background:var(--ft-bg-1); 1854 position:relative; 1855} 1856.site-footer.footer-compact.variant-bar::before { 1857 content:""; 1858 position:absolute; 1859 left:0;top:0; 1860 width:100%;height:3px; 1861 background:linear-gradient(90deg,var(--ft-orange-dark),var(--ft-orange)); 1862 box-shadow:0 0 12px -2px var(--ft-orange); 1863} 1864.site-footer.footer-compact.variant-bar .ft-bottom { 1865 background:#041c28; 1866} 1867 1868/* ========== VARIANT C: RADIAL GLOW BAWAH ========== */ 1869.site-footer.footer-compact.variant-radial { 1870 background: 1871 radial-gradient(circle at 70% 90%, rgba(245,202,86,0.22) 0%, rgba(245,202,86,0.04) 38%, rgba(245,202,86,0) 65%), 1872 linear-gradient(150deg,#052131 0%, #062d3f 50%, #041924 100%); 1873} 1874.site-footer.footer-compact.variant-radial .ft-bottom { 1875 background:#041c29; 1876} 1877
1878/* ========== VARIANT D: LAYERED STRIPES ========== */ 1879.site-footer.footer-compact.variant-stripes { 1880 background: 1881 repeating-linear-gradient(180deg, rgba(255,255,255,0.015) 0 38px, rgba(255,255,255,0) 38px 76px), 1882 linear-gradient(130deg,#052636 0%, #062a3c 60%, #041d28 100%); 1883 position:relative; 1884} 1885.site-footer.footer-compact.variant-stripes::after { 1886 content:""; 1887 position:absolute; 1888 left:0;bottom:0; 1889 width:100%;height:110px; 1890 background:linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,0)); 1891 pointer-events:none; 1892} 1893.site-footer.footer-compact.variant-stripes .ft-bottom { 1894 background:#031821; 1895} 1896 1897/* Garis aksen oranye tipis di atas bottom */ 1898.site-footer.footer-compact.variant-stripes .ft-bottom { 1899 position:relative; 1900} 1901.site-footer.footer-compact.variant-stripes .ft-bottom::before { 1902 content:""; 1903 position:absolute; 1904 left:0;top:0; 1905 width:100%;height:2px; 1906 background:linear-gradient(90deg,rgba(245,202,86,0),rgba(245,202,86,.65),rgba(245,202,86,0)); 1907} 1908 1909/* ========== VARIANT E: GLASS PANEL ========== */ 1910.site-footer.footer-compact.variant-glass { 1911 background:linear-gradient(145deg,#051c29 0%, #041620 65%, #03121b 100%); 1912} 1913.site-footer.footer-compact.variant-glass .ft-grid { 1914 position:relative; 1915 padding:32px clamp(10px,2vw,24px); 1916 border-radius:22px;
1917 background:linear-gradient(140deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02)); 1918 border:1px solid rgba(255,255,255,0.08); 1919 box-shadow:0 12px 28px -12px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04) inset; 1920 backdrop-filter:blur(8px); 1921 -webkit-backdrop-filter:blur(8px); 1922} 1923.site-footer.footer-compact.variant-glass .ft-bottom { 1924 background:#02141d; 1925} 1926 1927/* HOVER ACCENTS */ 1928.site-footer.footer-compact a { 1929 transition:color .25s, background .25s, box-shadow .25s; 1930} 1931.site-footer.footer-compact .ft-links a:hover { 1932 box-shadow:0 2px 6px -3px rgba(0,0,0,.5), 1933 0 0 0 1px rgba(245,202,86,0.55); 1934} 1935.site-footer.footer-compact .ft-social a { 1936 background:#0E3445; 1937 color:#bcd4dd; 1938} 1939.site-footer.footer-compact .ft-social a:hover { 1940 background:var(--ft-orange); 1941 color:#fff; 1942 box-shadow:0 0 0 3px rgba(245,202,86,0.28); 1943} 1944 1945/* Divider di atas footer (opsional) */ 1946.footer-divider-top { 1947 height:1px; 1948 background:linear-gradient(90deg,transparent,rgba(255,255,255,0.15),transparent); 1949 width:100%; 1950 margin:0 0 4px; 1951} 1952 1953/* ==== MOBILE FOOTER COLLAPSE (Non-Homepage Only) ==== */ 1954 1955/* Desktop: wrapper tidak mengganggu grid layout */ 1956.ft-collapsible-content { 1957 display: contents; 1958} 1959 1960@media (max-width: 768px) { 1961 /* Wrapper untuk konten yang bisa collapse */ 1962 .ft-top.ft-collapsible .ft-collapsible-content { 1963 display: block; 1964 max-height: 80px; 1965 overflow: hidden; 1966 position: relative; 1967 transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); 1968 } 1969 1970 /* Fade gradient overlay */ 1971 .ft-top.ft-collapsible .ft-collapsible-content::after { 1972 content: ''; 1973 position: absolute; 1974 bottom: 0; 1975 left: 0; 1976 right: 0; 1977 height: 70px; 1978 background: linear-gradient(to bottom, transparent 0%, rgba(6, 43, 69, 0.6) 40%, #062b45 100%); 1979 pointer-events: none; 1980 opacity: 1; 1981 transition: opacity 0.3s ease; 1982 z-index: 1; 1983 } 1984 1985 /* State: Expanded */ 1986 .ft-top.ft-collapsible.expanded .ft-collapsible-content { 1987 max-height: 1500px; 1988 } 1989 1990 .ft-top.ft-collapsible.expanded .ft-collapsible-content::after { 1991 opacity: 0; 1992 } 1993 1994 /* Expand Button - Di luar collapsible, selalu visible */ 1995 .ft-expand-btn { 1996 display: flex;
1997 align-items: center; 1998 justify-content: center; 1999 gap: 8px; 2000 width: 100%; 2001 margin: -8px 0 0; 2002 padding: 12px 20px; 2003 background: rgba(255,255,255,0.12); 2004 border: 1px solid rgba(255,255,255,0.25); 2005 border-radius: 10px; 2006 color: #e5f2f7; 2007 font-size: 13.5px; 2008 font-weight: 500; 2009 cursor: pointer; 2010 transition: all 0.3s ease; 2011 outline: none; 2012 position: relative; 2013 z-index: 10; 2014 } 2015 2016 .ft-expand-btn:hover { 2017 background: rgba(255,255,255,0.18); 2018 border-color: rgba(255,255,255,0.35); 2019 color: #fff; 2020 transform: translateY(-2px); 2021 } 2022 2023 .ft-expand-btn:active { 2024 transform: scale(0.98); 2025 } 2026 2027 .ft-expand-btn svg { 2028 width: 18px; 2029 height: 18px; 2030 transition: transform 0.3s ease; 2031 fill: currentColor; 2032 } 2033 2034 .ft-top.ft-collapsible.expanded .ft-expand-btn { 2035 margin-top: 15px; 2036 } 2037 2038 .ft-top.ft-collapsible.expanded .ft-expand-btn svg { 2039 transform: rotate(180deg); 2040 } 2041} 2042 2043/* Desktop: hide expand button */ 2044@media (min-width: 769px) { 2045 .ft-expand-btn { 2046 display: none !important; 2047 } 2048} 2049 2050/* RESPONSIVE (jika background panel butuh penyesuaian) */ 2051@media (max-width:640px){ 2052 .site-footer.footer-compact.variant-glass .ft-grid { 2053 padding:26px 18px; 2054 } 2055} 2056 2057</style> 2058 <style data-inlined="bottom-nav.css"> 2059/* Mobile Bottom Navigation */ 2060.bottom-nav { 2061 position: fixed; 2062 bottom: 0; 2063 left: 0; 2064 right: 0; 2065 background: #ffffff; 2066 border-top: 1px solid #e5e7eb; 2067 z-index: 9998; /* Below chat widget */ 2068 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); 2069 display: none; /* Hidden on desktop */ 2070} 2071 2072.bottom-nav-inner { 2073 display: flex; 2074 justify-content: space-around; 2075 align-items: center; 2076 padding: 2px 0; 2077 max-width: 640px; 2078 margin: 0 auto; 2079} 2080 2081.bottom-nav-item { 2082 flex: 1; 2083 display: flex; 2084 flex-direction: column; 2085 align-items: center; 2086 justify-content: center; 2087 gap: 4px; 2088 padding: 8px 8px; 2089 background: none; 2090 border: none; 2091 cursor: pointer; 2092 text-decoration: none; 2093 color: #6b7280; 2094 transition: all 0.3s ease; 2095 position: relative; 2096} 2097 2098.bottom-nav-item:hover { 2099 color: #C10DFF; 2100} 2101 2102.bottom-nav-item.active { 2103 flex-direction: row; 2104 gap: 8px; 2105 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 100%); 2106 color: white; 2107 border-radius: 20px; 2108 padding: 8px 16px; 2109 box-shadow: 0 2px 8px rgba(245,202,86,0.3); 2110} 2111 2112.bottom-nav-item-icon { 2113 width: 24px; 2114 height: 24px; 2115 position: relative; 2116} 2117 2118.bottom-nav-item-icon svg { 2119 width: 100%; 2120 height: 100%; 2121} 2122 2123/* Icon variant switching */ 2124.bottom-nav-item .icon-outlined { 2125 display: block; 2126} 2127 2128.bottom-nav-item .icon-filled { 2129 display: none; 2130} 2131 2132.bottom-nav-item.active .icon-outlined { 2133 display: none; 2134} 2135 2136.bottom-nav-item.active .icon-filled { 2137 display: block; 2138} 2139 2140.bottom-nav-item-label { 2141 font-size: 12px; 2142 font-weight: 600; 2143 line-height: 1; 2144 display: none; 2145 white-space: nowrap; 2146} 2147 2148.bottom-nav-item.active .bottom-nav-item-label { 2149 display: block; 2150} 2151 2152/* Avatar styles for bottom-nav */ 2153.bottom-nav-avatar-img, 2154.bottom-nav-avatar-initials { 2155 width: 24px; 2156 height: 24px; 2157 border-radius: 50%; 2158 display: block; 2159} 2160 2161.bottom-nav-avatar-img { 2162 object-fit: cover; 2163} 2164 2165.bottom-nav-avatar-initials { 2166 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 100%); 2167 color: white; 2168 font-size: 10px; 2169 font-weight: 700; 2170 display: flex; 2171 align-items: center; 2172 justify-content: center; 2173 line-height: 1; 2174 letter-spacing: 0.5px; 2175} 2176 2177/* Hide SVG when avatar is shown */ 2178.bottom-nav-item-icon:has(.bottom-nav-avatar-img) .bottom-nav-svg-icon, 2179.bottom-nav-item-icon:has(.bottom-nav-avatar-initials) .bottom-nav-svg-icon { 2180 display: none; 2181} 2182 2183/* Active state adjustments for avatar */ 2184.bottom-nav-item.active .bottom-nav-avatar-initials { 2185 background: white; 2186 color: #C10DFF; 2187 border: 2px solid rgba(255, 255, 255, 0.8); 2188 box-sizing: border-box; 2189} 2190 2191.bottom-nav-item.active .bottom-nav-avatar-img { 2192 border: 2px solid rgba(255, 255, 255, 0.9); 2193 box-sizing: border-box; 2194} 2195 2196.bottom-nav-item-badge { 2197 position: absolute; 2198 top: -4px; 2199 right: -4px; 2200 background: #e2a642; 2201 color: white; 2202 border-radius: 10px; 2203 padding: 2px 5px; 2204 font-size: 10px; 2205 font-weight: 600; 2206 min-width: 18px;
2207 height: 18px; 2208 display: flex; 2209 align-items: center; 2210 justify-content: center; 2211 line-height: 1; 2212} 2213 2214/* Show on mobile only */ 2215@media (max-width: 640px) { 2216 .bottom-nav { 2217 display: block; 2218 } 2219 2220 /* Add bottom padding to body to prevent content being hidden */ 2221 body { 2222 padding-bottom: 60px; 2223 } 2224} 2225 2226/* Mobile User Sheet - Bottom Sheet for Profile Menu */ 2227.mobile-user-sheet { 2228 position: fixed; 2229 bottom: 0; 2230 left: 0; 2231 right: 0; 2232 z-index: 10000; 2233 pointer-events: none; 2234} 2235 2236.mobile-user-sheet:not([hidden]) { 2237 pointer-events: auto; 2238} 2239 2240.mobile-user-sheet-backdrop { 2241 position: fixed; 2242 inset: 0; 2243 background: rgba(0, 0, 0, 0.5); 2244 opacity: 0; 2245 transition: opacity 0.2s ease-out; 2246} 2247 2248.mobile-user-sheet.active .mobile-user-sheet-backdrop { 2249 opacity: 1; 2250} 2251 2252.mobile-user-sheet-content { 2253 position: absolute; 2254 bottom: 0; 2255 left: 0; 2256 right: 0; 2257 background: white; 2258 border-radius: 16px 16px 0 0; 2259 max-height: 80vh; 2260 overflow-y: auto; 2261 transform: translateY(100%); 2262 transition: transform 0.3s ease-out; 2263} 2264 2265.mobile-user-sheet.active .mobile-user-sheet-content { 2266 transform: translateY(0); 2267} 2268 2269.mobile-user-sheet-handle { 2270 width: 40px; 2271 height: 4px; 2272 background: #d1d5db; 2273 border-radius: 2px; 2274 margin: 12px auto 16px; 2275} 2276 2277.mobile-user-sheet-header { 2278 padding: 0 20px 16px; 2279 border-bottom: 1px solid #e5e7eb; 2280} 2281 2282.mobile-user-info { 2283 display: flex; 2284 align-items: center; 2285 gap: 12px; 2286} 2287 2288.mobile-user-avatar { 2289 width: 48px; 2290 height: 48px; 2291 background: linear-gradient(135deg, #335DD9 0%, #fff1cc 50%, #C10DFF 100%); 2292 border-radius: 50%; 2293 display: flex; 2294 align-items: center; 2295 justify-content: center; 2296 font-size: 24px; 2297 color: #C10DFF; 2298 font-weight: 600; 2299} 2300 2301.mobile-user-details { 2302 flex: 1; 2303} 2304 2305.mobile-user-name { 2306 font-weight: 600; 2307 font-size: 16px; 2308 color: #1f2937; 2309 margin-bottom: 4px; 2310} 2311 2312.mobile-user-email { 2313 font-size: 13px; 2314 color: #6b7280; 2315} 2316 2317.mobile-user-sheet-body { 2318 padding: 8px 0; 2319} 2320 2321.mobile-user-sheet-item { 2322 display: flex; 2323 align-items: center; 2324 gap: 12px; 2325 padding: 14px 20px; 2326 background: none; 2327 border: none; 2328 width: 100%; 2329 text-align: left; 2330 cursor: pointer; 2331 text-decoration: none; 2332 color: #374151; 2333 transition: background 0.2s; 2334} 2335 2336.mobile-user-sheet-item:hover { 2337 background: #f9fafb; 2338} 2339 2340.mobile-user-sheet-icon { 2341 font-size: 20px; 2342 width: 24px; 2343 text-align: center; 2344} 2345 2346.mobile-user-sheet-label { 2347 font-size: 15px; 2348 font-weight: 500; 2349} 2350 2351.mobile-user-sheet-divider { 2352 height: 1px; 2353 background: #e5e7eb; 2354 margin: 8px 0; 2355} 2356 2357@keyframes fadeIn { 2358 from { 2359 opacity: 0; 2360 } 2361 to { 2362 opacity: 1; 2363 } 2364} 2365 2366@keyframes slideUp { 2367 from { 2368 transform: translateY(100%); 2369 } 2370 to { 2371 transform: translateY(0); 2372 } 2373} 2374 2375/* Ensure chat widget works well with bottom nav */ 2376@media (max-width: 640px) { 2377 .chat-widget { 2378 bottom: 0 !important; /* Chat opens from bottom */ 2379 } 2380 2381 .chat-popup { 2382 /* Chat popup should be above bottom nav */ 2383 z-index: 9999; 2384 } 2385} 2386 2387</style> 2388 <style data-inlined="search-suggest.css"> 2389.search { 2390 position: relative; 2391} 2392 2393/* AI Bot Toggle Button */ 2394.ai-bot-toggle { 2395 display: flex; 2396 align-items: center; 2397 justify-content: center; 2398 background: transparent; 2399 border: none; 2400 padding: 8px; 2401 cursor: pointer; 2402 border-radius: 8px; 2403 transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 2404 margin-left: 4px; 2405 flex-shrink: 0; 2406 display: none; 2407} 2408 2409.ai-bot-toggle .bot-icon { 2410 color: #9ca3af; 2411 transition: all 0.3s ease; 2412} 2413 2414.ai-bot-toggle:hover .bot-icon { 2415 color: #6b7280; 2416 transform: scale(1.1); 2417} 2418 2419.ai-bot-toggle.active .bot-icon { 2420 color: #C10DFF; 2421 animation: botPulse 2s ease-in-out infinite; 2422} 2423 2424.ai-bot-toggle.active { 2425 background: rgba(245,202,86,0.1); 2426} 2427 2428.ai-bot-toggle.active:hover .bot-icon { 2429 color: #C10DFF; 2430} 2431 2432@keyframes botPulse { 2433 0%, 100% { transform: scale(1); } 2434 50% { transform: scale(1.12); } 2435} 2436 2437/* Search Suggest Box */ 2438.search-suggest { 2439 position: absolute; 2440 top: 100%; left: 0; right: 0; 2441 background: #fff; 2442 border: 1px solid #e1e4e8; 2443 border-top: none; 2444 box-shadow: 0 4px 12px rgba(0,0,0,0.08); 2445 max-height: 400px; 2446 overflow-y: auto; 2447 z-index: 100; 2448 padding: 0; 2449 font-size: 14px; 2450 border-radius: 0 0 8px 8px; 2451} 2452 2453.suggest-item { 2454 display: flex; 2455 align-items: center; 2456 gap: 8px; 2457 padding: 10px 16px; 2458 cursor: pointer; 2459 transition: all 0.2s ease; 2460 border-bottom: 1px solid #f1f5f9; 2461} 2462 2463.suggest-item:last-child { 2464 border-bottom: none; 2465} 2466 2467.suggest-item:hover, 2468.suggest-item.active { 2469 background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%); 2470 padding-left: 20px; 2471} 2472 2473.suggest-thumb { 2474 width: 42px; 2475 height: 42px; 2476 object-fit: cover; 2477 border: 1px solid #ddd; 2478 border-radius: 4px; 2479 background: #fafafa; 2480} 2481 2482.suggest-meta { flex: 1; min-width: 0; } 2483 2484.suggest-name { 2485 font-weight: 500;
2486 color: #222; 2487 line-height: 1.2; 2488 white-space: nowrap; 2489 overflow: hidden; 2490 text-overflow: ellipsis; 2491} 2492 2493.suggest-price { 2494 color: #e2a642; 2495 font-size: 12px; 2496 margin-top: 2px; 2497} 2498 2499/* AI Insights */ 2500.suggest-ai-insight { 2501 display: flex; 2502 align-items: center; 2503 gap: 8px; 2504 padding: 8px 12px; 2505 background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); 2506 border-bottom: 1px solid #bae6fd; 2507 color: #0369a1; 2508 font-size: 12px; 2509 font-weight: 500; 2510 line-height: 1.4; 2511} 2512 2513.suggest-ai-insight svg { 2514 flex-shrink: 0; 2515 opacity: 0.8; 2516} 2517 2518.suggest-ai-insight span { 2519 flex: 1; 2520} 2521 2522/* AI Progress States */ 2523.ai-progress-container { 2524 padding: 20px 16px; 2525 background: linear-gradient(135deg, #fafbff 0%, #f5f7ff 100%); 2526} 2527 2528.ai-progress-header { 2529 display: flex; 2530 align-items: center; 2531 gap: 10px; 2532 margin-bottom: 16px; 2533} 2534 2535.ai-progress-icon { 2536 width: 36px; 2537 height: 36px; 2538 display: flex; 2539 align-items: center; 2540 justify-content: center; 2541 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 2542 border-radius: 50%; 2543 color: #fff; 2544 font-size: 18px; 2545 animation: aiIconSpin 3s linear infinite; 2546} 2547 2548@keyframes aiIconSpin { 2549 0% { transform: rotate(0deg); } 2550 100% { transform: rotate(360deg); } 2551} 2552 2553.ai-progress-text { 2554 flex: 1; 2555} 2556 2557.ai-progress-title { 2558 font-weight: 600; 2559 color: #1e293b; 2560 font-size: 14px; 2561 margin-bottom: 2px; 2562} 2563 2564.ai-progress-subtitle { 2565 font-size: 12px; 2566 color: #64748b; 2567} 2568 2569/* Progress Steps */ 2570.ai-progress-steps { 2571 display: flex; 2572 flex-direction: column; 2573 gap: 10px; 2574 margin-top: 16px; 2575} 2576 2577.ai-progress-step { 2578 display: flex; 2579 align-items: center; 2580 gap: 10px; 2581 padding: 8px 12px; 2582 background: #fff; 2583 border-radius: 8px; 2584 border: 1px solid #e2e8f0; 2585 transition: all 0.3s ease; 2586} 2587 2588.ai-progress-step.active { 2589 border-color: #667eea; 2590 background: #f8f9ff; 2591 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); 2592} 2593 2594.ai-progress-step.completed { 2595 border-color: #10b981; 2596 background: #f0fdf4; 2597} 2598 2599.ai-step-icon { 2600 width: 20px; 2601 height: 20px; 2602 display: flex; 2603 align-items: center; 2604 justify-content: center; 2605 border-radius: 50%; 2606 font-size: 11px; 2607 flex-shrink: 0; 2608} 2609 2610.ai-progress-step.active .ai-step-icon { 2611 background: #667eea; 2612 color: #fff; 2613 animation: pulse 1.5s ease-in-out infinite; 2614} 2615 2616.ai-progress-step.completed .ai-step-icon { 2617 background: #10b981; 2618 color: #fff; 2619} 2620 2621.ai-progress-step.pending .ai-step-icon { 2622 background: #e2e8f0; 2623 color: #94a3b8; 2624} 2625 2626.ai-step-text { 2627 flex: 1; 2628 font-size: 13px; 2629 color: #475569; 2630 font-weight: 500; 2631} 2632 2633.ai-progress-step.completed .ai-step-text { 2634 color: #059669; 2635} 2636 2637/* AI Analysis Result Banner */ 2638.ai-result-banner { 2639 padding: 12px 16px; 2640 background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); 2641 border-bottom: 1px solid #86efac; 2642 display: flex; 2643 align-items: center; 2644 gap: 10px; 2645} 2646 2647.ai-result-icon { 2648 font-size: 20px; 2649 animation: bounceIn 0.5s ease; 2650} 2651 2652@keyframes bounceIn { 2653 0% { transform: scale(0); opacity: 0; } 2654 50% { transform: scale(1.1); } 2655 100% { transform: scale(1); opacity: 1; } 2656} 2657 2658.ai-result-text { 2659 flex: 1; 2660} 2661 2662.ai-result-title { 2663 font-weight: 600; 2664 color: #065f46; 2665 font-size: 13px; 2666 margin-bottom: 2px; 2667} 2668 2669.ai-result-description { 2670 font-size: 12px; 2671 color: #047857; 2672 opacity: 0.9; 2673} 2674 2675/* Product Items Section Header */ 2676.suggest-section-header { 2677 padding: 10px 16px; 2678 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 2679 border-bottom: 1px solid #e2e8f0; 2680} 2681 2682.suggest-header-main { 2683 display: flex; 2684 align-items: center; 2685 gap: 6px; 2686 margin-bottom: 4px; 2687} 2688 2689.suggest-header-icon { 2690 font-size: 14px; 2691} 2692 2693.suggest-header-text { 2694 font-size: 12px; 2695 font-weight: 600; 2696 color: #64748b; 2697 text-transform: uppercase; 2698 letter-spacing: 0.5px; 2699} 2700 2701.suggest-header-hint { 2702 font-size: 11px; 2703 color: #94a3b8; 2704 font-weight: 400; 2705 text-transform: none; 2706 letter-spacing: 0; 2707 display: flex; 2708 align-items: center; 2709 gap: 4px; 2710} 2711 2712.suggest-header-hint kbd { 2713 display: inline-block; 2714 padding: 2px 6px; 2715 font-size: 10px; 2716 font-weight: 600; 2717 line-height: 1; 2718 color: #475569; 2719 background: #fff; 2720 border: 1px solid #cbd5e1; 2721 border-radius: 4px; 2722 box-shadow: 0 1px 2px rgba(0,0,0,0.05); 2723 font-family: 'Segoe UI', monospace; 2724} 2725 2726/* Footer Hint */ 2727.suggest-footer-hint { 2728 display: flex; 2729 align-items: center; 2730 justify-content: center; 2731 padding: 12px 16px; 2732 background: linear-gradient(135deg, #fef3c7 0%, #C10DFF 100%); 2733 border-top: 1px solid #C10DFF; 2734 color: #ae7e21; 2735 font-size: 12px; 2736 font-weight: 500; 2737 text-align: center; 2738 cursor: default; 2739} 2740 2741.suggest-footer-hint svg { 2742 color: #C10DFF; 2743 flex-shrink: 0; 2744} 2745 2746.suggest-footer-hint kbd { 2747 display: inline-block; 2748 padding: 3px 7px; 2749 margin: 0 3px; 2750 font-size: 11px; 2751 font-weight: 600; 2752 line-height: 1; 2753 color: #765212; 2754 background: #fff; 2755 border: 1px solid #C10DFF; 2756 border-radius: 4px; 2757 box-shadow: 0 1px 3px rgba(245,202,86,0.2); 2758 font-family: 'Segoe UI', monospace; 2759} 2760 2761.suggest-footer-link { 2762 color: #C10DFF; 2763 font-weight: 600; 2764 text-decoration: underline; 2765 text-underline-offset: 2px; 2766 text-decoration-thickness: 1px; 2767 cursor: pointer; 2768 transition: all 0.2s ease; 2769} 2770 2771.suggest-footer-link:hover { 2772 color: #c9832a; 2773 text-decoration-thickness: 2px; 2774 text-shadow: 0 0 8px rgba(245,202,86,0.3); 2775} 2776 2777/* Loading state */ 2778.suggest-loading { 2779 display: flex; 2780 align-items: center; 2781 justify-content: center; 2782 gap: 8px; 2783 padding: 16px 12px; 2784 color: #64748b; 2785 font-size: 13px; 2786} 2787 2788@keyframes pulse { 2789 0%, 100% { opacity: 1; } 2790 50% { opacity: 0.5; } 2791} 2792 2793.suggest-loading { 2794 animation: pulse 1.5s ease-in-out infinite; 2795} 2796 2797/* Responsive Mobile Styles */ 2798@media (max-width: 768px) { 2799 .ai-bot-toggle { 2800 padding: 6px; 2801 margin-left: 0; 2802 } 2803 2804 .ai-bot-toggle .bot-icon {
2805 width: 20px; 2806 height: 20px; 2807 } 2808 2809 .search-icon-btn { 2810 margin-right: 4px; 2811 } 2812 2813 .search-icon-btn svg { 2814 width: 18px; 2815 height: 18px; 2816 } 2817 2818 .search-suggest { 2819 max-height: 350px; 2820 } 2821 2822 /* Mobile hint adjustments */ 2823 .suggest-header-hint { 2824 font-size: 10px; 2825 } 2826 2827 .suggest-header-hint kbd { 2828 padding: 1px 4px; 2829 font-size: 9px; 2830 } 2831 2832 .suggest-footer-hint { 2833 padding: 10px 12px; 2834 font-size: 11px; 2835 } 2836 2837 .suggest-footer-hint svg { 2838 width: 14px; 2839 height: 14px; 2840 } 2841 2842 .suggest-footer-hint kbd { 2843 padding: 2px 5px; 2844 font-size: 10px; 2845 } 2846 2847 .ai-progress-container { 2848 padding: 16px 12px; 2849 } 2850 2851 .ai-progress-icon { 2852 width: 32px; 2853 height: 32px; 2854 font-size: 16px; 2855 } 2856 2857 .ai-progress-title { 2858 font-size: 13px; 2859 } 2860 2861 .ai-progress-subtitle { 2862 font-size: 11px; 2863 } 2864 2865 .ai-progress-step { 2866 padding: 6px 10px; 2867 } 2868 2869 .ai-step-text { 2870 font-size: 12px; 2871 } 2872 2873 .suggest-item { 2874 padding: 8px 12px; 2875 } 2876 2877 .suggest-thumb { 2878 width: 36px; 2879 height: 36px; 2880 } 2881 2882 .suggest-name { 2883 font-size: 13px; 2884 } 2885 2886 .suggest-price { 2887 font-size: 11px; 2888 } 2889} 2890 2891@media (max-width: 480px) { 2892 .ai-bot-toggle { 2893 padding: 4px; 2894 } 2895 2896 .ai-bot-toggle .bot-icon { 2897 width: 18px; 2898 height: 18px; 2899 } 2900 2901 .search-icon-btn svg { 2902 width: 16px; 2903 height: 16px; 2904 } 2905 2906 /* Ultra-compact hints for small screens */ 2907 .suggest-header-hint { 2908 display: none; /* Hide header hint on very small screens */ 2909 } 2910 2911 .suggest-footer-hint { 2912 padding: 8px 10px; 2913 font-size: 10px; 2914 flex-direction: column; 2915 gap: 4px; 2916 } 2917 2918 .suggest-footer-hint svg { 2919 display: none; /* Hide icon to save space */ 2920 } 2921 2922 .suggest-footer-hint kbd { 2923 padding: 2px 4px; 2924 font-size: 9px; 2925 } 2926 2927 .ai-progress-steps { 2928 gap: 8px; 2929 } 2930 2931 .ai-step-text { 2932 font-size: 11px; 2933 } 2934} 2935 2936/* AI Search Prompt Box */ 2937.ai-search-prompt { 2938 padding: 24px; 2939 background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); 2940} 2941 2942.ai-prompt-header { 2943 display: flex; 2944 align-items: center; 2945 gap: 12px; 2946 margin-bottom: 16px; 2947} 2948 2949.ai-prompt-icon { 2950 width: 48px; 2951 height: 48px; 2952 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 100%); 2953 border-radius: 12px; 2954 display: flex; 2955 align-items: center; 2956 justify-content: center; 2957 font-size: 24px; 2958 box-shadow: 0 4px 12px rgba(245,202,86,0.3); 2959} 2960 2961.ai-prompt-text { 2962 flex: 1; 2963} 2964 2965.ai-prompt-title { 2966 font-size: 16px; 2967 font-weight: 700; 2968 color: #1f2937; 2969 margin-bottom: 4px; 2970} 2971 2972.ai-prompt-subtitle { 2973 font-size: 13px; 2974 color: #6b7280; 2975} 2976 2977.ai-prompt-query { 2978 background: white; 2979 border: 2px solid #C10DFF; 2980 border-radius: 8px; 2981 padding: 12px 16px; 2982 margin-bottom: 16px; 2983} 2984 2985.ai-query-label { 2986 font-size: 11px; 2987 font-weight: 600; 2988 color: #C10DFF; 2989 text-transform: uppercase; 2990 letter-spacing: 0.5px; 2991 margin-bottom: 4px; 2992} 2993 2994.ai-query-text { 2995 font-size: 15px; 2996 font-weight: 600; 2997 color: #1f2937; 2998} 2999 3000.ai-search-trigger-btn { 3001 width: 100%; 3002 padding: 14px 20px; 3003 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 100%); 3004 color: white; 3005 border: none; 3006 border-radius: 10px; 3007 font-size: 15px; 3008 font-weight: 700; 3009 cursor: pointer; 3010 display: flex; 3011 align-items: center; 3012 justify-content: center; 3013 gap: 8px; 3014 transition: all 0.3s ease; 3015 box-shadow: 0 4px 12px rgba(245,202,86,0.4); 3016} 3017 3018.ai-search-trigger-btn:hover { 3019 transform: translateY(-2px); 3020 box-shadow: 0 6px 20px rgba(245,202,86,0.5); 3021} 3022 3023.ai-search-trigger-btn:active { 3024 transform: translateY(0); 3025} 3026 3027.ai-btn-icon { 3028 font-size: 18px; 3029 animation: sparkle 2s ease-in-out infinite; 3030} 3031 3032@keyframes sparkle { 3033 0%, 100% { opacity: 1; transform: scale(1); } 3034 50% { opacity: 0.7; transform: scale(1.2); } 3035} 3036 3037.ai-btn-text { 3038 font-size: 15px; 3039} 3040 3041.ai-prompt-footer { 3042 margin-top: 12px; 3043 text-align: center; 3044 font-size: 12px; 3045 color: #9ca3af; 3046 font-style: italic; 3047} 3048 3049/* ==================== TRENDING SEARCH STYLES ==================== */ 3050 3051.search-suggest.trending-mode { 3052 border-top: 3px solid #C10DFF; 3053 max-width: 900px; 3054 min-width: 600px; 3055} 3056 3057/* Two-column container */ 3058.trending-container { 3059 display: flex; 3060 gap: 0; 3061 padding: 0 !important; 3062} 3063 3064.trending-column { 3065 flex: 1; 3066 min-width: 0; 3067 padding: 0 !important; 3068} 3069 3070.trending-column.trending-searches { 3071 border-right: 2px solid #e2e8f0; padding: 0 !important; 3072} 3073 3074/* Mobile: stack columns vertically */ 3075@media (max-width: 768px) { 3076 .search-suggest.trending-mode { 3077 min-width: auto; 3078 max-width: 100%; 3079 } 3080 3081 .trending-container { 3082 flex-direction: column; 3083 } 3084 3085 .trending-column.trending-searches {
3086 border-right: none; 3087 border-bottom: 2px solid #e2e8f0; 3088 } 3089} 3090 3091.trending-header { 3092 display: flex; 3093 align-items: center; 3094 gap: 8px; 3095 padding: 14px 16px; 3096 border-bottom: 1px solid #e2e8f0; 3097 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 100%); 3098 color: white; 3099 position: sticky; 3100 top: 0; 3101 z-index: 10; 3102} 3103 3104.trending-icon { 3105 font-size: 20px; 3106 animation: trendingPulse 2s infinite; 3107} 3108 3109@keyframes trendingPulse { 3110 0%, 100% { 3111 transform: scale(1); 3112 filter: drop-shadow(0 0 0 rgba(245,202,86,0)); 3113 } 3114 50% { 3115 transform: scale(1.15); 3116 filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8)); 3117 } 3118} 3119 3120.trending-title { 3121 font-weight: 700; 3122 font-size: 14px; 3123 letter-spacing: 0.3px; 3124} 3125 3126.trending-subtitle { 3127 font-size: 11px; 3128 opacity: 0.9; 3129 margin-left: auto; 3130 font-style: italic; 3131} 3132 3133/* Trending searches item */ 3134.trending-item { 3135 display: flex; 3136 align-items: center; 3137 gap: 12px; 3138 padding: 12px 16px; 3139 cursor: pointer; 3140 transition: all 0.2s ease; 3141 border-bottom: 1px solid #f1f5f9; 3142 position: relative; 3143} 3144 3145.trending-item:hover { 3146 background: linear-gradient(90deg, #fff7ed 0%, #ffedd5 100%); 3147 padding-left: 20px; 3148} 3149 3150.trending-item:hover::before { 3151 content: ''; 3152 position: absolute; 3153 left: 0; 3154 top: 0; 3155 bottom: 0; 3156 width: 4px; 3157 background: linear-gradient(180deg, #C10DFF 0%, #C10DFF 100%); 3158} 3159 3160.trending-item:last-child { 3161 border-bottom: none; 3162} 3163 3164.trending-rank { 3165 font-size: 20px; 3166 flex-shrink: 0; 3167 width: 32px; 3168 text-align: center; 3169} 3170 3171.trending-query { 3172 flex: 1; 3173 font-size: 14px; 3174 color: #1f2937; 3175 font-weight: 500; 3176 line-height: 1.4; 3177} 3178 3179.trending-category { 3180 font-size: 11px; 3181 color: #C10DFF; 3182 background: #fff7ed; 3183 padding: 3px 10px; 3184 border-radius: 12px; 3185 white-space: nowrap; 3186 font-weight: 500; 3187 border: 1px solid #fff1cc; 3188} 3189 3190/* Trending products item */ 3191.trending-product-item { 3192 display: flex; 3193 align-items: center; 3194 gap: 12px; 3195 padding: 12px 16px; 3196 cursor: pointer; 3197 transition: all 0.2s ease; 3198 border-bottom: 1px solid #f1f5f9; 3199 position: relative; 3200} 3201 3202.trending-product-item:hover { 3203 background: linear-gradient(90deg, #fef3c7 0%, #C10DFF 100%); 3204 padding-left: 20px; 3205} 3206 3207.trending-product-item:hover::before { 3208 content: ''; 3209 position: absolute; 3210 left: 0; 3211 top: 0; 3212 bottom: 0; 3213 width: 4px; 3214 background: linear-gradient(180deg, #C10DFF 0%, #C10DFF 100%); 3215} 3216 3217.trending-product-item:last-child { 3218 border-bottom: none; 3219} 3220 3221.trending-product-thumb { 3222 width: 48px; 3223 height: 48px; 3224 object-fit: cover; 3225 border-radius: 8px; 3226 border: 1px solid #e5e7eb; 3227 flex-shrink: 0; 3228 background: white; 3229} 3230 3231.trending-product-info { 3232 flex: 1; 3233 min-width: 0; 3234} 3235 3236.trending-product-name { 3237 font-size: 13px; 3238 font-weight: 400; 3239 color: #1f2937; 3240 line-height: 1.4; 3241 margin-bottom: 0; 3242 padding: 0 !important; 3243 overflow: hidden; 3244 text-overflow: ellipsis; 3245 display: -webkit-box; 3246 -webkit-line-clamp: 2; 3247 -webkit-box-orient: vertical; 3248} 3249 3250.trending-product-price { 3251 font-size: 12px; 3252 font-weight: 700; 3253 color: #C10DFF; 3254 padding: 4px 0 !important; 3255} 3256.trending-product { padding: 0;} 3257/* ==================== END TRENDING STYLES ==================== */ 3258 3259</style> 3260 <style data-inlined="product-detail.css"> 3261/* === PRODUCT DETAIL v3 (Tokopedia-like) â Full stylesheet === */ 3262 3263/* Root / Container */ 3264.product-single { 3265 --pd-gap: 2rem; 3266 --pd-border: #e2e8f0; 3267 --pd-bg: #ffffff; 3268 --pd-muted: #64748b; 3269 --pd-dark: #0f172a; 3270 --pd-accent: #0d6efd; /* tema utama situs */ 3271 --pd-radius: 12px; 3272 --pd-soft: #f8fafc; 3273 --pd-soft-border: #e5e7eb; 3274 --pd-danger: #e2a642; 3275 --pd-success-bg: #dcfce7; 3276 --pd-success-text: #166534; 3277 --pd-danger-bg: #fee2e2; 3278 --pd-danger-text: #c9832a; 3279 --pd-shadow: 0 6px 20px -10px rgba(0,0,0,.12); 3280 --pd-transition: .25s cubic-bezier(.4,.0,.2,1); 3281 max-width:1280px; 3282 margin-inline:auto; 3283} 3284 3285/* === Sticky Product Header Bar (muncul saat scroll) === */ 3286.pd-sticky-header { 3287 position: fixed; 3288 top: 0; 3289 left: 0; 3290 right: 0; 3291 z-index: 998; 3292 background: rgba(255, 255, 255, 0.98); 3293 backdrop-filter: blur(12px); 3294 -webkit-backdrop-filter: blur(12px); 3295 border-bottom: 1px solid rgba(226, 232, 240, 0.8); 3296 box-shadow: 0 4px 16px -4px rgba(0, 0, 0, 0.08); 3297 transform: translateY(-100%); 3298 opacity: 0; 3299 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), 3300 opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); 3301 pointer-events: none; 3302} 3303 3304.pd-sticky-header.visible { 3305 transform: translateY(0); 3306 opacity: 1;
3307 pointer-events: auto; 3308} 3309 3310.pd-sticky-header-container { 3311 max-width: 1280px; 3312 margin: 0 auto; 3313 padding: 12px 20px; 3314 display: flex; 3315 align-items: center; 3316 gap: 16px; 3317} 3318 3319.pd-sticky-thumb { 3320 width: 48px; 3321 height: 48px; 3322 flex-shrink: 0; 3323 border-radius: 8px; 3324 border: 1px solid #e2e8f0; 3325 overflow: hidden; 3326 background: #f8fafc; 3327} 3328 3329.pd-sticky-thumb img { 3330 width: 100%; 3331 height: 100%; 3332 object-fit: cover; 3333 display: block; 3334} 3335 3336.pd-sticky-info { 3337 flex: 1; 3338 min-width: 0; 3339 display: flex; 3340 flex-direction: column; 3341 gap: 4px; 3342} 3343 3344.pd-sticky-title { 3345 font-size: 0.9rem; 3346 font-weight: 600; 3347 color: #0f172a; 3348 line-height: 1.3; 3349 white-space: nowrap; 3350 overflow: hidden; 3351 text-overflow: ellipsis; 3352 margin: 0; 3353} 3354 3355.pd-sticky-price-row { 3356 display: flex; 3357 align-items: center; 3358 gap: 8px; 3359 flex-wrap: wrap; 3360} 3361 3362.pd-sticky-price { 3363 font-size: 1.1rem; 3364 font-weight: 700; 3365 color: #0d6efd; 3366 line-height: 1; 3367} 3368 3369.pd-sticky-price-original { 3370 font-size: 0.75rem; 3371 color: #94a3b8; 3372 text-decoration: line-through; 3373 line-height: 1; 3374} 3375 3376.pd-sticky-discount { 3377 background: #e2a642; 3378 color: #fff; 3379 font-size: 0.65rem; 3380 font-weight: 600; 3381 padding: 2px 6px; 3382 border-radius: 4px; 3383 letter-spacing: 0.3px; 3384 line-height: 1; 3385} 3386 3387.pd-sticky-actions { 3388 display: flex; 3389 gap: 8px; 3390 flex-shrink: 0; 3391 align-items: center; 3392} 3393 3394.pd-sticky-share-btn { 3395 padding: 8px 16px; 3396 border-radius: 8px; 3397 border: 1px solid #e2e8f0; 3398 background: #fff; 3399 color: #64748b; 3400 cursor: pointer; 3401 transition: all 0.2s ease; 3402 display: inline-flex; 3403 align-items: center; 3404 justify-content: center; 3405 flex-shrink: 0; 3406} 3407 3408.pd-sticky-share-btn:hover { 3409 background: #f8fafc; 3410 border-color: #cbd5e1; 3411 color: #0f172a; 3412 transform: translateY(-1px); 3413} 3414 3415.pd-sticky-share-btn:active { 3416 transform: translateY(0); 3417} 3418 3419.pd-sticky-share-btn svg { 3420 display: block; 3421 width: 16px; 3422 height: 16px; 3423} 3424 3425.pd-sticky-btn-chat svg { 3426 width: 16px !important; 3427 height: 16px !important; 3428 max-width: none !important; 3429 margin-right: 4px; 3430 flex-shrink: 0; 3431} 3432 3433.pd-sticky-btn { 3434 padding: 8px 16px; 3435 border-radius: 8px; 3436 font-size: 0.85rem; 3437 font-weight: 600; 3438 cursor: pointer; 3439 transition: all 0.2s ease; 3440 border: 0; 3441 display: inline-flex; 3442 align-items: center; 3443 justify-content: center; 3444 white-space: nowrap; 3445} 3446 3447.pd-sticky-btn-cart { 3448 background: #fff; 3449 color: #0d6efd; 3450 border: 1px solid #0d6efd; 3451} 3452 3453.pd-sticky-btn-cart:hover { 3454 background: rgba(13, 110, 253, 0.06); 3455} 3456 3457.pd-sticky-btn-chat { 3458 background: #fff; 3459 color: #64748b; 3460 border: 1px solid #cbd5e1; 3461 flex: 0.8; 3462} 3463 3464.pd-sticky-btn-chat:hover { 3465 background: #f8fafc; 3466 border-color: #94a3b8; 3467 color: #475569; 3468} 3469 3470.pd-sticky-btn-buy { 3471 background: #0d6efd; 3472 color: #fff; 3473 border: 1px solid #0d6efd; 3474 box-shadow: 0 2px 8px -2px rgba(13, 110, 253, 0.4); 3475} 3476 3477.pd-sticky-btn-buy:hover { 3478 filter: brightness(0.95); 3479} 3480 3481.pd-sticky-btn:disabled { 3482 opacity: 0.5; 3483 cursor: not-allowed; 3484} 3485 3486/* Hide on mobile (use existing FAB instead) */ 3487@media (max-width: 880px) { 3488 .pd-sticky-header { 3489 display: none; 3490 } 3491} 3492 3493/* === PRINT STYLES (for PDF Export) === */ 3494@media print { 3495 /* Reset & Base */ 3496 * { 3497 -webkit-print-color-adjust: exact !important; 3498 print-color-adjust: exact !important; 3499 } 3500 3501 body { 3502 background: white !important; 3503 margin: 0; 3504 padding: 0; 3505 } 3506 3507 /* Hide all navigation, buttons, and interactive elements */ 3508 nav, header, footer, 3509 nav *, header *, footer *, 3510 .navbar, .navbar *, 3511 .nav-item, .nav-link, 3512 .pd-sticky-header, 3513 .pd-sticky-header.visible, 3514 .pd-breadcrumb, 3515 .pd-sidebar, 3516 .pd-mobile-fab, 3517 .pd-share-btn, 3518 .pd-sticky-share-btn, 3519 #pd-share-btn, 3520 .share-button, 3521 [data-share-btn], 3522 button, 3523 .btn-add-cart, 3524 .btn-buy, 3525 .pd-actions, 3526 .pd-compare-section, 3527 .pd-compare-btn, 3528 .pd-compare-info, 3529 #pd-compare-btn, 3530 .compare-note, 3531 .compare-container, 3532 .compare-dropdown, 3533 .compare-panel, 3534 .compare-wrapper, 3535 [data-compare], 3536 [id*="compare"], 3537 [class*="compare"], 3538 .pd-thumb-gallery, 3539 .pd-installment-card, 3540 .pd-installment-card2, 3541 .inst-alt-banner--compact, 3542 .qoala-banner-card, 3543 .tradein-compact-card, 3544 .pwp-deals-section, 3545 #pwp-deals-section, 3546 .pwp-trigger-badge, 3547 .pd-related-full, 3548 #pd-similar, 3549 .pd-similar-strip, 3550 .pd-scroll-top, 3551 .stock-info-btn, 3552 .stock-popover, 3553 .pd-countdown-modern, 3554 .pd-share-modal-overlay, 3555 [id*="promo-"], 3556 .bottom-nav, 3557 .pd-buy-form, 3558 .pd-buy-card, 3559 .pd-extra-meta, 3560 .pd-reviews-section, 3561 #reviews, 3562 hr, 3563 .divider, 3564 .separator { 3565 display: none !important; 3566 visibility: hidden !important; 3567 opacity: 0 !important; 3568 height: 0 !important; 3569 max-height: 0 !important; 3570 overflow: hidden !important; 3571 margin: 0 !important; 3572 padding: 0 !important; 3573 border: none !important; 3574 } 3575 3576 /* Extra aggressive reset for any remaining borders */ 3577 .pd-info-card *, 3578 .pd-meta-inline *, 3579 .pd-variants * { 3580 border: none !important; 3581 border-top: none !important; 3582 border-bottom: none !important; 3583 } 3584 3585 /* Layout restructure */ 3586 .product-single { 3587 max-width: 100% !important; 3588 margin: 0 !important; 3589 padding: 0 !important; 3590 box-shadow: none !important; 3591 } 3592 3593 /* Reset all shadows and borders for print */ 3594 * { 3595 box-shadow: none !important; 3596 } 3597 3598 .pd-top-layout { 3599 display: grid !important; 3600 grid-template-columns: 280px 1fr !important; 3601 gap: 24px !important; 3602 margin: 0 0 24px 0 !important; 3603 padding: 0 !important; 3604 page-break-inside: avoid;
3605 align-items: start !important; 3606 } 3607 3608 /* Print header with logo - centered and larger */ 3609 .product-single::before { 3610 content: "" !important; 3611 display: block !important; 3612 width: 150px !important; 3613 height: 50px !important; 3614 background: url('/assets/img/logo.png') no-repeat center !important; 3615 background-size: contain !important; 3616 margin: 0 auto 20px !important; 3617 visibility: visible !important; 3618 opacity: 1 !important; 3619 } 3620 3621 /* Media card - left column */ 3622 .pd-media-card { 3623 border: none !important; 3624 padding: 0 !important; 3625 margin: 0 !important; 3626 text-align: center; 3627 page-break-inside: avoid; 3628 } 3629 3630 /* Hide desktop image, use mobile slider for both desktop and mobile */ 3631 .pd-main-image { 3632 display: none !important; 3633 } 3634 3635 /* Mobile slider - show first image only in print */ 3636 .pd-mobile-slider { 3637 display: block !important; 3638 overflow: visible !important; 3639 } 3640 3641 .pd-mslide { 3642 display: none !important; 3643 } 3644 3645 .pd-mslide:first-child { 3646 display: block !important; 3647 border: 1px solid #e2e8f0 !important; 3648 max-width: 280px !important; 3649 margin: 0 auto !important; 3650 border-radius: 8px !important; 3651 } 3652 3653 .pd-mslide:first-child img { 3654 max-height: 280px !important; 3655 width: 100% !important; 3656 height: auto !important; 3657 object-fit: contain !important; 3658 } 3659 3660 /* Info card - right column */ 3661 .pd-info-card { 3662 border: none !important; 3663 border-top: none !important; 3664 border-bottom: none !important; 3665 padding: 0 !important; 3666 margin: 0 !important; 3667 background: white !important; 3668 box-shadow: none !important; 3669 } 3670 3671 /* Title */ 3672 .pd-title-row { 3673 display: block !important; 3674 margin-bottom: 10px !important; 3675 border: none !important; 3676 border-top: none !important; 3677 border-bottom: none !important; 3678 padding: 0 !important; 3679 box-shadow: none !important; 3680 text-decoration: none !important; 3681 } 3682 3683 /* Hide share button in print */ 3684 .pd-title-row .pd-share-btn, 3685 .pd-title-row #pd-share-btn { 3686 display: none !important; 3687 } 3688 3689 .pd-title { 3690 font-size: 13pt !important; 3691 color: #000 !important; 3692 margin: 0 0 10px 0 !important; 3693 page-break-after: avoid; 3694 font-weight: 700 !important; 3695 line-height: 1.3 !important; 3696 border: none !important; 3697 border-top: none !important; 3698 border-bottom: none !important; 3699 padding-bottom: 0 !important; 3700 box-shadow: none !important; 3701 text-decoration: none !important; 3702 } 3703 3704 /* Price and QR container - side by side */ 3705 .pd-price-qr-container { 3706 display: flex !important; 3707 gap: 16px !important; 3708 align-items: flex-start !important; 3709 margin-bottom: 14px !important; 3710 box-shadow: none !important; 3711 border: none !important; 3712 text-decoration: none !important; 3713 } 3714 3715 /* Price block - left side */ 3716 .pd-price-block { 3717 flex: 1 !important; 3718 margin: 0 !important; 3719 padding-bottom: 0 !important; 3720 border-bottom: none !important; 3721 box-shadow: none !important; 3722 } 3723 3724 .pd-price-row { 3725 margin-bottom: 8px !important; 3726 border: none !important; 3727 border-top: none !important; 3728 border-bottom: none !important; 3729 padding: 0 !important; 3730 } 3731 3732 /* Hide all empty or unnecessary elements in price row */ 3733 .pd-price-row > span:empty, 3734 .pd-price-row > .sold-inline, 3735 #pd-sold-inline { 3736 display: none !important; 3737 } 3738 3739 .pd-price-current { 3740 font-size: 18pt !important; 3741 color: #0d6efd !important; 3742 } 3743 3744 /* Show price range for variant products in print */ 3745 .pd-price-current[data-price-range]::after { 3746 content: attr(data-price-range) !important; 3747 display: block !important; 3748 font-size: 11pt !important; 3749 color: #64748b !important; 3750 margin-top: 4px !important; 3751 font-weight: normal !important; 3752 } 3753 3754 /* Hide 'Range:' prefix text */ 3755 .pd-price-current[data-price-range]::after { 3756 content: attr(data-price-range) !important; 3757 } 3758 3759 .pd-price-original { 3760 font-size: 12pt !important; 3761 color: #64748b !important; 3762 } 3763 3764 .pd-discount-badge { 3765 font-size: 10pt !important; 3766 padding: 2px 6px !important; 3767 } 3768 3769 /* QR Code section - right side beside price */ 3770 .pd-qr-print { 3771 display: block !important; 3772 flex: 0 0 100px !important; 3773 margin: 0 !important; 3774 padding: 8px !important; 3775 background: #f8fafc !important; 3776 border-radius: 8px !important; 3777 text-align: center !important; 3778 page-break-inside: avoid !important; 3779 border: 1px solid #cbd5e1 !important; 3780 } 3781 3782 .pd-qr-label { 3783 font-size: 6.5pt !important; 3784 color: #64748b !important; 3785 margin-bottom: 5px !important; 3786 line-height: 1.2 !important; 3787 font-weight: 600 !important; 3788 } 3789 3790 .pd-qr-image { 3791 width: 80px !important; 3792 height: 80px !important; 3793 display: block !important; 3794 margin: 0 auto !important; 3795 } 3796 3797 /* Meta info */ 3798 .pd-meta-inline { 3799 display: flex !important; 3800 gap: 12px !important; 3801 margin-bottom: 0 !important; 3802 padding-bottom: 14px !important; 3803 font-size: 10pt !important; 3804 border: none !important; 3805 border-top: none !important; 3806 border-bottom: none !important; 3807 padding-top: 0 !important; 3808 box-shadow: none !important; 3809 text-decoration: none !important; 3810 background: transparent !important; 3811 } 3812 3813 /* Hide stock badge in print */ 3814 .stock-badge, 3815 .stock-info-btn { 3816 display: none !important; 3817 } 3818 3819 .sku { 3820 color: #64748b !important; 3821 } 3822 3823 /* Hide sold badge if empty/zero */ 3824 .sold-inline { 3825 display: none !important; 3826 } 3827 3828 /* Hide variant summary section in print */ 3829 .pd-variant-combined, 3830 #pd-variant-combined, 3831 .pd-variant-summary, 3832 [data-role="sold-chip"] { 3833 display: none !important; 3834 } 3835 3836 /* Hide compare section completely in print */ 3837 .pd-compare-section, 3838 .pd-compare-btn, 3839 #pd-compare-btn, 3840 .pd-compare-info, 3841 .compare-note, 3842 .compare-text, 3843 .compare-remove { 3844 display: none !important; 3845 visibility: hidden !important; 3846 height: 0 !important; 3847 margin: 0 !important; 3848 padding: 0 !important; 3849 overflow: hidden !important; 3850 } 3851 3852 /* Variants - show all options with selected highlighted */ 3853 .pd-variants { 3854 margin: 0 !important; 3855 padding: 0 !important; 3856 page-break-inside: avoid !important; 3857 border: none !important; 3858 border-top: none !important; 3859 border-bottom: none !important; 3860 box-shadow: none !important; 3861 background: transparent !important; 3862 } 3863 3864 /* Add 'Variant:' label before variant groups */ 3865 .pd-variants::before { 3866 content: "Variant:" !important; 3867 display: block !important; 3868 font-size: 11pt !important; 3869 font-weight: 700 !important; 3870 color: #0f172a !important; 3871 margin-bottom: 10px !important; 3872 border: none !important; 3873 padding: 0 !important; 3874 border-top: none !important; 3875 border-bottom: none !important; 3876 } 3877 3878 .v-group { 3879 display: flex !important;
3880 align-items: center !important; 3881 gap: 8px !important; 3882 margin-bottom: 8px !important; 3883 line-height: 1 !important; 3884 page-break-inside: avoid !important; 3885 border: none !important; 3886 padding: 0 !important; 3887 } 3888 3889 .v-group:last-child { 3890 margin-bottom: 0 !important; 3891 } 3892 3893 .v-label { 3894 font-weight: 700 !important; 3895 margin: 0 !important; 3896 font-size: 9pt !important; 3897 color: #475569 !important; 3898 min-width: 60px !important; 3899 flex-shrink: 0 !important; 3900 border: none !important; 3901 padding: 0 !important; 3902 } 3903 3904 .v-options { 3905 display: flex !important; 3906 gap: 6px !important; 3907 flex-wrap: wrap !important; 3908 border: none !important; 3909 padding: 0 !important; 3910 margin: 0 !important; 3911 } 3912 3913 .v-chip { 3914 border: 1px solid #cbd5e1 !important; 3915 padding: 3px 10px !important; 3916 font-size: 9pt !important; 3917 background: #f8fafc !important; 3918 border-radius: 6px !important; 3919 display: inline-flex !important; 3920 align-items: center !important; 3921 page-break-inside: avoid !important; 3922 } 3923 3924 /* Highlight selected variant with blue background */ 3925 .v-chip.selected { 3926 background: #eff6ff !important; 3927 border-color: #2563eb !important; 3928 font-weight: 600 !important; 3929 } 3930 3931 /* Hide radio inputs */ 3932 .v-chip input[type="radio"] { 3933 display: none !important; 3934 } 3935 3936 /* Hide "Habis" badge in print */ 3937 .v-chip .chip-badge { 3938 display: none !important; 3939 } 3940 3941 /* Short description - HIDDEN in print */ 3942 .pd-short-desc { 3943 display: none !important; 3944 } 3945 3946 /* QR Code section - right side beside price */ 3947 .pd-qr-print { 3948 display: block !important; 3949 flex: 0 0 100px !important; 3950 margin: 0 !important; 3951 padding: 8px !important; 3952 background: #f8fafc !important; 3953 border-radius: 8px !important; 3954 text-align: center !important; 3955 page-break-inside: avoid !important; 3956 border: 1px solid #cbd5e1 !important; 3957 } 3958 3959 .pd-qr-label { 3960 font-size: 6.5pt !important; 3961 color: #64748b !important; 3962 margin-bottom: 5px !important; 3963 line-height: 1.2 !important; 3964 font-weight: 600 !important; 3965 } 3966 3967 .pd-qr-image { 3968 width: 80px !important; 3969 height: 80px !important; 3970 display: block !important; 3971 margin: 0 auto !important; 3972 } 3973 3974 /* Full description - below 2 column layout */ 3975 .pd-full-description { 3976 border: none !important; 3977 padding: 0 !important; 3978 margin: 24px 0 0 0 !important; 3979 page-break-before: avoid; 3980 grid-column: 1 / -1 !important; 3981 } 3982 3983 .pd-full-desc-title { 3984 font-size: 13pt !important; 3985 font-weight: 700 !important; 3986 margin: 0 0 12px 0 !important; 3987 color: #000 !important; 3988 border-top: none !important; 3989 border-bottom: none !important; 3990 padding-top: 16px !important; 3991 } 3992 3993 .pd-full-desc-body { 3994 font-size: 10pt !important; 3995 line-height: 1.6 !important; 3996 color: #334155 !important; 3997 } 3998 3999 .pd-full-desc-body img { 4000 max-height: 200px !important; 4001 width: auto !important; 4002 page-break-inside: avoid; 4003 display: none !important; /* Hide images in print to save space */ 4004 } 4005 4006 /* Also target #deskripsi img if exists */ 4007 #deskripsi img { 4008 max-width: 350px !important; 4009 height: auto !important; 4010 } 4011 4012 .pd-full-desc-body table { 4013 border-collapse: collapse !important; 4014 width: 100% !important; 4015 margin: 12px 0 !important; 4016 font-size: 9pt !important; 4017 } 4018 4019 .pd-full-desc-body th, 4020 .pd-full-desc-body td { 4021 border: 1px solid #cbd5e1 !important; 4022 padding: 6px 8px !important; 4023 } 4024 4025 /* Print footer */ 4026 .product-single::after { 4027 content: "Plaza IT - plazait.co.id | Printed: " attr(data-print-date); 4028 display: block; 4029 margin-top: 30px; 4030 padding-top: 16px; 4031 border-top: 1px solid #e2e8f0; 4032 font-size: 9pt; 4033 color: #64748b; 4034 text-align: center; 4035 } 4036 4037 /* Page breaks */ 4038 .pd-media-card, 4039 .pd-price-block, 4040 .pd-full-desc-title { 4041 page-break-after: avoid; 4042 } 4043 4044 .pd-full-description { 4045 page-break-before: auto; 4046 } 4047} 4048 4049/* === Breadcrumb (truncate last item) === */ 4050.pd-breadcrumb{ 4051 display:flex; 4052 flex-wrap:nowrap; 4053 align-items:center; 4054 gap:.5rem; 4055 font-size:.72rem; 4056 margin:.7rem 0 1.2rem .5rem; 4057 color:var(--pd-muted); 4058 overflow:hidden; 4059} 4060.pd-breadcrumb a{color:var(--pd-accent);text-decoration:none} 4061.pd-breadcrumb a:hover{text-decoration:underline} 4062.pd-breadcrumb a, 4063.pd-breadcrumb span{flex:0 0 auto} 4064.pd-breadcrumb .current{ 4065 flex:1 1 auto; 4066 min-width:0; 4067 white-space:nowrap; 4068 overflow:hidden; 4069 text-overflow:ellipsis; 4070 font-weight:600;
4071 color:var(--pd-dark); 4072 padding-right:2px; 4073} 4074 4075/* === Top layout === */ 4076.pd-top-layout{ 4077 display:grid; 4078 gap:var(--pd-gap); 4079 grid-template-columns:minmax(260px,340px) minmax(460px,1fr) 300px; 4080 align-items:start; 4081 margin-bottom:2.4rem; 4082} 4083 4084/* === Media column === */ 4085.pd-media-card{ 4086 background:#fff; 4087 border:1px solid var(--pd-border); 4088 border-radius:var(--pd-radius); 4089 padding:1rem 1rem 1.2rem; 4090} 4091.pd-main-image{margin:0 0 .8rem;border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff;position:relative} 4092.pd-main-image img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4093 4094/* Gallery Thumbnails */ 4095.pd-thumb-gallery{margin-top:.6rem;display:flex;flex-wrap:wrap;gap:.55rem} 4096.pd-thumb-btn{ 4097 border:1px solid var(--pd-soft-border); 4098 background:#fff; 4099 cursor:pointer; 4100 width:64px;height:64px;padding:2px; 4101 display:flex;align-items:center;justify-content:center; 4102 overflow:hidden;border-radius:8px; 4103 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 4104 position:relative; 4105} 4106.pd-thumb-btn img{max-width:100%;max-height:100%;object-fit:contain;display:block} 4107.pd-thumb-btn:hover{border-color:var(--pd-accent)} 4108.pd-thumb-btn.active{border-color:var(--pd-accent);box-shadow:0 0 0 2px rgba(13,110,253,.25)} 4109.pd-thumb-btn:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4110.pd-thumb-btn:active{transform:scale(.96)} 4111 4112/* === Info column === */ 4113.pd-info-card{ 4114 background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4115 padding:1.4rem 1.6rem 2rem; 4116} 4117.pd-title{margin:0 0 1rem;font-size:1.2rem;line-height:1.25;font-weight:600;color:var(--pd-dark)} 4118.pd-price-block{margin:0 0 1rem} 4119.pd-price-row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;margin:0 0 .35rem} 4120.pd-price-current{font-size:2rem;font-weight:700;color:var(--pd-accent)} 4121.pd-price-original{font-size:.9rem;text-decoration:line-through;color:#94a3b8} 4122.pd-discount-badge{background:var(--pd-danger);color:#fff;font-size:.68rem;padding:4px 8px 3px;font-weight:600;border-radius:6px;letter-spacing:.5px} 4123.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} 4124.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} 4125.stock-badge.in{background:var(--pd-success-bg);color:var(--pd-success-text)} 4126.stock-badge.out{background:var(--pd-danger-bg);color:var(--pd-danger-text)} 4127.digital-badge{padding:4px 10px;border-radius:24px;background:linear-gradient(135deg,#e2a642 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} 4128.sku{color:var(--pd-muted)} 4129.pd-short-desc{font-size:.85rem;line-height:1.5;margin:0 0 1.2rem;color:#334155;overflow-wrap:anywhere} 4130.pd-short-desc p{margin:0 0 .75rem} 4131.pd-short-desc ul,.pd-short-desc ol{margin:.5rem 0 .9rem 1.1rem;font-size:.85rem;line-height:1.45} 4132 4133/* Meta row for sold and rating badges */ 4134.pd-meta-row { 4135 display: flex; 4136 align-items: center; 4137 flex-wrap: wrap; 4138 gap: 5px; 4139 margin-top: 8px; 4140 margin-bottom: 4px; 4141} 4142 4143/* Sales (Terjual) badge inline */ 4144.sold-inline{ 4145 display:inline-block;font-size:.65rem;font-weight:600;color:#475569; 4146 background:#f1f5f9;padding:3px 8px 4px;border-radius:6px;letter-spacing:.4px;margin-left:0; 4147} 4148 4149/* Price row divider */ 4150.price-divider { 4151 display: inline-block; 4152 margin: 0 .4rem; 4153 color: #cbd5e1; 4154 font-weight: 400; 4155} 4156 4157/* Rating badge inline */ 4158.rating-inline { 4159 display: inline-flex; 4160 align-items: center; 4161 gap: 4px; 4162 padding: 3px 8px 4px; 4163 background: #fff7ed; 4164 border-radius: 6px; 4165 font-size: .65rem; 4166 font-weight: 600; 4167 color: #C10DFF; 4168 text-decoration: none; 4169 transition: all .2s ease; 4170 cursor: pointer; 4171 margin-left: .5rem; 4172 white-space: nowrap; 4173} 4174 4175.rating-inline:hover { 4176 background: #ffedd5; 4177 transform: translateY(-1px); 4178 box-shadow: 0 2px 4px rgba(245,202,86,0.1); 4179} 4180 4181.rating-inline .star-icon { 4182 font-size: .75rem; 4183 color: #C10DFF; 4184 line-height: 1; 4185} 4186 4187.rating-inline .rating-value { 4188 color: #C10DFF; 4189} 4190 4191.rating-inline .rating-badge-count { 4192 color: #a36422; 4193 opacity: 0.85; 4194} 4195 4196@media (max-width: 640px) { 4197 .rating-inline { 4198 font-size: .65rem; 4199 padding: 3px 8px 4px; 4200 } 4201 .rating-inline .star-icon { 4202 font-size: .7rem; 4203 } 4204} 4205 4206/* Highlight animation for review section */ 4207@keyframes highlightFlash { 4208 0%, 100% { background-color: transparent; } 4209 50% { background-color: rgba(245,202,86,0.1); } 4210} 4211 4212.highlight-flash { 4213 animation: highlightFlash 1.5s ease; 4214} 4215 4216/* === Variant chips === */ 4217.pd-variants{margin-top:14px} 4218.v-group{margin-bottom:12px} 4219.v-label{font-weight:600;margin-bottom:6px} 4220.v-options{display:flex;flex-wrap:wrap;gap:8px} 4221.v-chip{ 4222 display:inline-flex;align-items:center;gap:8px; 4223 padding:6px 10px;border:1px solid #d6d9de;border-radius:999px; 4224 cursor:pointer;background:#fff;transition:.15s ease;user-select:none 4225} 4226.v-chip:hover{border-color:#9aa3af;box-shadow:0 1px 0 rgba(0,0,0,.04)} 4227.v-chip.selected{border-color:#2563eb;background:#eff6ff} 4228.v-chip.disabled{opacity:.45;cursor:not-allowed} 4229.v-chip.out{border-style:dashed} 4230.v-chip input{display:none} 4231.v-chip .dot{width:14px;
4231height:14px;border-radius:50%;background:var(--dot,#999);box-shadow:inset 0 0 0 1px rgba(0,0,0,.2)} 4232.v-chip .dot--neutral{background:#9aa3af} 4233.v-chip .txt{font-size:.95rem;white-space:nowrap} 4234.v-chip .chip-badge{margin-left:2px;font-size:.72rem;line-height:1;padding:3px 6px;border-radius:6px;background:#fee2e2;color:#a36422;border:1px solid #fecaca} 4235.pd-variant-summary{margin-top:8px;color:#475569;font-size:.92rem} 4236.pd-variant-summary .vs-item{display:inline-flex;gap:4px;margin-right:10px} 4237 4238/* === Buy form (di sidebar kanan untuk desktop) === */ 4239.pd-buy-form{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem 1.4rem;margin:0} 4240.qty-label{display:block;font-size:.6rem;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--pd-muted);margin-bottom:.35rem} 4241.pd-buy-form input[type=number]{width:90px;padding:.55rem .6rem;border:1px solid var(--pd-soft-border);border-radius:6px;font-size:.85rem} 4242.pd-actions{display:flex;gap:.8rem;flex-wrap:wrap} 4243 4244/* Tombol senada tema */ 4245.btn-add-cart,.btn-buy,.btn-chat-icon{ 4246 cursor:pointer;border:0;font-weight:700;border-radius:10px;font-size:.9rem; 4247 display:inline-flex;align-items:center;justify-content:center;gap:.4rem; 4248 min-height:44px;padding:0 1rem;transition:var(--pd-transition) 4249} 4250.btn-add-cart{background:var(--brand-500);color:#fff;box-shadow:0 2px 8px -2px rgba(13,110,253,.35)} 4251.btn-add-cart:hover{filter:brightness(.95)} 4252.btn-buy{background:#fff;color:var(--brand-500);border:2px solid var(--brand-500)} 4253.btn-buy:hover{background:rgba(13,110,253,.06)} 4254.btn-chat-icon{background:#fff;color:#C10DFF;border:2px solid #C10DFF;padding:0;width:44px;min-width:44px} 4255.btn-chat-icon:hover{background:rgba(245,202,86,0.06);border-color:#C10DFF} 4256.btn-chat-icon svg{width:20px;height:20px;flex-shrink:0} 4257.btn-add-cart:disabled,.btn-buy:disabled,.btn-chat-icon:disabled{opacity:.5;cursor:not-allowed} 4258 4259/* Buy with Voucher Button - Cleaner, more compact design */ 4260.btn-buy-voucher{ 4261 cursor:pointer;border:0;font-weight:600;border-radius:10px; 4262 display:inline-flex;align-items:center;justify-content:space-between;gap:10px; 4263 min-height:44px;padding:0 14px 0 12px;transition:all 0.3s ease; 4264 background:linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); 4265 color:#fff; 4266 box-shadow:0 3px 10px rgba(124,58,237,0.3); 4267 position:relative; 4268 overflow:hidden; 4269 flex:1 1 auto 4270} 4271.btn-buy-voucher:hover{ 4272 transform:translateY(-2px); 4273 box-shadow:0 5px 16px rgba(124,58,237,0.4) 4274} 4275.btn-buy-voucher:active{ 4276 transform:translateY(0); 4277 box-shadow:0 2px 6px rgba(124,58,237,0.3) 4278} 4279.btn-buy-voucher .bv-icon{ 4280 font-size:20px; 4281 line-height:1; 4282 flex-shrink:0 4283} 4284.btn-buy-voucher .bv-text{ 4285 display:flex; 4286 flex-direction:column; 4287 align-items:flex-start; 4288 gap:2px; 4289 flex:1; 4290 min-width:0; 4291 font-size:14px; 4292 line-height:1.2 4293} 4294.btn-buy-voucher .bv-text small{ 4295 font-size:10px; 4296 opacity:0.85; 4297 font-weight:500; 4298 letter-spacing:0.3px; 4299 font-family:monospace 4300} 4301.btn-buy-voucher .bv-save{ 4302 font-size:12px; 4303 font-weight:700; 4304 background:rgba(255,255,255,0.25); 4305 padding:4px 8px; 4306 border-radius:6px; 4307 white-space:nowrap; 4308 flex-shrink:0 4309} 4310 4311/* Extra meta (opsional) */ 4312.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} 4313.pd-extra-meta span{font-weight:600;color:var(--pd-muted);margin-right:.3rem} 4314.pd-extra-meta a{color:var(--pd-accent);text-decoration:none} 4315.pd-extra-meta a:hover{text-decoration:underline} 4316 4317/* === Sidebar === */ 4318.pd-sidebar{display:flex;flex-direction:column;gap:1.2rem} 4319.pd-side-card{background:#fff;border:1px solid var(--pd-border);border-radius:var(--pd-radius);padding:1rem 1.1rem 1.2rem} 4320.pd-side-card h3{margin:0 0 .7rem;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4321.pd-side-card ul{list-style:none;margin:0;padding:0;display:grid;gap:.45rem;font-size:.7rem;color:#334155} 4322.pd-side-card li{position:relative;padding-left:13px;line-height:1.25} 4323.pd-side-card li:before{content:'';width:6px;height:6px;background:var(--pd-accent);border-radius:50%;position:absolute;
4323left:0;top:.45rem} 4324.pay-logos{display:flex;gap:.4rem;flex-wrap:wrap;margin:.1rem 0 .4rem} 4325.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} 4326 4327/* Buy card hanya desktop (mobile pakai FAB) */ 4328.pd-buy-card{display:block} 4329@media (max-width:880px){ .pd-buy-card{display:none} .pd-title-row {padding-right: 0 !important;}} 4330 4331/* === Full Description === */ 4332.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} 4333.pd-full-desc-title{margin:0 0 1.1rem;font-size:1.15rem;font-weight:600;color:var(--pd-dark)} 4334.pd-full-desc-body{font-size:.9rem;line-height:1.6;color:#334155;overflow-wrap:anywhere} 4335.pd-full-desc-body p{margin:0 0 1rem} 4336.pd-full-desc-body ul,.pd-full-desc-body ol{margin:.5rem 0 1rem 1.2rem} 4337.pd-full-desc-body table{width:100%;border-collapse:collapse;margin:1rem 0 1.2rem} 4338.pd-full-desc-body table,.pd-full-desc-body th,.pd-full-desc-body td{border-bottom:1px dotted #cbd5e1;vertical-align: top} 4339.pd-full-desc-body th,.pd-full-desc-body td{padding:.45rem .55rem;text-align:left} 4340.pd-full-desc-body img{max-width:100%;height:auto;display:block;} 4341 4342/* === Related (container) === */ 4343.pd-related-full{ 4344 background:#fff;border:1px solid var(--pd-border); 4345 border-radius:var(--pd-radius);padding:1.4rem 1.6rem 1.9rem; 4346 margin:0 0 2rem; 4347} 4348.pd-related-title{margin:0 0 1.1rem;font-size:1.05rem;font-weight:600;color:var(--pd-dark)} 4349 4350/* Related Grid (dirapikan: gabung selector agar tidak duplikat) */ 4351.pd-related-grid, 4352.pd-related-grid--full{ 4353 display:grid !important; 4354 gap:16px !important; 4355 grid-template-columns:repeat(auto-fill,minmax(170px,1fr)) !important; 4356 align-items:stretch; 4357} 4358 4359/* Fallback jika grid tidak didukung */ 4360@supports not (display:grid){ 4361 .pd-related-grid, 4362 .pd-related-grid--full{display:flex !important;flex-wrap:wrap} 4363 .pd-related-grid .pd-related-card, 4364 .pd-related-grid--full .pd-related-card{flex:0 0 calc(20% - 16px);max-width:calc(20% - 16px)} 4365 @media (max-width:1100px){ 4366 .pd-related-grid .pd-related-card, 4367 .pd-related-grid--full .pd-related-card{flex:0 0 calc(25% - 16px);max-width:calc(25% - 16px)} 4368 } 4369 @media (max-width:820px){ 4370 .pd-related-grid .pd-related-card, 4371 .pd-related-grid--full .pd-related-card{flex:0 0 calc(33.333% - 16px);max-width:calc(33.333% - 16px)} 4372 } 4373 @media (max-width:580px){ 4374 .pd-related-grid .pd-related-card, 4375 .pd-related-grid--full .pd-related-card{flex:0 0 calc(50% - 16px);max-width:calc(50% - 16px)} 4376 } 4377} 4378 4379/* Related Card */ 4380.pd-related-card{ 4381 display:flex;flex-direction:column;border:1px solid var(--pd-soft-border); 4382 background:#fff;border-radius:10px;text-decoration:none;color:inherit; 4383 overflow:hidden;transition:var(--pd-transition);position:relative;min-width:0; 4384} 4385.pd-related-card:hover{transform:translateY(-4px);box-shadow:var(--pd-shadow)} 4386.pd-related-card .thumb{aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;position:relative} 4387.pd-related-card img{width:100%;height:100%;object-fit:cover;display:block} 4388.mini-disc{position:absolute;top:6px;left:6px;background:#e2a642;color:#fff;font-size:.55rem;padding:3px 6px 2px;border-radius:4px;font-weight:600} 4389.pd-related-card .info{padding:.55rem .65rem .75rem;display:flex;flex-direction:column;gap:.25rem} 4390.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} 4391.pd-related-card .price{font-size:.7rem;font-weight:600;color:var(--pd-accent)} 4392.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;} 4393.pd-related-card .price .final {font-size: .82rem; font-weight: 700; color: #0d6efd;} 4394.pd-related-card .price .ori {font-size: .6rem; text-decoration: line-through; color: #94a3b8;font-weight: 500;} 4395 4396/* === Mobile slider (scroll-snap) â gambar utama tetap tampil === */ 4397.pd-mobile-slider{display:none} 4398.pd-mobile-slider img{width:100%;display:block;object-fit:contain;aspect-ratio:1/1} 4399 4400/* Gallery counter badge */ 4401.pd-gallery-counter{ 4402 position:absolute; 4403 bottom:12px; 4404 left:12px; 4405 background:rgba(255,255,255,0.25); 4406 color:#9E9E9E; 4407 padding:4px 10px; 4408 border-radius:10px; 4409 font-size:12px; 4410 font-weight:600; 4411 z-index:100; 4412 backdrop-filter:blur(6px); 4413 -webkit-backdrop-filter:blur(6px); 4414 font-variant-numeric:tabular-nums; 4415 pointer-events:none; 4416 display:none; 4417 box-shadow:0 1px 4px rgba(0,0,0,.15); 4418 line-height:1; 4419 border:1px solid rgba(0,0,0,0.08); 4420} 4421 4422@media (max-width:640px){ 4423 .pd-media-card{border: 0;padding: 0 1rem 0;} 4424 .pd-info-card {border: 0; padding: 0 1.3rem 0; } 4425 .pd-side-card {flex: 1 1 !important;} 4426 .pd-top-layout {gap: 0; margin-bottom: 0;} 4427 .pd-variants {margin-bottom: 1.5rem;} 4428 .pd-main-image{display:none} 4429 .pd-gallery-counter{display:block} 4430 .pd-mobile-slider-wrapper{ 4431 position:relative; 4432 margin-bottom:.6rem; 4433 } 4434 .pd-mobile-slider{ 4435 display:flex;gap:8px;overflow-x:auto; 4436 scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
4437 } 4438 .pd-mslide{ 4439 min-width:100%;flex:0 0 100%;scroll-snap-align:center; 4440 border:1px solid var(--pd-soft-border);border-radius:10px;overflow:hidden;background:#fff 4441 } 4442 .pd-thumb-gallery{display:none} /* hide thumbnails on mobile */ 4443} 4444 4445/* === Mobile Floating Action Bar === */ 4446.pd-mobile-fab{ 4447 position:fixed; 4448 left:0; 4449 right:0; 4450 bottom:42px; /* Position above bottom-nav (60px min-height + 6px padding) */ 4451 display:none; 4452 align-items:center; 4453 gap:.75rem; 4454 padding:12px 16px; 4455 background:rgba(255,255,255,0.95); 4456 backdrop-filter:blur(20px); 4457 -webkit-backdrop-filter:blur(20px); 4458 border-top:1px solid rgba(228,231,235,0.6); 4459 box-shadow:0 -4px 16px -4px rgba(0,0,0,0.12); 4460 z-index:999; /* Below bottom-nav (1000) */ 4461 transition:bottom .3s cubic-bezier(0.4, 0.0, 0.2, 1); 4462} 4463 4464/* When bottom-nav is hidden, move FAB to bottom */ 4465body:has(.bottom-nav.hidden) .pd-mobile-fab { 4466 bottom: 0; 4467} 4468.pd-mobile-fab .mf-actions{display:flex;gap:.5rem;width:100%} 4469.mf-btn{ 4470 border-radius:10px; 4471 padding:.6rem 1rem; 4472 font-weight:600; 4473 font-size:14px; 4474 cursor:pointer; 4475 transition:all .2s; 4476 -webkit-tap-highlight-color:transparent; 4477 display:inline-flex; 4478 align-items:center; 4479 justify-content:center; 4480 gap:4px; 4481} 4482.mf-buy{ 4483 background:#C10DFF; 4484 color:#fff; 4485 border:0; 4486 box-shadow:0 2px 8px rgba(245,202,86,0.3); 4487 flex:1; 4488 flex-direction:column; 4489 padding:.55rem 1rem; 4490 line-height:1; 4491 min-height:44px; 4492 font-family:inherit; 4493} 4494.mf-buy .mf-buy-text{ 4495 font-size:11px; 4496 font-weight:600; 4497 opacity:0.95; 4498 font-family:inherit; 4499} 4500.mf-buy .mf-buy-price{ 4501 font-size:13px; 4502 font-weight:800; 4503 letter-spacing:-0.3px; 4504 font-family:inherit; 4505 line-height:1; 4506} 4507.mf-buy:active{transform:scale(0.97)} 4508.mf-cart{ 4509 background:#fff; 4510 color:#C10DFF; 4511 border:2px solid #C10DFF; 4512 width:44px; 4513 height:44px; 4514 min-height:44px; 4515 padding:0; 4516 flex-shrink:0; 4517 font-family:inherit; 4518} 4519.mf-cart:active{transform:scale(0.95)} 4520.mf-chat{background:#fff;color:#C10DFF;border:2px solid #C10DFF;width:44px;height:44px;min-height:44px;padding:0;flex-shrink:0;font-family:inherit} 4521.mf-chat:active{transform:scale(0.95)} 4522.mf-chat svg{width:20px;height:20px;flex-shrink:0} 4523.mf-share{ 4524 background:#fff; 4525 color:#C10DFF; 4526 border:2px solid #C10DFF; 4527 width:44px; 4528 height:44px; 4529 padding:0; 4530 flex-shrink:0; 4531} 4532.mf-share:active{transform:scale(0.95)} 4533.mf-btn:disabled{opacity:.5;cursor:not-allowed} 4534@media (max-width:640px){ 4535 .pd-mobile-fab{display:flex} 4536 .product-single{padding-bottom:0;} /* Space for FAB + bottom-nav */ 4537} 4538 4539/* === Installment cards === */ 4540.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} 4541.pd-installment-card h3{margin:0 0 .2rem;font-size:.78rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--pd-dark)} 4542 4543.inst-tabs{display:flex;gap:.4rem} 4544.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)} 4545.inst-tab.active,.inst-tab:hover{background:var(--pd-accent);color:#fff;border-color:var(--pd-accent)} 4546.inst-tab:focus{outline:2px solid var(--pd-accent);outline-offset:2px} 4547 4548.inst-tenors{display:flex;gap:.4rem;flex-wrap:wrap} 4549.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} 4550.tenor-btn.active,.tenor-btn:hover{background:#1d7df0;color:#fff;border-color:#1d7df0} 4551 4552.inst-result{display:flex;flex-direction:column;gap:.45rem;font-size:.63rem} 4553.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} 4554.inst-line.highlight{background:#1d7df0;color:#fff;border-color:#1d7df0;font-weight:600} 4555.inst-line .lbl{opacity:.8} 4556.inst-line .val{font-weight:600;letter-spacing:.3px} 4557.inst-footnote{font-size:.53rem;line-height:1.25;color:#64748b;margin-top:.2rem} 4558@media (max-width: 860px){ 4559 .pd-installment-card{order:3} 4560 .inst-tabs .inst-tab,.inst-tenors .tenor-btn{font-size:.65rem} 4561 4562 /* Voucher button responsive - make it simpler */ 4563 .btn-buy-voucher{ 4564 padding:0 12px; 4565 gap:8px 4566 } 4567 .btn-buy-voucher .bv-icon{ 4568 font-size:18px 4569 } 4570 .btn-buy-voucher .bv-text{ 4571 font-size:13px 4572 } 4573 .btn-buy-voucher .bv-text small{ 4574 font-size:9px 4575 } 4576 .btn-buy-voucher .bv-save{ 4577 font-size:11px; 4578 padding:3px 6px 4579 } 4580} 4581 4582/* Installment List v2 (eye-catching) */ 4583.pd-installment-card2{ 4584 background:linear-gradient(135deg,#ffffff 0%,#f1f7ff 65%,#e8f2ff 100%); 4585 border:1px solid var(--pd-border);border-radius:var(--pd-radius); 4586 padding:1rem 1.05rem 1.2rem;display:flex;flex-direction:column;gap:.5rem; 4587 position:relative;overflow:hidden 4588} 4589.pd-installment-card2:before{ 4590 content:'';position:absolute;width:140px;height:140px; 4591 background:radial-gradient(circle at center,rgba(29,125,240,.18),rgba(29,125,240,0)); 4592 top:-40px;right:-40px;pointer-events:none 4593} 4594.pd-installment-card2 h3{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#0f2f54} 4595.pd-installment-card2 .inst-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem} 4596.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)} 4597 4598.inst-bank-tabs{display:flex;gap:.45rem} 4599.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} 4600.bank-tab.active,.bank-tab:hover{background:#1d7df0;color:#fff;border-color:#1d7df0;box-shadow:0 3px 10px -4px rgba(29,125,240,.6)} 4601.bank-tab:focus{outline:2px solid #1d7df0;outline-offset:2px} 4602 4603.inst-month-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem} 4604.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} 4605.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} 4606.inst-month-item:hover{transform:translateY(-2px);box-shadow:0 6px 14px -6px rgba(0,40,120,.25)} 4607.inst-month-item:hover:before{opacity:1} 4608.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)} 4609.amount{flex:1;font-size:.8rem;font-weight:600;color:#0f2f54;letter-spacing:.3px} 4610.tag{font-size:.55rem;font-weight:600;padding:.28rem .55rem;border-radius:30px;letter-spacing:.4px;text-transform:uppercase;white-space:nowrap} 4611.tag-zero{background:#16a34a;color:#fff;box-shadow:0 2px 8px -3px rgba(16,180,90,.55)} 4612 4613.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} 4614.inst-disclaimer{font-size:.52rem;line-height:1.25;color:#64748b;margin-top:-.2rem} 4615 4616@media (max-width:860px){ 4617 .pd-installment-card2{order:3} 4618 .tenor-badge{min-width:38px;font-size:.75rem} 4619 .amount{font-size:.78rem} 4620} 4621 4622/* Alt Fintech Banner (compact) */ 4623.inst-alt-banner--compact{ 4624 background:linear-gradient(142deg,#1d7df0 0%,#3c98ff 55%,#67bdff 100%); 4625 color:#fff;border:0;padding:.95rem .95rem 1.05rem;border-radius:16px; 4626 box-shadow:0 6px 18px -6px rgba(0,70,160,.45);position:relative;overflow:hidden;min-height:auto; 4627} 4628.inst-alt-banner--compact:before, 4629.inst-alt-banner--compact:after{ 4630 content:'';position:absolute;border-radius:50%;background:rgba(255,255,255,.15);filter:blur(2px);pointer-events:none; 4631} 4632.inst-alt-banner--compact:before{width:130px;height:130px;top:-50px;right:-35px} 4633.inst-alt-banner--compact:after{width:90px;height:90px;bottom:-40px;left:-30px}
4634.inst-alt-banner--compact .iab-top{margin:0 0 .55rem;display:flex;flex-direction:column;gap:.25rem} 4635.inst-alt-banner--compact .iab-title{font-size:.74rem;font-weight:700;letter-spacing:.7px;line-height:1.15} 4636.inst-alt-banner--compact .iab-sub{font-size:.55rem;font-weight:500;opacity:.95;letter-spacing:.4px} 4637 4638.iab-logos-compact{display:flex;gap:.55rem;align-items:center;margin:0 0 .6rem;flex-wrap:nowrap;justify-content:space-between} 4639.iab-logos-compact .logo-fin{width:30%} 4640.iab-logos-compact .logo{ 4641 width:48px;height:48px;border-radius:14px;display:flex;align-items:center;justify-content:center; 4642 font-size:.62rem;font-weight:700;letter-spacing:.3px;position:relative;color:#fff; 4643 box-shadow:0 4px 10px -4px rgba(0,0,0,.35);background:rgba(255,255,255,.15); 4644 backdrop-filter:blur(4px);transition:.25s;text-shadow:0 1px 2px rgba(0,0,0,.4);user-select:none; 4645} 4646.iab-logos-compact .logo:before{ 4647 content:'';position:absolute;inset:0;border-radius:inherit; 4648 background:linear-gradient(140deg,rgba(255,255,255,.35),rgba(255,255,255,0));mix-blend-mode:overlay;opacity:.55;pointer-events:none; 4649} 4650.iab-logos-compact .logo:hover{transform:translateY(-3px) scale(1.05);box-shadow:0 8px 18px -6px rgba(0,0,0,.45)} 4651 4652.iab-cta--compact{ 4653 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; 4654} 4655.iab-cta--compact:hover{transform:translateY(-2px);box-shadow:0 8px 18px -7px rgba(0,0,0,.45)} 4656.iab-footnote--compact{margin-top:.45rem;font-size:.46rem;opacity:.85;letter-spacing:.25px} 4657@media (max-width:860px){ 4658 .inst-alt-banner--compact{order:4;margin-top:.4rem;flex:1 1} 4659 .iab-logos-compact .logo{width:46px;height:46px} 4660} 4661 4662/* === Scroll To Top (baru) === */ 4663.pd-scroll-top{ 4664 position:fixed; 4665 right:20px; 4666 bottom:90px; 4667 width:48px; 4668 height:48px; 4669 border-radius:50%; 4670 background:linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); 4671 color:#0f2f54; 4672 border:1px solid rgba(226, 232, 240, 0.8); 4673 box-shadow:0 8px 20px -8px rgba(0,0,0,.3), 0 2px 6px rgba(0,0,0,.1); 4674 display:flex; 4675 align-items:center; 4676 justify-content:center; 4677 cursor:pointer; 4678 z-index:1105; 4679 opacity:0; 4680 transform:translateY(12px) scale(.92); 4681 transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); 4682 backdrop-filter:blur(8px); 4683 -webkit-backdrop-filter:blur(8px); 4684} 4685.pd-scroll-top:hover{ 4686 box-shadow:0 12px 28px -10px rgba(0,0,0,.4), 0 4px 12px rgba(0,0,0,.15); 4687 transform:translateY(0) scale(1); 4688 background:linear-gradient(135deg, #ffffff 0%, #fefefe 100%); 4689 border-color:rgba(226, 232, 240, 1); 4690} 4691.pd-scroll-top:active{ 4692 transform:translateY(0) scale(.96); 4693} 4694.pd-scroll-top:focus-visible{outline:2px solid var(--pd-accent);outline-offset:2px} 4695.pd-scroll-top.visible{opacity:1;transform:translateY(0) scale(1)} 4696@media (max-width:640px){ 4697 /* Dynamic positioning via JavaScript */ 4698 .pd-scroll-top{ 4699 right:14px; 4700 bottom:var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))); 4701 } 4702} 4703 4704/* === Responsive layout tweaks === */ 4705@media (max-width:1250px){ 4706 .pd-top-layout{grid-template-columns:minmax(260px,330px) 1fr} 4707 .pd-sidebar{grid-column:1 / -1;flex-direction:row;flex-wrap:wrap;gap:1rem} 4708 /* Other cards 2-per-row; buy card always takes full row to avoid height-mismatch gaps */ 4709 .pd-side-card{flex:1 1 calc(50% - .5rem)} 4710 .pd-buy-card{flex:0 0 100%} 4711} 4712@media (max-width:880px){ 4713 .pd-top-layout{grid-template-columns:1fr} 4714 .pd-sidebar{flex-direction:column; padding: 0 1rem;} 4715 .pd-title{font-size:1.2rem} 4716 .pd-price-current{font-size:1.5rem} 4717} 4718@media (max-width:640px){ 4719 /* Related: 2 kolom di mobile */ 4720 .pd-related-full{padding:1.2rem;border:0;padding-top: .6rem;margin-bottom:.6rem;} 4721 .pd-related-grid,.pd-related-grid--full{grid-template-columns:repeat(2,1fr) !important;gap:12px !important} 4722} 4723@media (max-width:560px){ 4724 .pd-actions{flex-direction:column} 4725 .btn-add-cart,.btn-buy,.btn-buy-voucher{width:100%} 4726 .pd-full-description{padding:1.2rem;border:0;padding-bottom:0;} 4727} 4728@media (max-width:380px){ 4729 .pd-related-full{padding:.9rem .7rem 1.1rem} 4730 .pd-related-grid,.pd-related-grid--full{gap:8px !important} 4731 .pd-related-card .info{padding:.4rem .48rem .55rem} 4732} 4733 4734/* === Modern Countdown (Product Detail) === */ 4735.pd-countdown-modern{ 4736 --cd-bg1:#0ea5e9; /* sky-500 */ 4737 --cd-bg2:#6366f1; /* indigo-500 */ 4738 --cd-text:#0b1220; 4739 --cd-contrast:#ffffff; 4740 --cd-shadow: 0 8px 24px -10px rgba(0,0,0,.25); 4741 --cd-border: rgba(255,255,255,.25); 4742 --cd-box: rgba(255,255,255,.65); 4743 --cd-box-text:#0f172a; 4744 --cd-danger:#e2a642; 4745 4746 display:flex; gap:.75rem; align-items:center; 4747 background:linear-gradient(135deg,var(--cd-bg1),var(--cd-bg2)); 4748 border:1px solid var(--cd-border); 4749 color:var(--cd-contrast); 4750 border-radius:14px; 4751 padding:.6rem .8rem; 4752 box-shadow:var(--cd-shadow); 4753 position:relative; 4754 overflow:hidden; 4755 margin-bottom: 1rem; 4756} 4757 4758.pd-countdown-modern .pd-cd-icon{ 4759 width:28px;height:28px;flex:0 0 28px; 4760 display:grid;place-items:center; 4761 background:rgba(255,255,255,.22); 4762 border:1px solid var(--cd-border); 4763 border-radius:10px; 4764 box-shadow: inset 0 1px 0 rgba(255,255,255,.35); 4765} 4766.pd-countdown-modern .pd-cd-icon svg{width:18px;height:18px;fill:#fff;opacity:.95} 4767
4768.pd-countdown-modern .pd-cd-body{display:flex;flex-direction:column;gap:.35rem;min-width:0} 4769 4770.pd-countdown-modern .pd-cd-label{ 4771 font-size:.72rem;font-weight:700;letter-spacing:.4px;opacity:.95;line-height:1; 4772 text-shadow:0 1px 0 rgba(0,0,0,.08); 4773} 4774 4775.pd-countdown-modern .pd-cd-time{ 4776 display:flex; align-items:end; gap:.35rem; flex-wrap:wrap; 4777 line-height:1; 4778} 4779 4780.pd-countdown-modern .unit{ 4781 display:flex; flex-direction:column; align-items:center; gap:.18rem; 4782 min-width:58px; 4783} 4784.pd-countdown-modern .num{ 4785 display:inline-flex; align-items:center; justify-content:center; 4786 min-width:58px; height:40px; padding:0 .4rem; 4787 background:var(--cd-box); 4788 color:var(--cd-box-text); 4789 border:1px solid rgba(15,23,42,.08); 4790 border-radius:10px; 4791 font: 800 1.05rem ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; 4792 letter-spacing:.5px; 4793 box-shadow: 0 4px 14px -6px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.55); 4794} 4795.pd-countdown-modern .unit-label{ 4796 font-size:.62rem; font-weight:700; opacity:.9; text-shadow:0 1px 0 rgba(255,255,255,.15); 4797} 4798 4799.pd-countdown-modern .colon{ 4800 font-weight:900; font-size:1.05rem; opacity:.95; transform: translateY(-2px); 4801 animation: cdBlink 1.2s infinite steps(2, jump-none); 4802} 4803@keyframes cdBlink { 0%{opacity:1} 50%{opacity:.25} 100%{opacity:1} } 4804 4805.pd-countdown-modern .pd-cd-progress{ 4806 display:none; 4807 position:relative; 4808 height:6px; border-radius:999px; overflow:hidden; 4809 background:rgba(255,255,255,.25); 4810 border:1px solid rgba(255,255,255,.28); 4811} 4812.pd-countdown-modern .pd-cd-progress .bar{ 4813 height:100%; width:0%; background:linear-gradient(90deg,#22c55e,#a3e635); 4814 box-shadow: 0 2px 8px rgba(0,0,0,.25) inset; 4815 transition:width .35s ease; 4816} 4817 4818/* State saat hampir habis */ 4819.pd-countdown-modern.danger{ 4820 --cd-bg1:#C10DFF; /* orange-500 */ 4821 --cd-bg2:#e2a642; /* red-500 */ 4822} 4823 4824/* Responsive */ 4825@media (max-width:640px){ 4826 .pd-countdown-modern{ 4827 padding:.55rem .65rem; border-radius:12px; gap:.6rem; 4828 } 4829 .pd-countdown-modern .unit{min-width:52px} 4830 .pd-countdown-modern .num{min-width:52px;height:36px;font-size:.98rem} 4831} 4832 4833/* === Modern Countdown polish (shine + compact) === */ 4834.pd-countdown-modern{ 4835 position:relative; 4836} 4837.pd-countdown-modern::after{ 4838 content:""; 4839 position:absolute; inset:-1px; 4840 background: linear-gradient(120deg, rgba(255,255,255,.12), rgba(255,255,255,0) 35%), 4841 radial-gradient(120% 120% at 0% 0%, rgba(255,255,255,.08), rgba(255,255,255,0) 60%); 4842 border-radius: inherit; 4843 pointer-events:none; 4844 mix-blend-mode: screen; 4845 animation: sheen 4.5s ease-in-out infinite; 4846} 4847@keyframes sheen { 4848 0% { opacity:.35; transform: translateX(-10%) } 4849 50% { opacity:.15; transform: translateX(4%) } 4850 100% { opacity:.35; transform: translateX(-10%) } 4851} 4852 4853/* Mode compact (aktifkan dengan data-mode="compact" di elemen) */ 4854.pd-countdown-modern[data-mode="compact"]{ 4855 gap:.55rem; padding:.5rem .6rem; border-radius:12px; 4856} 4857.pd-countdown-modern[data-mode="compact"] .unit{ 4858 min-width:46px; 4859} 4860.pd-countdown-modern[data-mode="compact"] .num{ 4861 min-width:46px; height:34px; font-size:.92rem; 4862} 4863.pd-countdown-modern[data-mode="compact"] .pd-cd-label{ 4864 font-size:.66rem; 4865} 4866.pd-countdown-modern[data-mode="compact"] .pd-cd-icon{ 4867 width:24px; height:24px; border-radius:8px; 4868} 4869.pd-countdown-modern[data-mode="compact"] .pd-cd-progress{ 4870 height:5px; 4871} 4872 4873/* ============================================================ 4874 Sidebar: shared refinements for medium + tablet (641px â 1250px) 4875 Buy card = form & paylater on the same row. 4876 Alt-fintech logos compact (no over-stretching). 4877 ============================================================ */ 4878@media (min-width:641px) and (max-width:1250px) { 4879 /* Buy card: put qty/buy buttons and paylater promo inline */ 4880 .pd-buy-card { 4881 display: flex !important; 4882 flex-direction: row; 4883 flex-wrap: wrap; 4884 align-items: center; 4885 gap: .6rem .75rem; 4886 } 4887 .pd-buy-card .pd-buy-form { flex: 1 1 0; min-width: 0; } 4888 .pd-buy-card .paylater-promo { 4889 flex: 0 0 auto;
4890 align-self: center; 4891 margin: 0; 4892 max-width: 44%; 4893 } 4894 4895 /* Alt-fintech banner: tighter logos */ 4896 .iab-logos-compact { 4897 justify-content: center; 4898 gap: 1.25rem; 4899 } 4900 .iab-logos-compact .logo-fin { 4901 width: auto; 4902 max-height: 28px; 4903 object-fit: contain; 4904 } 4905} 4906 4907/* ============================================================ 4908 Tablet layout (641px â 880px) 4909 Restores 2-column grid + horizontal sidebar for mid-size screens. 4910 Overrides the generic â¤880px mobile-collapse rules above. 4911 ============================================================ */ 4912@media (min-width:641px) and (max-width:880px) { 4913 /* 2-column grid: image-card | info-card */ 4914 .pd-top-layout { 4915 grid-template-columns: minmax(260px, 40%) 1fr; 4916 gap: 1rem; 4917 } 4918 4919 /* Sidebar: full-width horizontal flex row below the two main columns */ 4920 .pd-sidebar { 4921 grid-column: 1 / -1; 4922 flex-direction: row; 4923 flex-wrap: wrap; 4924 gap: .75rem; 4925 padding: 0; 4926 } 4927 /* Banner cards fill 2-per-row */ 4928 .pd-side-card { flex: 1 1 calc(50% - .375rem); } 4929 /* Buy card: full row (tallest card â avoids white-space gap if shared with shorter banners) */ 4930 .pd-buy-card { display: block !important; flex: 0 0 100%; } 4931 4932 /* Hide installment simulator + alt-fintech banner on tablet (too much vertical space) */ 4933 .pd-installment-card2, 4934 .inst-alt-banner--compact { display: none !important; } 4935 4936 /* Restore sticky add-to-cart header (hidden at â¤880px by default) */ 4937 .pd-sticky-header { display: flex !important; } 4938 4939 /* Typography â tighten slightly to fit the narrower info column */ 4940 .pd-price-current { font-size: 1.65rem; } 4941 .pd-title { font-size: 1.1rem; } 4942 .pd-info-card { padding: 1.1rem 1.2rem 1.5rem; } 4943} 4944</style> 4945 <style data-inlined="product-grid.css"> 4946/* Product Grid (Firebird) */ 4947 4948.product-archive { 4949 max-width: 1320px; 4950 margin-inline: auto; 4951 padding: 0 0 3rem; 4952 /*font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,sans-serif;*/ 4953} 4954 4955.pa-toolbar { 4956 display: flex; 4957 flex-wrap: wrap; 4958 gap: 1rem; 4959 justify-content: space-between; 4960 align-items: flex-start; 4961} 4962 4963.pa-title { 4964 font-size: 1.4rem; 4965 margin: 0; 4966 font-weight: 600; 4967 line-height: 1.2; 4968 color: #0f172a; 4969} 4970 4971.pa-left { 4972 display: flex; 4973 flex-wrap: wrap; 4974 gap: .75rem; 4975 align-items: center; 4976} 4977 4978.pa-filter-badge { 4979 background: #f1f5f9; 4980 border: 1px solid #e2e8f0; 4981 padding: .45rem .6rem; 4982 border-radius: 8px; 4983 font-size: .7rem; 4984 display: flex; 4985 align-items: center; 4986 gap: .4rem; 4987 line-height: 1; 4988 color: #475569; 4989} 4990.pa-filter-badge strong { color: #0f172a; } 4991.pa-filter-badge .clear { 4992 display:inline-block; 4993 text-decoration:none; 4994 background:#e2e8f0; 4995 color:#475569; 4996 font-weight:600; 4997 padding:0 .35rem .05rem; 4998 border-radius:4px; 4999 transition:.2s; 5000 font-size:.75rem; 5001 line-height:1; 5002} 5003.pa-filter-badge .clear:hover { background:#cbd5e1; color:#1e293b; } 5004 5005.pa-right { 5006 display:flex; 5007 gap:1rem; 5008 flex-wrap:wrap; 5009 align-items:flex-start; 5010 margin-bottom: 0.5rem; 5011} 5012 5013.pa-search { 5014 display:flex; 5015 gap:.5rem; 5016 align-items:center; 5017 background:#fff; 5018 border:1px solid #e2e8f0; 5019 padding:.4rem .5rem; 5020 border-radius:8px; 5021} 5022.pa-search input[type=text] { 5023 border:0; 5024 outline:none; 5025 font-size:.8rem; 5026 padding:.35rem .4rem;
5027 min-width:200px; 5028} 5029.pa-search button { 5030 background:#0d6efd; 5031 border:0; 5032 color:#fff; 5033 font-size:.7rem; 5034 padding:.45rem .75rem; 5035 border-radius:6px; 5036 cursor:pointer; 5037 font-weight:600; 5038 letter-spacing:.3px; 5039 transition:.25s; 5040} 5041.pa-search button:hover { background:#0b5dc4; } 5042 5043.pa-sort { 5044 display:flex; 5045 gap:.4rem; 5046 align-items:center; 5047 font-size:.7rem; 5048 background:#fff; 5049 border:1px solid #e2e8f0; 5050 padding:.4rem .55rem; 5051 border-radius:8px; 5052} 5053.pa-sort select { 5054 font-size:.75rem; 5055 padding:.35rem .5rem; 5056 border:1px solid #cbd5e1; 5057 border-radius:6px; 5058 background:#fff; 5059 outline:none; 5060} 5061 5062.pa-empty { 5063 border:1px dashed #cbd5e1; 5064 padding:3rem 1rem; 5065 text-align:center; 5066 border-radius:12px; 5067 background:#fff; 5068} 5069.pa-empty p { 5070 margin:0 0 1rem; 5071 font-size:.9rem; 5072 color:#475569; 5073} 5074.btn-reset { 5075 display:inline-block; 5076 background:#0d6efd; 5077 color:#fff; 5078 font-size:.7rem; 5079 padding:.55rem 1rem; 5080 border-radius:8px; 5081 text-decoration:none; 5082 font-weight:600; 5083 letter-spacing:.4px; 5084 transition:.25s; 5085} 5086.btn-reset:hover { background:#0a58c7; } 5087 5088/* Grid */ 5089.pa-grid { 5090 display:grid; 5091 gap:1rem; 5092 grid-template-columns:repeat(auto-fill, minmax(180px,1fr)); 5093 align-items:stretch; 5094} 5095 5096.pa-card { 5097 background:#fff; 5098 border:1px solid #e2e8f0; 5099 border-radius:12px; 5100 display:flex; 5101 flex-direction:column; 5102 text-decoration:none; 5103 color:inherit; 5104 position:relative; 5105 overflow:hidden; 5106 transition:.28s cubic-bezier(.4,0,.2,1); 5107 min-width:0; 5108 padding: 0; 5109} 5110.pa-card:hover { 5111 transform:translateY(-4px); 5112 box-shadow:0 8px 22px -8px rgba(0,0,0,.15); 5113 border-color:#dce2ea; 5114} 5115 5116.pa-card .thumb { 5117 aspect-ratio:1/1; 5118 background:#f1f5f9; 5119 display:flex; 5120 align-items:center; 5121 justify-content:center; 5122 overflow:hidden; 5123 position:relative; 5124} 5125.pa-card img { 5126 width:100%; 5127 height:100%; 5128 object-fit:cover; 5129 display:block; 5130} 5131.badge-disc { 5132 position:absolute; 5133 top:8px; 5134 left:8px; 5135 background:#e2a642; 5136 color:#fff; 5137 font-size:.6rem; 5138 padding:4px 6px 3px; 5139 border-radius:6px; 5140 font-weight:700; 5141 letter-spacing:.4px; 5142} 5143 5144.pa-card .body { 5145 padding:.65rem .7rem .8rem; 5146 display:flex; 5147 flex-direction:column; 5148 gap:.45rem; 5149 min-height:90px; 5150} 5151 5152.pa-card .title { 5153 font-size:.75rem; 5154 line-height:1.25; 5155 margin:0; 5156 font-weight:600;
5157 color:#1e293b; 5158 height:2.4em; 5159 overflow:hidden; 5160} 5161 5162.pa-card .prices { 5163 display:flex; 5164 flex-wrap:wrap; 5165 align-items:baseline; 5166 gap:.4rem; 5167} 5168.pa-card .prices .final { 5169 font-size:.83rem; 5170 font-weight:700; 5171 color:#0d6efd; 5172} 5173.pa-card .prices .ori { 5174 font-size:.63rem; 5175 text-decoration:line-through; 5176 color:#94a3b8; 5177 font-weight:500; 5178} 5179 5180.pa-card .cat { 5181 margin-top:auto; 5182 font-size:.55rem; 5183 letter-spacing:.6px; 5184 text-transform:uppercase; 5185 background:#f1f5f9; 5186 border:1px solid #e2e8f0; 5187 padding:.28rem .45rem .25rem; 5188 border-radius:20px; 5189 font-weight:600; 5190 color:#475569; 5191 align-self:flex-start; 5192} 5193.pa-card .meta-line { 5194 margin-top:4px; 5195 font-size:.65rem; 5196 color:#475569; 5197} 5198.pa-card .meta-line .sold { 5199 display:inline-block; 5200 background:#f1f5f9; 5201 padding:2px 6px 3px; 5202 border-radius:4px; 5203 font-weight:600; 5204 letter-spacing:.3px; 5205 color:#334155; 5206} 5207 5208/* Pagination */ 5209.pa-pagination { 5210 margin-top:2rem; 5211 display:flex; 5212 flex-wrap:wrap; 5213 gap:.4rem; 5214 justify-content:center; 5215 font-size:.7rem; 5216 align-items:center; 5217} 5218.pa-pagination a, 5219.pa-pagination span { 5220 min-width:34px; 5221 height:34px; 5222 display:flex; 5223 align-items:center; 5224 justify-content:center; 5225 border-radius:8px; 5226 text-decoration:none; 5227 font-weight:600; 5228 padding:0 .6rem; 5229 box-sizing:border-box; 5230 border:1px solid #e2e8f0; 5231 background:#fff; 5232 color:#334155; 5233 transition:.25s; 5234} 5235.pa-pagination a:hover { 5236 background:#f1f5f9; 5237 color:#0f172a; 5238} 5239.pa-pagination .current { 5240 background:#0d6efd; 5241 color:#fff; 5242 border-color:#0d6efd; 5243} 5244.pa-pagination .gap { 5245 border:none; 5246 background:transparent; 5247 font-weight:400; 5248 color:#94a3b8; 5249} 5250 5251/* =============== Mobile Filter Bar (Tokopedia-like) =============== */ 5252.pa-mobile-filterbar{display:none} 5253@media (max-width: 640px){ 5254 /* sembunyikan tombol lama */ 5255 .pa-btn-filter{display:none} 5256 5257 .pa-mobile-filterbar{ 5258 display:block; 5259 position:sticky; 5260 top:0; /* sesuaikan jika ada header fixed di atas */ 5261 z-index:5; 5262 background:#fff; 5263 border-bottom:1px solid #e2e8f0; 5264 padding:.4rem .5rem; 5265 margin:-.4rem -1rem .6rem; /* full-bleed di container mobile */ 5266 } 5267 .pa-mobile-filterbar .mf-scroll{ 5268 display:flex;align-items:center;gap:.5rem; 5269 overflow-x:auto; 5270 -webkit-overflow-scrolling:touch; 5271 scrollbar-width:none; 5272 } 5273 .pa-mobile-filterbar .mf-scroll::-webkit-scrollbar{display:none} 5274 5275 .mf-chip{ 5276 flex:0 0 auto; 5277 background:#fff; 5278 color:#334155; 5279 text-decoration:none; 5280 border:1px solid #e2e8f0; 5281 border-radius:999px; 5282 padding:.44rem .7rem; 5283 font-size:.78rem; 5284 font-weight:600; 5285 letter-spacing:.2px; 5286 white-space:nowrap; 5287 } 5288 .mf-chip:active{transform:scale(.98)} 5289 .mf-chip.active{ 5290 background:#eaf2ff; 5291 border-color:#0d6efd; 5292 color:#0d6efd; 5293 } 5294 5295 .mf-filter-btn{ 5296 position:relative; 5297 flex:0 0 auto; 5298 width:38px;height:38px; 5299 border-radius:50%; 5300 border:1.5px solid #0d6efd; 5301 background:#fff; 5302 display:inline-flex;align-items:center;justify-content:center; 5303 color:#0d6efd; 5304 cursor:pointer; 5305 } 5306 .mf-filter-btn:active{transform:scale(.96)} 5307 .mf-filter-btn svg{width:18px;height:18px;display:block} 5308 .mf-filter-btn .mf-badge{ 5309 position:absolute;top:0;right:-4px; 5310 min-width:18px;height:18px;border-radius:9px; 5311 background:#e2a642;color:#fff; 5312 font-size:.65rem;font-weight:700; 5313 display:flex;align-items:center;justify-content:center; 5314 padding:0 4px;line-height:1; 5315 box-shadow:0 2px 6px -2px rgba(226,166,66,.6); 5316 } 5317} 5318 5319/* Responsive */ 5320@media (max-width: 640px) { 5321 .product-archive { padding: 0 1rem 3rem; } 5322 .pa-grid { grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); } 5323 .pa-search input[type=text]{ min-width:140px; } 5324 .pa-toolbar { flex-direction:column; align-items:stretch; margin-bottom: 0; display: none;} 5325 .pa-right { display: none; } 5326 .pa-sort { width: 100%; justify-content:center; } 5327 .pa-mobile-filterbar { display: flex !important; } 5328} 5329 5330.pa-mobile-filterbar { 5331 display: none; 5332 align-items: center; 5333 gap: 8px; 5334 padding: 8px 0 8px 12px; 5335 background: #fff; 5336 width: 100vw; 5337} 5338.pa-mobile-filterbar .mf-scroll { 5339 display: flex; 5340 gap: 8px; 5341 overflow-x: auto; 5342 -webkit-overflow-scrolling: touch; 5343 scrollbar-width: none; /* Firefox */ 5344 flex: 1 1 auto; /* takes remaining width */ 5345} 5346.pa-mobile-filterbar .mf-scroll::-webkit-scrollbar { display: none; } /* WebKit */ 5347.pa-mobile-filterbar .mf-chip { 5348 flex: 0 0 auto; /* prevent shrinking, keep width based on content */ 5349 white-space: nowrap; 5350} 5351.pa-mobile-filterbar .mf-filter-btn { 5352 flex: 0 0 auto; /* do not shrink */ 5353 position: relative; /* keep in place, not inside scroller */ 5354 display: inline-flex; 5355 align-items: center; 5356 gap: 6px; 5357} 5358 5359/* Optional: ensure on small screens the button stays visible with min width */ 5360@media (max-width: 768px) { 5361 .pa-mobile-filterbar .mf-filter-btn { 5362 padding: 6px 10px; 5363 } 5364} 5365 5366/* Product Specification Chips */ 5367.pa-card .spec-chips { 5368 font-size: .6rem; 5369 color: #64748b; 5370 font-weight: 500; 5371 letter-spacing: .2px; 5372 margin-top: 2px; 5373 margin-bottom: 4px;
5374 line-height: 1.2; 5375 opacity: 0.9; 5376 max-width: 100%; 5377 overflow: hidden; 5378 text-overflow: ellipsis; 5379 white-space: nowrap; 5380} 5381 5382.pa-card .spec-chips .spec-divider { 5383 color: #94a3b8; 5384 font-weight: 400; 5385 margin: 0 1px; 5386} 5387</style> 5388 <style data-inlined="site-notice.css"> 5389/** 5390 * Site Notice Banner - Reusable Notification System 5391 * Dismissible top bar for announcements, holidays, maintenance, etc. 5392 */ 5393 5394.holiday-notice { 5395 position: fixed; 5396 top: 0; 5397 left: 0; 5398 right: 0; 5399 z-index: 9999; 5400 background: linear-gradient(135deg, #e2a642 0%, #C10DFF 100%); 5401 border-bottom: 2px solid rgba(255, 255, 255, 0.2); 5402 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 5403 animation: slideDown 0.4s cubic-bezier(0.4, 0, 0.2, 1); 5404 transform-origin: top; 5405} 5406 5407.holiday-notice.hidden { 5408 animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 5409} 5410 5411@keyframes slideDown { 5412 from { 5413 transform: translateY(-100%); 5414 opacity: 0; 5415 } 5416 to { 5417 transform: translateY(0); 5418 opacity: 1; 5419 } 5420} 5421 5422@keyframes slideUp { 5423 from { 5424 transform: translateY(0); 5425 opacity: 1; 5426 } 5427 to { 5428 transform: translateY(-100%); 5429 opacity: 0; 5430 } 5431} 5432 5433.holiday-notice-container { 5434 max-width: 1400px; 5435 margin: 0 auto; 5436 padding: 12px 20px; 5437 display: flex;
5438 align-items: center; 5439 justify-content: space-between; 5440 gap: 16px; 5441} 5442 5443.holiday-notice-content { 5444 display: flex; 5445 align-items: center; 5446 gap: 12px; 5447 flex: 1; 5448} 5449 5450.holiday-notice-icon { 5451 font-size: 20px; 5452 line-height: 1; 5453 flex-shrink: 0; 5454} 5455 5456.holiday-notice-text { 5457 color: #ffffff; 5458 font-size: 14px; 5459 font-weight: 500; 5460 line-height: 1.5; 5461 margin: 0; 5462 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 5463} 5464 5465.holiday-notice-emoji { 5466 display: inline-block; 5467 animation: bounce 2s ease-in-out infinite; 5468} 5469 5470@keyframes bounce { 5471 0%, 100% { 5472 transform: translateY(0); 5473 } 5474 50% { 5475 transform: translateY(-4px); 5476 } 5477} 5478 5479.holiday-notice-close { 5480 background: rgba(255, 255, 255, 0.2); 5481 border: 1px solid rgba(255, 255, 255, 0.3); 5482 border-radius: 6px; 5483 padding: 6px; 5484 cursor: pointer; 5485 color: #ffffff; 5486 transition: all 0.2s ease; 5487 flex-shrink: 0; 5488 display: flex; 5489 align-items: center; 5490 justify-content: center; 5491 width: 32px; 5492 height: 32px; 5493} 5494 5495.holiday-notice-close:hover { 5496 background: rgba(255, 255, 255, 0.3); 5497 transform: scale(1.05); 5498} 5499 5500.holiday-notice-close:active { 5501 transform: scale(0.95); 5502} 5503 5504.holiday-notice-close:focus { 5505 outline: 2px solid rgba(255, 255, 255, 0.5); 5506 outline-offset: 2px; 5507} 5508 5509/* Body padding adjustment when banner is visible */ 5510body.has-holiday-notice { 5511 padding-top: 52px; 5512} 5513 5514/* Tablet Styles */ 5515@media (max-width: 1024px) { 5516 .holiday-notice-container { 5517 padding: 10px 16px; 5518 } 5519 5520 .holiday-notice-text { 5521 font-size: 13px; 5522 } 5523 5524 .holiday-notice-icon { 5525 font-size: 18px; 5526 } 5527 5528 body.has-holiday-notice { 5529 padding-top: 48px; 5530 } 5531} 5532 5533/* Mobile Styles */ 5534@media (max-width: 640px) { 5535 .holiday-notice-container { 5536 padding: 10px 12px; 5537 gap: 8px; 5538 } 5539 5540 .holiday-notice-content { 5541 gap: 8px; 5542 } 5543 5544 .holiday-notice-text { 5545 font-size: 12px; 5546 line-height: 1.4; 5547 } 5548 5549 .holiday-notice-icon { 5550 font-size: 16px; 5551 } 5552 5553 .holiday-notice-close { 5554 width: 28px; 5555 height: 28px; 5556 } 5557 5558 .holiday-notice-close svg { 5559 width: 16px; 5560 height: 16px; 5561 } 5562 5563 body.has-holiday-notice { 5564 padding-top: 44px; 5565 } 5566} 5567 5568/* Ensure header doesn't get covered - removed z-index as it conflicts with dropdown stacking */ 5569/* .header, 5570.navbar, 5571header { 5572 position: relative; 5573 z-index: 100; 5574} */ 5575 5576</style> 5577 <style data-inlined="benchmark.css"> 5578/** 5579 * Benchmark Section Styles 5580 * For product detail page benchmark display 5581 */ 5582 5583.pd-benchmark-section { 5584 background: #fff; 5585 border-radius: 12px; 5586 padding: 24px; 5587 margin: 32px 0; 5588 box-shadow: 0 2px 8px rgba(0,0,0,0.06); 5589 border: 1px solid #e5e7eb; 5590} 5591 5592.benchmark-header { 5593 margin-bottom: 20px; 5594} 5595 5596.benchmark-title { 5597 font-size: 22px; 5598 font-weight: 700; 5599 color: #1a1a1a; 5600 margin: 0 0 12px 0; 5601 display: flex;
5602 align-items: center; 5603 gap: 8px; 5604} 5605 5606.benchmark-hint { 5607 font-size: 14px; 5608 color: #6b7280; 5609 margin: 0; 5610} 5611 5612/* Preview Badges */ 5613.benchmark-preview-badges { 5614 display: flex; 5615 flex-wrap: wrap; 5616 gap: 10px; 5617 margin-top: 12px; 5618} 5619 5620.benchmark-badge { 5621 display: inline-flex; 5622 align-items: center; 5623 gap: 6px; 5624 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5625 color: #fff; 5626 padding: 8px 16px; 5627 border-radius: 20px; 5628 font-size: 13px; 5629 font-weight: 500; 5630 box-shadow: 0 2px 6px rgba(102, 126, 234, 0.25); 5631 transition: all 0.2s ease; 5632} 5633 5634.benchmark-badge:hover { 5635 transform: translateY(-2px); 5636 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35); 5637} 5638 5639.benchmark-badge strong { 5640 font-weight: 600; 5641 opacity: 0.9; 5642} 5643 5644/* Load Button */ 5645.btn-load-benchmark { 5646 display: flex; 5647 align-items: center; 5648 justify-content: center; 5649 gap: 10px; 5650 width: 100%; 5651 max-width: 420px; 5652 padding: 14px 24px; 5653 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 5654 color: #fff; 5655 border: none; 5656 border-radius: 8px; 5657 font-size: 15px; 5658 font-weight: 600; 5659 cursor: pointer; 5660 transition: all 0.3s ease; 5661 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 5662 margin: 0 auto; 5663} 5664 5665.btn-load-benchmark:hover:not(:disabled) { 5666 transform: translateY(-2px); 5667 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); 5668} 5669 5670.btn-load-benchmark:disabled { 5671 opacity: 0.6; 5672 cursor: not-allowed; 5673} 5674 5675.btn-load-benchmark .btn-icon { 5676 font-size: 20px; 5677} 5678 5679.btn-load-benchmark.loading .btn-icon { 5680 animation: spin 1s linear infinite; 5681} 5682 5683@keyframes spin { 5684 from { transform: rotate(0deg); } 5685 to { transform: rotate(360deg); } 5686} 5687 5688/* Benchmark Content */ 5689.benchmark-content { 5690 margin-top: 24px; 5691 padding-top: 24px; 5692 border-top: 2px solid #f3f4f6; 5693} 5694 5695.benchmark-loading { 5696 text-align: center; 5697 padding: 40px 20px; 5698 color: #6b7280; 5699} 5700 5701.benchmark-loading-spinner { 5702 width: 48px; 5703 height: 48px; 5704 margin: 0 auto 16px; 5705 border: 4px solid #f3f4f6; 5706 border-top-color: #667eea; 5707 border-radius: 50%; 5708 animation: spin 1s linear infinite; 5709} 5710 5711.benchmark-error { 5712 text-align: center; 5713 padding: 32px 20px; 5714 color: #e2a642; 5715 background: #fee2e2; 5716 border-radius: 8px; 5717} 5718 5719/* Benchmark Data Display */ 5720.benchmark-data-grid { 5721 display: grid; 5722 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 5723 gap: 16px; 5724 margin-bottom: 24px; 5725} 5726 5727.benchmark-card { 5728 background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); 5729 border: 1px solid #e5e7eb; 5730 border-radius: 10px; 5731 padding: 18px; 5732 text-align: center; 5733 transition: all 0.2s ease; 5734} 5735 5736.benchmark-card:hover { 5737 transform: translateY(-3px); 5738 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.15); 5739} 5740 5741.benchmark-card-label { 5742 font-size: 13px; 5743 color: #6b7280; 5744 font-weight: 500; 5745 margin-bottom: 8px; 5746 text-transform: uppercase; 5747 letter-spacing: 0.5px; 5748} 5749 5750.benchmark-card-value { 5751 font-size: 24px; 5752 font-weight: 700; 5753 color: #1a1a1a; 5754 line-height: 1.2; 5755} 5756 5757.benchmark-card-value.large { 5758 font-size: 28px; 5759} 5760 5761.benchmark-card-subtitle { 5762 font-size: 12px; 5763 color: #9ca3af; 5764 margin-top: 4px; 5765} 5766 5767/* Spec Details */ 5768.benchmark-specs { 5769 background: #f9fafb; 5770 border-radius: 10px; 5771 padding: 20px; 5772 margin-top: 20px; 5773} 5774 5775.benchmark-specs-title { 5776 font-size: 16px; 5777 font-weight: 600; 5778 color: #1a1a1a; 5779 margin: 0 0 16px 0; 5780} 5781 5782.benchmark-spec-row { 5783 display: flex; 5784 justify-content: space-between; 5785 padding: 10px 0; 5786 border-bottom: 1px solid #e5e7eb; 5787} 5788 5789.benchmark-spec-row:last-child { 5790 border-bottom: none; 5791} 5792 5793.benchmark-spec-label { 5794 font-size: 14px; 5795 color: #6b7280; 5796 font-weight: 500; 5797} 5798 5799.benchmark-spec-value { 5800 font-size: 14px; 5801 color: #1a1a1a; 5802 font-weight: 600; 5803 text-align: right; 5804} 5805 5806/* Score Badges */ 5807.benchmark-score-badge { 5808 display: inline-block; 5809 padding: 6px 14px; 5810 border-radius: 20px; 5811 font-size: 13px; 5812 font-weight: 600; 5813 margin-top: 8px; 5814} 5815 5816.benchmark-score-badge.excellent { 5817 background: #10b981; 5818 color: #fff; 5819} 5820 5821.benchmark-score-badge.good { 5822 background: #3b82f6; 5823 color: #fff; 5824} 5825 5826.benchmark-score-badge.average { 5827 background: #C10DFF; 5828 color: #fff; 5829} 5830 5831.benchmark-score-badge.poor { 5832 background: #e2a642; 5833 color: #fff; 5834} 5835 5836/* Attribution */ 5837.benchmark-attribution { 5838 text-align: center; 5839 margin-top: 24px; 5840 padding-top: 20px; 5841 border-top: 1px solid #e5e7eb; 5842 font-size: 13px; 5843 color: #9ca3af; 5844} 5845 5846.benchmark-attribution a { 5847 color: #667eea; 5848 text-decoration: none; 5849 font-weight: 500; 5850} 5851 5852.benchmark-attribution a:hover { 5853 text-decoration: underline; 5854} 5855 5856/* Responsive */ 5857@media (max-width: 768px) { 5858 .pd-benchmark-section { 5859 padding: 16px; 5860 margin: 20px 0; 5861 } 5862 5863 .benchmark-title { 5864 font-size: 18px; 5865 } 5866 5867 .benchmark-preview-badges { 5868 flex-direction: column; 5869 gap: 8px; 5870 } 5871 5872 .benchmark-badge { 5873 justify-content: center; 5874 } 5875 5876 .btn-load-benchmark { 5877 font-size: 14px; 5878 padding: 12px 20px; 5879 } 5880 5881 .benchmark-data-grid { 5882 grid-template-columns: 1fr 1fr; 5883 gap: 12px; 5884 } 5885 5886 .benchmark-card { 5887 padding: 14px; 5888 } 5889 5890 .benchmark-card-value { 5891 font-size: 20px; 5892 } 5893 5894 .benchmark-spec-row { 5895 flex-direction: column; 5896 gap: 4px; 5897 } 5898 5899 .benchmark-spec-value { 5900 text-align: left; 5901 } 5902} 5903 5904@media (max-width: 480px) { 5905 .benchmark-data-grid { 5906 grid-template-columns: 1fr; 5907 } 5908} 5909 5910</style> 5911 <style data-inlined="product-filters.css"> 5912/* ========================================================= 5913 Product Filters (Desktop + Mobile Drawer) 5914 Light Only + FIX: 5915 - Desktop: f-actions selalu tampil (static bottom) 5916 - Mobile: konten filter dapat discroll 5917 ========================================================= */ 5918 5919/* ========== Layout wrapper (tidak mengubah grid produk) ========== */ 5920.product-archive.with-filters .pa-layout { 5921 display:flex; 5922 gap:24px; 5923 align-items:flex-start; 5924} 5925.pa-main { flex:1; min-width:0; } 5926 5927/* ========================================================= 5928 DESKTOP PANEL 5929 ========================================================= */ 5930.pa-filters { 5931 width:300px; 5932 background:linear-gradient(145deg,#ffffff 0%,#f8fafc 55%,#f1f5f9 100%); 5933 border:1px solid #e2e8f0; 5934 padding:18px 18px 12px; /* bottom dikurangi karena f-actions punya padding sendiri */ 5935 border-radius:18px; 5936 position:sticky; 5937 top:80px; 5938 height:calc(100vh - 100px); /* tinggi stabil untuk inner scroll */ 5939 font-size:14px; 5940 box-shadow:0 1px 2px rgba(0,0,0,.04),0 4px 14px rgba(15,23,42,.08); 5941 backdrop-filter:blur(3px); 5942 -webkit-backdrop-filter:blur(3px);
5943 transition:box-shadow .3s, background .4s; 5944 display:flex; 5945 flex-direction:column; 5946 overflow:hidden; /* cegah double scroll, inner yang scroll */ 5947} 5948.pa-filters::before { 5949 content:""; 5950 position:absolute; 5951 inset:0 auto 0 0; 5952 width:7px; 5953 border-radius:18px 0 0 18px; 5954 background:linear-gradient(180deg,#C10DFF 0%,#C10DFF 35%,#1d6ce0 100%); 5955} 5956.pa-filters.active { box-shadow:0 8px 32px -4px rgba(15,23,42,.35); } 5957 5958/* Scroll area (desktop/mode shared) */ 5959.filters-scroll { 5960 flex:1; 5961 min-height:0; /* wajib agar overflow terbaca dalam flex container */ 5962 overflow:auto; 5963 padding-right:6px; 5964 margin-right:-6px; /* kompensasi scrollbar */ 5965 scrollbar-width:thin; 5966 overscroll-behavior:contain; 5967} 5968.filters-scroll::-webkit-scrollbar { width:8px; } 5969.filters-scroll::-webkit-scrollbar-thumb { 5970 background:#cbd5e1; 5971 border-radius:20px; 5972 border:2px solid #f1f5f9; 5973} 5974.filters-scroll::-webkit-scrollbar-thumb:hover { background:#94a3b8; } 5975 5976/* Heading */ 5977.pa-filters h2 { 5978 margin:0 0 14px; 5979 font-size:20px; 5980 font-weight:700; 5981 letter-spacing:.4px; 5982 line-height:1.15; 5983 background:linear-gradient(90deg,#0f172a,#1d6ce0 60%,#0f172a); 5984 -webkit-background-clip:text; 5985 color:transparent; 5986 background-size:200% 100%; 5987 animation:titleSheen 8s linear infinite; 5988} 5989@keyframes titleSheen { 5990 0%{background-position:0 0} 5991 50%{background-position:100% 0} 5992 100%{background-position:0 0} 5993} 5994 5995/* Form groups */ 5996.pa-filters .f-group { margin-bottom:18px; position:relative; } 5997.pa-filters label { 5998 display:block; 5999 font-weight:600; 6000 margin:0 0 6px; 6001 font-size:13px; 6002 color:#0f172a; 6003 letter-spacing:.3px; 6004} 6005 6006/* Inputs / selects */ 6007.pa-filters input[type=text], 6008.pa-filters input[type=number], 6009.pa-filters select { 6010 width:100%; 6011 padding:7px 10px; 6012 border:1px solid #cbd5e1; 6013 border-radius:8px; 6014 font-size:13px; 6015 min-height:34px; 6016 background:#ffffff; 6017 box-sizing:border-box; 6018 color:#0f172a; 6019 transition:border-color .25s, box-shadow .25s, background .25s; 6020 -webkit-appearance:none; 6021 -moz-appearance:none; 6022 appearance:none; 6023} 6024.pa-filters select { 6025 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"); 6026 background-repeat:no-repeat; 6027 background-position:right 10px center; 6028 background-size:12px; 6029 padding-right:32px; 6030} 6031.pa-filters input[type=text]:hover, 6032.pa-filters input[type=number]:hover, 6033.pa-filters select:hover { border-color:#94b5ec; } 6034.pa-filters input[type=text]:focus, 6035.pa-filters input[type=number]:focus, 6036.pa-filters select:focus { 6037 outline:none; 6038 border-color:#1d6ce0; 6039 box-shadow:0 0 0 3px rgba(29,108,224,.25); 6040 background:#f8fbff; 6041} 6042.pa-filters .range { display:flex; gap:10px; } 6043.pa-filters .range input { flex:1; } 6044.pa-filters .hint { font-size:11px; color:#64748b; margin-top:6px; } 6045 6046/* Spec blocks */ 6047.f-specs { margin-top:4px; } 6048.spec-attr { 6049 margin:0 0 14px; 6050 padding:12px 12px 10px; 6051 border:1px solid #e2e8f0; 6052 border-radius:14px; 6053 background:linear-gradient(130deg,#ffffff 0%,#f7faff 85%); 6054 position:relative; 6055 transition:border-color .3s, box-shadow .3s, background .4s; 6056} 6057.spec-attr.collapsed { padding-bottom:8px; } 6058.spec-attr.has-selected { 6059 border-color:#1d6ce0; 6060 box-shadow:0 0 0 2px rgba(29,108,224,.15),0 2px 8px -2px rgba(29,108,224,.35); 6061 background:linear-gradient(135deg,#ffffff 0%,#e9f2ff 100%); 6062} 6063.spec-attr-title { 6064 font-weight:600; 6065 margin:0 0 8px; 6066 font-size:13px; 6067 display:flex; 6068 justify-content:space-between; 6069 align-items:center; 6070 cursor:pointer; 6071 color:#0f172a; 6072 user-select:none; 6073} 6074.spec-attr-title .chevron { 6075 font-size:10px; 6076 transition:transform .25s, opacity .25s; 6077 opacity:.65; 6078} 6079.spec-attr.collapsed .spec-attr-title .chevron { 6080 transform:rotate(-90deg); 6081 opacity:.5; 6082} 6083 6084/* Spec options */ 6085.spec-attr-options { display:flex; flex-wrap:wrap; gap:6px; } 6086.spec-attr-options label { 6087 display:inline-flex; 6088 align-items:center; 6089 gap:4px; 6090 font-weight:500; 6091 border:1px solid #d4dde7; 6092 padding:6px 9px; 6093 border-radius:30px; 6094 cursor:pointer; 6095 background:#f3f6fa; 6096 font-size:12px; 6097 line-height:1.1; 6098 transition:background .25s, border-color .25s, color .25s, box-shadow .25s; 6099 position:relative; 6100} 6101.spec-attr-options label:hover { background:#e8f1ff; border-color:#b1cff3; } 6102.spec-attr-options input { 6103 margin:0; 6104 position:absolute; 6105 inset:0; 6106 opacity:0;
6107 cursor:pointer; 6108} 6109.spec-attr-options label:has(input:checked) { 6110 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6111 border-color:#1d6ce0; 6112 color:#ffffff; 6113 box-shadow:0 2px 6px -1px rgba(29,108,224,.55); 6114} 6115.spec-attr-options label:has(input:checked)::after { 6116 content:""; 6117 position:absolute; 6118 right:6px; 6119 top:6px; 6120 width:6px; 6121 height:6px; 6122 background:#fff; 6123 border-radius:50%; 6124 box-shadow:0 0 0 4px rgba(255,255,255,.25),0 0 0 7px rgba(29,108,224,.5); 6125 animation:pulseDot 1.9s ease-out infinite; 6126} 6127@keyframes pulseDot { 6128 0%{transform:scale(.8);opacity:.9} 6129 60%{transform:scale(1.4);opacity:.15} 6130 100%{transform:scale(.8);opacity:.9} 6131} 6132 6133/* Badges */ 6134.pa-filter-badges { margin:0 0 18px; display:flex; flex-wrap:wrap; gap:6px; } 6135.pa-filter-badge { 6136 display:inline-flex; 6137 gap:6px; 6138 align-items:center; 6139 background:linear-gradient(135deg,#eef4ff 0%,#e0edff 100%); 6140 border:1px solid #b4cff3; 6141 padding:5px 10px 5px 12px; 6142 border-radius:22px; 6143 margin:2px 4px 0 0; 6144 font-size:11px; 6145 font-weight:500; 6146 line-height:1; 6147 color:#0f172a; 6148 position:relative; 6149 overflow:hidden; 6150} 6151.pa-filter-badge::before { 6152 content:""; 6153 position:absolute; 6154 inset:0; 6155 background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.35),rgba(255,255,255,0)); 6156 transform:translateX(-100%); 6157 animation:badgeSheen 6s linear infinite; 6158} 6159@keyframes badgeSheen { 6160 0%{transform:translateX(-100%)} 6161 55%{transform:translateX(120%)} 6162 100%{transform:translateX(120%)} 6163} 6164.pa-filter-badge a.clear { 6165 text-decoration:none; 6166 color:#c9832a; 6167 font-weight:700; 6168 padding:0 3px; 6169 display:inline-flex; 6170 align-items:center; 6171 justify-content:center; 6172 border-radius:50%; 6173 line-height:1; 6174 transition:background .25s, color .25s; 6175} 6176.pa-filter-badge a.clear:hover { background:#ffeded; color:#a36422; } 6177.pa-btn-filter { width: 100%;} 6178 6179/* ========================================================= 6180 ACTIONS (DESKTOP) - STATIC BOTTOM 6181 ========================================================= */ 6182.f-actions { 6183 margin-top:10px; 6184 display:flex; 6185 gap:12px; 6186 flex-wrap:wrap; 6187 padding:16px 0 4px; 6188 border-top:1px solid #e2e8f0; 6189 background:linear-gradient(180deg,rgba(255,255,255,0) 0%,#f1f5f9 70%); 6190 flex-shrink:0; 6191} 6192.f-actions button { 6193 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6194 color:#fff; 6195 border:none; 6196 padding:10px 20px; 6197 border-radius:12px; 6198 cursor:pointer; 6199 font-size:14px; 6200 font-weight:600; 6201 letter-spacing:.3px; 6202 display:inline-flex; 6203 align-items:center; 6204 gap:6px; 6205 box-shadow:0 4px 14px -2px rgba(29,108,224,.45); 6206 transition:transform .25s, box-shadow .3s; 6207} 6208.f-actions button:hover { transform:translateY(-3px); box-shadow:0 8px 26px -4px rgba(29,108,224,.6); } 6209.f-actions button:active { transform:translateY(-1px) scale(.98); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6210.f-actions .reset { 6211 font-size:12px; 6212 text-decoration:none; 6213 color:#e2a642; 6214 align-self:center; 6215 font-weight:600; 6216 padding:4px 6px; 6217 border-radius:8px; 6218 transition:background .25s, color .25s; 6219} 6220.f-actions .reset:hover { background:#ffe6e6; color:#c9832a; text-decoration:underline; } 6221.pa-filters .close-drawer { display: none; } 6222 6223/* Form wrapper: flex container agar inner .filters-scroll bisa scroll di desktop */ 6224.filter-form { 6225 flex: 1; 6226 min-height: 0; 6227 display: flex; 6228 flex-direction: column; 6229 overflow: hidden; 6230} 6231 6232/* ========================================================= 6233 FILTER TOGGLE (MOBILE TRIGGER) 6234 ========================================================= */ 6235.filter-toggle { 6236 display:none; 6237 margin:0 0 14px; 6238 background:linear-gradient(135deg,#1d6ce0 0%,#155ab8 100%); 6239 color:#fff; 6240 border:none; 6241 padding:11px 18px; 6242 font-size:14px; 6243 font-weight:600; 6244 border-radius:14px; 6245 cursor:pointer; 6246 letter-spacing:.4px; 6247 box-shadow:0 4px 14px -2px rgba(29,108,224,.55); 6248 position:relative; 6249 z-index:100; 6250 transition:box-shadow .3s, transform .25s; 6251} 6252.filter-toggle:hover { box-shadow:0 8px 26px -4px rgba(29,108,224,.6); transform:translateY(-2px); } 6253.filter-toggle:active { transform:translateY(0); box-shadow:0 4px 14px -4px rgba(29,108,224,.55); } 6254.filter-toggle .count-badge { 6255 display:inline-block; 6256 background:linear-gradient(135deg,#C10DFF 0%,#C10DFF 55%,#C10DFF 100%); 6257 color:#1d2b44; 6258 font-size:11px; 6259 font-weight:700; 6260 padding:3px 7px 4px;
6261 border-radius:30px; 6262 margin-left:8px; 6263 box-shadow:0 0 0 2px #ffffff,0 0 0 4px #1d6ce0; 6264} 6265 6266/* Overlay (mobile) */ 6267.pa-filters-overlay { display:none; } 6268 6269/* ========================================================= 6270 MOBILE DRAWER 6271 ========================================================= */ 6272@media (max-width:768px){ 6273 .product-archive.with-filters .pa-layout { flex-direction:column; gap:12px; } 6274 .filter-toggle { display:inline-flex; align-items:center; gap:6px; } 6275 6276 .pa-filters-overlay { 6277 position:fixed; 6278 inset:0; 6279 background:rgba(0,20,45,.55); 6280 z-index:998; 6281 opacity:0; 6282 transition:opacity .35s; 6283 display:block; 6284 pointer-events:none; 6285 backdrop-filter:blur(2px); 6286 } 6287 .pa-filters-overlay.active { opacity:1; pointer-events:auto; } 6288 6289 .pa-filters { 6290 position:fixed; 6291 top:auto; 6292 left:0; right:0; bottom:0; 6293 max-height:92vh; 6294 height:auto; 6295 width:100%; 6296 border-radius:30px 30px 0 0; 6297 border:1px solid #dbe4ef; 6298 padding:26px 22px 0; 6299 z-index:10000; /* di atas bottom-nav (9998) */ 6300 transform:translateY(102%); 6301 transition:transform .55s cubic-bezier(.68,-0.35,.25,1); 6302 box-shadow:0 -4px 40px -6px rgba(15,23,42,.45); 6303 overflow:hidden; /* outer hidden, inner scroll */ 6304 } 6305 .pa-filters.active { transform:translateY(0); } 6306 6307 /* Drag handle indicator */ 6308 .pa-filters::after { 6309 content:""; 6310 display:block; 6311 position:absolute; 6312 top:10px; 6313 left:50%; 6314 transform:translateX(-50%); 6315 width:36px; 6316 height:4px; 6317 background:#cbd5e1; 6318 border-radius:4px; 6319 } 6320 body.filter-open { overflow:hidden; touch-action:none; } 6321 6322 /* Restore mobile-friendly input sizes (prevent iOS zoom on focus) */ 6323 .pa-filters input[type=text], 6324 .pa-filters input[type=number], 6325 .pa-filters select { 6326 font-size:16px; 6327 min-height:44px; 6328 padding:10px 12px; 6329 border-radius:10px; 6330 } 6331 .pa-filters select { padding-right:32px; } 6332 6333 .filters-scroll { 6334 flex:1; 6335 overflow:auto; 6336 padding:0 0 120px; /* ruang di atas bar actions */ 6337 margin:0; 6338 -webkit-overflow-scrolling:touch; 6339 } 6340 6341 .pa-filters h2 { font-size:19px; margin-bottom:10px; } 6342 6343 .pa-filters .close-drawer { 6344 display: block; 6345 position:absolute; 6346 right:14px; top:12px; 6347 background:linear-gradient(135deg,#e2a642 0%,#c9832a 100%); 6348 color:#fff; 6349 padding:7px 14px; 6350 border:none; 6351 border-radius:10px; 6352 font-size:12px; 6353 font-weight:600; 6354 cursor:pointer; 6355 box-shadow:0 3px 10px -2px rgba(226,166,66,.55); 6356 transition:transform .25s, box-shadow .3s; 6357 z-index: 99; 6358 } 6359 .pa-filters .close-drawer:hover { transform:translateY(-2px); box-shadow:0 6px 18px -4px rgba(226,166,66,.6); } 6360 6361 .spec-attr { border-radius:18px; } 6362 .spec-attr-options label { font-size:11px; padding:5px 9px; } 6363 6364 /* Actions fixed di viewport bawah */ 6365 .f-actions { 6366 position:fixed; 6367 left:0; right:0; bottom:0; 6368 margin:0; 6369 border-top:1px solid #d6dee8; 6370 padding:14px 16px calc(16px + env(safe-area-inset-bottom)); 6371 background:#ffffffea; 6372 backdrop-filter:blur(6px); 6373 box-shadow:0 -4px 22px -6px rgba(15,23,42,.25); 6374 justify-content:space-between; 6375 z-index:10001; /* di atas pa-filters (10000) dan bottom-nav (9998) */ 6376 } 6377 6378 /* Sembunyikan bottom-nav saat filter drawer terbuka */ 6379 body.filter-open .bottom-nav { 6380 display: none !important; 6381 } 6382 .f-actions button { 6383 flex:1 1 auto; 6384 justify-content:center; 6385 padding:14px 14px; 6386 font-size:15px; 6387 border-radius:14px; 6388 } 6389 .f-actions .reset { 6390 order:-1; 6391 font-size:12px; 6392 padding:2px 8px; 6393 } 6394} 6395 6396/* Scrollbar (outer panel desktop only kept for compatibility) */ 6397.pa-filters::-webkit-scrollbar { width:0; } /* disembunyikan karena inner yang scroll */ 6398 6399/* Utilities */ 6400.hidden { display:none !important; } 6401.gradient-text { 6402 background:linear-gradient(90deg,#1d6ce0,#155ab8,#1d6ce0); 6403 -webkit-background-clip:text; 6404 color:transparent; 6405 animation:titleSheen 10s linear infinite; 6406} 6407.glow-focus:focus {
6408 outline:none !important; 6409 box-shadow:0 0 0 3px rgba(29,108,224,.4) !important; 6410 border-color:#1d6ce0 !important; 6411} 6412 6413/* Simple fade (opsional) */ 6414@keyframes fadeIn { 6415 from { opacity:0; transform:translateY(8px); } 6416 to { opacity:1; transform:translateY(0); } 6417} 6418</style> 6419 <style data-inlined="mega-menu-tiles.css"> 6420/* Tile Mega Menu â Modern E-commerce Enhanced (2025) */ 6421 6422/* Design Tokens */ 6423:root { 6424 --mm2-bg: #ffffff; 6425 --mm2-surface: #ffffff; 6426 --mm2-surface-elevated: rgba(255,255,255,0.98); 6427 --mm2-muted: #f8fafc; 6428 --mm2-border: #e2e8f0; 6429 --mm2-border-strong: #cbd5e1; 6430 --mm2-accent: #6366f1; 6431 --mm2-accent-hover: #4f46e5; 6432 --mm2-accent-10: rgba(99,102,241,.10); 6433 --mm2-accent-20: rgba(99,102,241,.20); 6434 --mm2-fg: #0f172a; 6435 --mm2-fg-muted: #64748b; 6436 --mm2-fg-light: #94a3b8; 6437 6438 /* Enhanced Shadows - Multi-layer elevation */ 6439 --mm2-shadow-sm: 0 2px 8px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04); 6440 --mm2-shadow-md: 0 8px 24px rgba(15,23,42,.10), 0 2px 8px rgba(15,23,42,.06); 6441 --mm2-shadow-lg: 0 20px 60px rgba(15,23,42,.14), 0 8px 24px rgba(15,23,42,.08); 6442 --mm2-shadow-xl: 0 32px 80px rgba(15,23,42,.18), 0 12px 32px rgba(15,23,42,.10); 6443 --mm2-shadow-hover: 0 16px 40px rgba(15,23,42,.16), 0 6px 16px rgba(15,23,42,.08); 6444 6445 /* Border Radius */ 6446 --mm2-radius-xl: 20px; 6447 --mm2-radius-lg: 16px; 6448 --mm2-radius-md: 12px; 6449 --mm2-radius-sm: 10px; 6450 6451 --mm2-z: 60; 6452 6453 /* Animation Timing */ 6454 --mm2-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 6455 --mm2-transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 6456 --mm2-transition-slow: 350ms cubic-bezier(0.4, 0, 0.2, 1); 6457} 6458 6459.navbar { position: relative; z-index: var(--mm2-z); } 6460 6461/* Trigger Button - Enhanced */ 6462.mm2-trigger { 6463 display: inline-flex; align-items: center; gap: 8px; 6464 padding: 10px 14px; border-radius: var(--mm2-radius-md); 6465 border: 1.5px solid var(--mm2-border); 6466 background: var(--mm2-surface); color: var(--mm2-fg); 6467 cursor: pointer; user-select: none; 6468 transition: all var(--mm2-transition-fast); 6469 text-decoration: none; 6470 font-weight: 600; 6471 box-shadow: var(--mm2-shadow-sm); 6472} 6473.mm2-trigger:hover { 6474 border-color: var(--mm2-accent); 6475 background: var(--mm2-muted); 6476 box-shadow: var(--mm2-shadow-md); 6477 transform: translateY(-1px); 6478} 6479.mm2-trigger:active { transform: translateY(0); } 6480.mm2-trigger svg {max-width: unset !important; transition: transform var(--mm2-transition-fast); } 6481.mm2-trigger:hover svg { transform: scale(1.05); } 6482 6483/* Backdrop - Enhanced Glassmorphism */ 6484.mm2-backdrop { 6485 position: fixed; 6486 inset: 0; 6487 top: 0; 6488 left: 0; 6489 right: 0; 6490 bottom: 0; 6491 background: rgba(15,23,42,.40); 6492 backdrop-filter: blur(8px) saturate(1.2); 6493 -webkit-backdrop-filter: blur(8px) saturate(1.2); 6494 opacity: 0; 6495 pointer-events: none; 6496 z-index: 499; 6497 transition: opacity var(--mm2-transition-base); 6498} 6499.mm2-open .mm2-backdrop { opacity: 1; pointer-events: auto; } 6500 6501/* Panel - Glassmorphism Enhanced */ 6502.mm2-panel { 6503 position: fixed; 6504 left: 50%; 6505 top: 120px; 6506 transform: translateX(-50%) translateY(-16px); 6507 width: min(1200px, 95vw); 6508 background: var(--mm2-surface-elevated); 6509 backdrop-filter: blur(20px) saturate(1.8); 6510 -webkit-backdrop-filter: blur(20px) saturate(1.8); 6511 border: 1.5px solid var(--mm2-border-strong); 6512 border-radius: var(--mm2-radius-xl); 6513 box-shadow: var(--mm2-shadow-xl); 6514 display: none; 6515 overflow: hidden; 6516 opacity: 0; 6517 z-index: 500; 6518 transition: transform var(--mm2-transition-slow), opacity var(--mm2-transition-base); 6519} 6520.mm2-open .mm2-panel { 6521 display: block; 6522 opacity: 1; 6523 transform: translateX(-50%) translateY(0); 6524} 6525 6526/* View switcher */ 6527.mm2-view { display: none; } 6528.mm2-open .mm2-view.is-active { display: block; } 6529 6530/* Top grid (level-1 tiles) - Enhanced */ 6531.mm2-top { 6532 padding: 24px; 6533 max-height: 75vh; overflow: auto; 6534} 6535.mm2-grid { 6536 display: grid; 6537 grid-template-columns: repeat(4, minmax(0,1fr)); 6538 gap: 14px; 6539} 6540@media (max-width: 1200px) { .mm2-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } } 6541@media (max-width: 960px) { .mm2-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; } } 6542 6543/* Stagger animation for cards */ 6544.mm2-open .mm2-card { 6545 animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1) backwards; 6546} 6547.mm2-open .mm2-card:nth-child(1) { animation-delay: 0.02s; } 6548.mm2-open .mm2-card:nth-child(2) { animation-delay: 0.04s; } 6549.mm2-open .mm2-card:nth-child(3) { animation-delay: 0.06s; } 6550.mm2-open .mm2-card:nth-child(4) { animation-delay: 0.08s; } 6551.mm2-open .mm2-card:nth-child(5) { animation-delay: 0.10s; } 6552.mm2-open .mm2-card:nth-child(6) { animation-delay: 0.12s; } 6553.mm2-open .mm2-card:nth-child(7) { animation-delay: 0.14s; } 6554.mm2-open .mm2-card:nth-child(8) { animation-delay: 0.16s; } 6555.mm2-open .mm2-card:nth-child(9) { animation-delay: 0.18s; } 6556.mm2-open .mm2-card:nth-child(10) { animation-delay: 0.20s; } 6557.mm2-open .mm2-card:nth-child(11) { animation-delay: 0.22s; } 6558.mm2-open .mm2-card:nth-child(12) { animation-delay: 0.24s; } 6559.mm2-open .mm2-card:nth-child(n+13) { animation-delay: 0.26s; } 6560 6561@keyframes slideInUp { 6562 from { 6563 opacity: 0; 6564 transform: translateY(12px); 6565 } 6566 to { 6567 opacity: 1;
6568 transform: translateY(0); 6569 } 6570} 6571 6572.mm2-card { 6573 position: relative; 6574 display: flex; gap: 14px; align-items: center; 6575 padding: 16px; border-radius: var(--mm2-radius-md); 6576 background: var(--mm2-surface); 6577 border: 1.5px solid var(--mm2-border); 6578 text-decoration: none; color: var(--mm2-fg); 6579 transition: all var(--mm2-transition-fast); 6580 min-height: 72px; 6581 box-shadow: var(--mm2-shadow-sm); 6582} 6583.mm2-card:hover { 6584 border-color: var(--mm2-accent); 6585 background: linear-gradient(135deg, rgba(255,255,255,1), rgba(248,250,252,1)); 6586 box-shadow: var(--mm2-shadow-hover); 6587 transform: translateY(-3px); 6588} 6589.mm2-card:active { transform: translateY(-1px); } 6590 6591/* Icon with category-specific gradients */ 6592.mm2-card .mm2-ico { 6593 width: 48px; height: 48px; border-radius: 12px; 6594 display: inline-flex; align-items: center; justify-content: center; 6595 background: linear-gradient(135deg, #e0e7ff, #fff); 6596 border: 1.5px solid #c7d2fe; 6597 color: #4f46e5; 6598 flex-shrink: 0; 6599 transition: all var(--mm2-transition-fast); 6600 box-shadow: 0 2px 8px rgba(99,102,241,0.1); 6601} 6602.mm2-card:hover .mm2-ico { 6603 transform: scale(1.08) rotate(2deg); 6604 box-shadow: 0 4px 16px rgba(99,102,241,0.2); 6605} 6606.mm2-card .mm2-ico svg { transition: transform var(--mm2-transition-fast); } 6607.mm2-card:hover .mm2-ico svg { transform: scale(1.1); } 6608 6609.mm2-card .mm2-title { 6610 font-weight: 700; 6611 font-size: 15px; 6612 letter-spacing: -0.01em; 6613 line-height: 1.3; 6614} 6615.mm2-card .mm2-sub { 6616 color: var(--mm2-fg-muted); 6617 font-size: 12.5px; 6618 margin-top: 3px; 6619 display: flex; 6620 align-items: center; 6621 gap: 4px; 6622 white-space: nowrap; 6623 overflow: hidden; 6624 text-overflow: ellipsis; 6625} 6626.mm2-card .mm2-count { 6627 display: inline-flex; 6628 padding: 2px 8px; 6629 border-radius: 6px; 6630 background: var(--mm2-accent-10); 6631 color: var(--mm2-accent); 6632 font-weight: 600; 6633 font-size: 11px; 6634 margin-left: 4px; 6635} 6636 6637/* Detail view (level-2 + level-3) - Enhanced */ 6638.mm2-detail { 6639 position: relative; 6640 padding: 16px 24px 24px; 6641 max-height: 70vh; overflow: auto; 6642} 6643.mm2-detail-header { 6644 display: flex; align-items: center; gap: 14px; 6645 padding: 10px 0 16px; border-bottom: 2px solid var(--mm2-muted); margin-bottom: 16px; 6646} 6647.mm2-back { 6648 display: inline-flex; align-items: center; justify-content: center; 6649 width: 40px; height: 40px; border-radius: 12px; 6650 border: 1.5px solid var(--mm2-border); background: #fff; color: var(--mm2-fg); 6651 cursor: pointer; 6652 transition: all var(--mm2-transition-fast); 6653 box-shadow: var(--mm2-shadow-sm); 6654} 6655.mm2-back:hover { 6656 border-color: var(--mm2-accent); 6657 background: var(--mm2-muted); 6658 transform: translateX(-3px); 6659 box-shadow: var(--mm2-shadow-md); 6660} 6661.mm2-detail-title { 6662 font-weight: 800; 6663 font-size: 18px; 6664 letter-spacing: -0.02em; 6665} 6666 6667/* Two-pane layout (desktop) - Enhanced */ 6668.mm2-detail-layout { 6669 display: grid; 6670 grid-template-columns: 280px 1fr; 6671 gap: 20px; 6672} 6673 6674/* Left: subnav (level-2 list) - Enhanced */ 6675.mm2-subnav { 6676 position: sticky; top: 8px; 6677 align-self: start; 6678 max-height: calc(70vh - 56px); 6679 overflow: auto; 6680 padding-right: 6px; 6681} 6682.mm2-subnav::-webkit-scrollbar { width: 8px; height: 8px; } 6683.mm2-subnav::-webkit-scrollbar-track { background: transparent; } 6684.mm2-subnav::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; } 6685.mm2-subnav::-webkit-scrollbar-thumb:hover { background: #94a3b8; } 6686 6687.mm2-subitem { 6688 display: flex; align-items: center; justify-content: flex-start; gap: 10px; 6689 width: 100%; 6690 padding: 12px 14px; 6691 border-radius: 12px; 6692 border: 1.5px solid transparent; 6693 background: transparent; 6694 color: var(--mm2-fg); 6695 cursor: pointer; 6696 text-align: left; 6697 transition: all var(--mm2-transition-fast); 6698 margin-bottom: 6px; 6699 font-weight: 600; 6700 font-size: 14px; 6701 position: relative; 6702} 6703.mm2-subitem::before { 6704 content: ''; 6705 position: absolute; 6706 left: 0; 6707 top: 50%; 6708 transform: translateY(-50%); 6709 width: 3px; 6710 height: 0; 6711 background: var(--mm2-accent); 6712 border-radius: 0 2px 2px 0; 6713 transition: height var(--mm2-transition-fast); 6714} 6715.mm2-subitem:hover { 6716 background: var(--mm2-muted); 6717 color: var(--mm2-accent-hover); 6718} 6719.mm2-subitem.active { 6720 background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(99,102,241,0.04)); 6721 border-color: var(--mm2-accent-20); 6722 color: var(--mm2-accent-hover); 6723 box-shadow: inset 0 0 0 1px rgba(99,102,241,0.1); 6724} 6725.mm2-subitem.active::before { 6726 height: 24px; 6727} 6728 6729/* Right: subcontent (level-3 grid) - Enhanced */ 6730.mm2-subcontent { min-height: 240px; } 6731.mm2-links { 6732 display: grid; gap: 12px 14px; 6733 grid-template-columns: repeat(3, minmax(0,1fr)); 6734} 6735@media (max-width: 1200px) { .mm2-links { grid-template-columns: repeat(2, minmax(0,1fr)); } } 6736 6737.mm2-link { 6738 display: flex; align-items: center; gap: 12px; 6739 padding: 14px; border: 1.5px solid var(--mm2-border); border-radius: var(--mm2-radius-md); 6740 background: var(--mm2-surface); color: var(--mm2-fg); text-decoration: none; 6741 min-height: 52px; 6742 font-weight: 500; 6743 font-size: 14px; 6744 transition: all var(--mm2-transition-fast); 6745 box-shadow: var(--mm2-shadow-sm); 6746 position: relative; 6747 justify-content: space-between; 6748} 6749.mm2-link:hover { 6750 background: var(--mm2-muted); 6751 border-color: var(--mm2-accent); 6752 transform: translateX(4px); 6753 box-shadow: var(--mm2-shadow-md); 6754} 6755.mm2-link-text { 6756 flex: 1; 6757} 6758 6759/* Product Thumbnails */ 6760.mm2-thumbs { 6761 display: flex; 6762 gap: 6px; 6763 align-items: center; 6764 margin-left: auto; 6765 flex-shrink: 0; 6766} 6767.mm2-thumb { 6768 width: 36px; 6769 height: 36px; 6770 border-radius: 8px; 6771 object-fit: cover; 6772 border: 1.5px solid var(--mm2-border); 6773 box-shadow: 0 2px 6px rgba(0,0,0,0.08); 6774 transition: all var(--mm2-transition-fast); 6775 background: var(--mm2-muted); 6776} 6777.mm2-link:hover .mm2-thumb { 6778 border-color: var(--mm2-accent); 6779 box-shadow: 0 4px 10px rgba(99,102,241,0.15); 6780 transform: scale(1.08); 6781} 6782
6783/* Thumbnail Skeleton Loader */ 6784.mm2-thumb-skeleton { 6785 width: 36px; 6786 height: 36px; 6787 border-radius: 8px; 6788 background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); 6789 background-size: 200% 100%; 6790 animation: shimmer 1.5s infinite; 6791 border: 1.5px solid var(--mm2-border); 6792} 6793 6794.mm2-badge { 6795 font-size: 11px; padding: 3px 10px; margin-left: auto; 6796 border-radius: 12px; background: var(--mm2-accent-10); color: var(--mm2-accent); 6797 font-weight: 700; 6798 border: 1px solid var(--mm2-accent-20); 6799} 6800 6801/* "See all" highlight card - Enhanced */ 6802.mm2-seeall { 6803 border: 2px dashed var(--mm2-accent); 6804 background: linear-gradient(135deg, rgba(99,102,241,.08), rgba(99,102,241,.02)); 6805 font-weight: 700; 6806} 6807.mm2-seeall:hover { 6808 background: linear-gradient(135deg, rgba(99,102,241,.14), rgba(99,102,241,.06)); 6809 border-style: solid; 6810 transform: translateX(6px) scale(1.02); 6811} 6812 6813/* Mobile panel styles - Enhanced with drag handle & better touch targets */ 6814@media (max-width: 960px) { 6815 .mm2-panel { 6816 position: fixed; 6817 left: 0; 6818 right: 0; 6819 bottom: 0; 6820 top: auto; 6821 width: 100%; 6822 max-height: 88vh; 6823 transform: translateX(0) translateY(100%); 6824 border-radius: var(--mm2-radius-xl) var(--mm2-radius-xl) 0 0; 6825 border-bottom: none; 6826 } 6827 .mm2-open .mm2-panel { 6828 transform: translateX(0) translateY(0); z-index: 9999; 6829 } 6830 6831 /* Enhanced backdrop for mobile */ 6832 .mm2-backdrop { 6833 backdrop-filter: blur(6px) saturate(1.2); 6834 -webkit-backdrop-filter: blur(6px) saturate(1.2); 6835 } 6836 6837 /* Mobile close button - Soft glassmorphism */ 6838 .mm2-mobile-close { 6839 position: absolute; 6840 top: 4px; 6841 right: 8px; 6842 width: 28px; 6843 height: 28px; 6844 border-radius: 50%; 6845 border: 1px solid rgba(226, 232, 240, 0.6); 6846 background: rgba(255, 255, 255, 0.85); 6847 backdrop-filter: blur(12px) saturate(1.8); 6848 -webkit-backdrop-filter: blur(12px) saturate(1.8); 6849 color: var(--mm2-fg-muted); 6850 display: inline-flex;
6851 align-items: center; 6852 justify-content: center; 6853 cursor: pointer; 6854 transition: all var(--mm2-transition-fast); 6855 box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08); 6856 z-index: 20; 6857 } 6858 .mm2-mobile-close:hover { 6859 background: rgba(254, 242, 242, 0.95); 6860 border-color: rgba(226,166,66,0.8); 6861 color: #e2a642; 6862 box-shadow: 0 4px 12px rgba(226,166,66,0.15); 6863 } 6864 .mm2-mobile-close:active { 6865 transform: scale(0.92); 6866 } 6867 6868 /* Drag handle indicator */ 6869 .mm2-top::before, 6870 .mm2-detail::before { 6871 content: ''; 6872 position: sticky; 6873 top: 0; 6874 left: 50%; 6875 transform: translateX(-50%); 6876 width: 40px; 6877 height: 4px; 6878 background: var(--mm2-border-strong); 6879 border-radius: 999px; 6880 margin: 12px auto 20px; 6881 display: block; 6882 z-index: 10; 6883 transition: background var(--mm2-transition-fast); 6884 } 6885 .mm2-panel:active .mm2-top::before, 6886 .mm2-panel:active .mm2-detail::before { 6887 background: #94a3b8; 6888 } 6889 6890 .mm2-top { 6891 padding: 10px; 6892 } 6893 6894 .mm2-top, .mm2-detail { 6895 max-height: min(82vh, 720px); 6896 padding-top: 8px; 6897 } 6898 6899 /* Two-pane collapses: subnav becomes GRID (no horizontal scroll) */ 6900 .mm2-detail-layout { display: block; } 6901 .mm2-subnav { 6902 position: static; max-height: none; overflow: visible; 6903 display: grid; gap: 10px; padding: 10px 4px 16px; margin-bottom: 16px; 6904 grid-template-columns: repeat(2, minmax(0,1fr)); 6905 border-bottom: 2px solid var(--mm2-muted); 6906 } 6907 .mm2-subitem { 6908 margin: 0; width: 100%; text-align: center; 6909 border-radius: 14px; 6910 background: #fff; border: 1.5px solid var(--mm2-border); 6911 white-space: normal; line-height: 1.3; min-height: 48px; 6912 justify-content: center; 6913 padding: 12px 10px; 6914 font-size: 13.5px; 6915 box-shadow: var(--mm2-shadow-sm); 6916 } 6917 .mm2-subitem::before { display: none; } 6918 .mm2-subitem:hover { 6919 transform: scale(1.02); 6920 } 6921 .mm2-subitem.active { 6922 background: linear-gradient(135deg, var(--mm2-accent), var(--mm2-accent-hover)); 6923 border-color: var(--mm2-accent-hover); 6924 color: white; 6925 box-shadow: 0 4px 12px rgba(99,102,241,0.3); 6926 font-weight: 700; 6927 } 6928 6929 /* Level-3 grid tetap 2 kolom dengan spacing lebih baik */ 6930 .mm2-links { 6931 grid-template-columns: repeat(2, minmax(0,1fr)); 6932 gap: 10px; 6933 } 6934 6935 /* Mobile: hide thumbnails untuk save space */ 6936 .mm2-thumbs { 6937 display: none; 6938 } 6939 6940 /* Larger touch targets for mobile */ 6941 .mm2-card { 6942 min-height: 80px; 6943 padding: 18px 14px; 6944 } 6945 .mm2-card .mm2-ico { 6946 width: 50px; 6947 height: 50px; 6948 } 6949 .mm2-card .mm2-title { 6950 font-size: 14px; 6951 } 6952 .mm2-card .mm2-sub { 6953 font-size: 11.5px; 6954 } 6955 .mm2-link { 6956 min-height: 56px; 6957 padding: 16px; 6958 } 6959 .mm2-back { 6960 width: 44px; 6961 height: 44px; 6962 } 6963 6964 /* Grid spacing */ 6965 .mm2-grid { 6966 gap: 10px; 6967 } 6968} 6969</style> 6970 <style data-inlined="product-compare.css"> 6971/* Product Compare Feature Styles */ 6972 6973/* Compare Button in Product Detail - Modern Chip Style */ 6974.pd-compare-section { 6975 margin: 0.5rem 0; 6976 display: flex; 6977 align-items: center; 6978 gap: 0.5rem; 6979} 6980 6981.pd-compare-btn { 6982 display: inline-flex; 6983 align-items: center; 6984 gap: 0.4rem; 6985 padding: 0.5rem 0.85rem; 6986 border: 1px solid #cbd5e1; 6987 border-radius: 20px; 6988 background: #fff; 6989 color: #64748b; 6990 font-weight: 600; 6991 font-size: 0.8rem; 6992 cursor: pointer; 6993 transition: all 0.2s ease; 6994 position: relative; 6995 white-space: nowrap; 6996} 6997 6998.pd-compare-btn:hover { 6999 border-color: #3b82f6; 7000 color: #3b82f6; 7001 background: #f0f9ff; 7002 transform: translateY(-1px); 7003 box-shadow: 0 2px 6px rgba(59, 130, 246, 0.15); 7004} 7005 7006.pd-compare-btn.added { 7007 border-color: #16a34a; 7008 color: #16a34a; 7009 background: #f0fdf4; 7010} 7011 7012.pd-compare-btn .compare-remove { 7013 display: none; 7014} 7015 7016.pd-compare-btn.added .compare-text { 7017 display: none; 7018} 7019 7020.pd-compare-btn.added .compare-remove { 7021 display: flex !important; 7022 align-items: center; 7023 gap: 0.25rem; 7024 font-weight: 600; 7025 font-size: 0.9rem; 7026 color: #e2a642 !important; 7027 opacity: 1 !important; 7028 visibility: visible !important; 7029 z-index: 1; 7030 position: relative; 7031} 7032 7033.pd-compare-btn.added .compare-remove[hidden] { 7034 display: flex !important; 7035} 7036 7037/* Force visibility for added state */ 7038.pd-compare-btn.added .compare-remove { 7039 display: flex !important; 7040} 7041 7042[hidden].compare-remove { 7043 display: none !important; 7044} 7045 7046.pd-compare-btn.added [hidden].compare-remove { 7047 display: flex !important; 7048} 7049 7050/* Ensure the added state text is always visible */ 7051.pd-compare-btn.added { 7052 border-color: #e2a642 !important; 7053 color: #e2a642 !important; 7054 background: #fef2f2 !important; 7055} 7056 7057.pd-compare-btn.added .compare-remove svg { 7058 color: #e2a642 !important; 7059} 7060 7061.pd-compare-btn.added:hover { 7062 border-color: #15803d; 7063 color: #15803d; 7064 background: #dcfce7; 7065 transform: translateY(-1px); 7066 box-shadow: 0 2px 6px rgba(22, 163, 74, 0.2); 7067} 7068 7069.pd-compare-btn .compare-icon { 7070 width: 16px; 7071 height: 16px; 7072 flex-shrink: 0; 7073} 7074 7075.pd-compare-info { 7076 display: inline-flex; 7077 align-items: center; 7078} 7079 7080.compare-note { 7081 color: #94a3b8; 7082 font-size: 0.7rem; 7083 font-weight: 500; 7084 transition: color 0.2s ease; 7085} 7086 7087.pd-compare-btn.added + .pd-compare-info .compare-note { 7088 color: #16a34a; 7089 font-weight: 600; 7090} 7091 7092/* Compare Badge in Navigation */ 7093.compare-indicator { 7094 position: relative; 7095} 7096 7097.compare-indicator .badge { 7098 position: absolute; 7099 top: -8px; 7100 right: -8px; 7101 min-width: 18px;
7102 height: 18px; 7103 padding: 0 5px; 7104 background: #e2a642; 7105 color: white; 7106 border-radius: 9px; 7107 font-size: 11px; 7108 7109 7110/* Floating Compare Badge (Sticky) */ 7111.compare-floating-badge { 7112 position: fixed; 7113 right: 16px; 7114 bottom: calc(20px + 60px); /* Above scroll-to-top button */ 7115 width: auto; 7116 min-width: 56px; 7117 height: 56px; 7118 padding: 0 16px; 7119 border-radius: 28px; 7120 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); 7121 color: #fff; 7122 box-shadow: 0 8px 24px -8px rgba(59, 130, 246, 0.5); 7123 cursor: pointer; 7124 z-index: 1104; 7125 opacity: 0; 7126 transform: translateY(8px) scale(0.9); 7127 transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.2s ease; 7128 display: flex; 7129 align-items: center; 7130 justify-content: center; 7131 gap: 8px; 7132 font-weight: 600; 7133 font-size: 0.85rem; 7134 pointer-events: none; 7135} 7136 7137.compare-floating-badge.visible { 7138 opacity: 1; 7139 transform: translateY(0) scale(1); 7140 pointer-events: auto; 7141} 7142 7143.compare-floating-badge:hover { 7144 box-shadow: 0 12px 32px -10px rgba(59, 130, 246, 0.6); 7145 transform: translateY(-2px) scale(1.02); 7146} 7147 7148.compare-floating-badge:active { 7149 transform: translateY(0) scale(0.98); 7150} 7151 7152.compare-floating-badge .cfb-icon { 7153 width: 20px; 7154 height: 20px; 7155 flex-shrink: 0; 7156} 7157 7158.compare-floating-badge .cfb-count { 7159 font-size: 0.9rem; 7160 font-weight: 700; 7161 min-width: 20px; 7162 text-align: center; 7163 background: rgba(255, 255, 255, 0.25); 7164 padding: 2px 8px; 7165 border-radius: 12px; 7166 line-height: 1; 7167 display: flex; 7168 align-items: center; 7169 justify-content: center; 7170 height: 24px; 7171} 7172 7173.compare-floating-badge .cfb-text { 7174 display: none; 7175} 7176 7177@media (min-width: 641px) { 7178 .compare-floating-badge .cfb-text { 7179 display: block; 7180 white-space: nowrap; 7181 } 7182} 7183 7184@media (max-width: 640px) { 7185 .compare-floating-badge { 7186 right: 14px; 7187 bottom: calc(var(--scroll-top-bottom, calc(68px + 18px + env(safe-area-inset-bottom))) + 60px); 7188 min-width: 48px; 7189 height: 48px; 7190 padding: 0 12px; 7191 border-radius: 24px; 7192 } 7193 7194 .compare-floating-badge .cfb-icon { 7195 width: 18px; 7196 height: 18px; 7197 } 7198 7199 .compare-floating-badge .cfb-count { 7200 font-size: 0.8rem; 7201 padding: 2px 6px; 7202 height: 20px; 7203 } 7204} font-weight: 600; 7205 display: flex; 7206 align-items: center; 7207 justify-content: center; 7208 transform: scale(0); 7209 transition: transform 0.2s ease; 7210} 7211 7212.compare-indicator .badge.active { 7213 transform: scale(1); 7214} 7215 7216/* Compare Sidebar */ 7217.compare-sidebar { 7218 position: fixed; 7219 top: 0; 7220 right: -400px; 7221 width: 400px; 7222 height: 100vh; 7223 background: #ffffff; 7224 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); 7225 z-index: 9999; 7226 display: flex; 7227 flex-direction: column; 7228 transition: right 0.3s ease; 7229} 7230 7231.compare-sidebar.open { 7232 right: 0; 7233} 7234 7235.compare-overlay { 7236 position: fixed; 7237 top: 0; 7238 left: 0; 7239 width: 100%; 7240 height: 100%; 7241 background: rgba(0, 0, 0, 0.5); 7242 z-index: 9998; 7243 opacity: 0; 7244 visibility: hidden; 7245 transition: all 0.3s ease; 7246} 7247 7248.compare-overlay.active { 7249 opacity: 1; 7250 visibility: visible; 7251} 7252 7253.compare-header { 7254 display: flex; 7255 align-items: center; 7256 justify-content: space-between; 7257 padding: 1.25rem; 7258 border-bottom: 1px solid #e2e8f0; 7259 background: #f8fafc; 7260} 7261 7262.compare-title { 7263 display: flex; 7264 align-items: center; 7265 gap: 0.5rem; 7266 font-size: 1rem; 7267 font-weight: 600; 7268 color: #1e293b; 7269 margin: 0; 7270} 7271 7272.compare-count { 7273 color: #64748b; 7274 font-weight: 400; 7275} 7276 7277.compare-close { 7278 display: flex; 7279 align-items: center; 7280 justify-content: center; 7281 width: 32px; 7282 height: 32px; 7283 border: none; 7284 border-radius: 6px; 7285 background: transparent; 7286 color: #64748b; 7287 cursor: pointer; 7288 transition: all 0.2s ease; 7289} 7290 7291.compare-close:hover { 7292 background: #e2e8f0; 7293 color: #374151; 7294} 7295 7296.compare-content { 7297 flex: 1; 7298 overflow-y: auto; 7299 padding: 1rem; 7300} 7301 7302.compare-empty { 7303 display: flex; 7304 flex-direction: column; 7305 align-items: center; 7306 justify-content: center; 7307 height: 100%; 7308 text-align: center; 7309 padding: 2rem 1rem; 7310} 7311 7312.compare-empty h5 { 7313 margin: 1rem 0 0.5rem; 7314 color: #374151; 7315} 7316 7317.compare-empty p { 7318 color: #64748b; 7319 font-size: 0.875rem; 7320 line-height: 1.5; 7321} 7322 7323.compare-list { 7324 display: flex; 7325 flex-direction: column; 7326 gap: 0.75rem; 7327} 7328 7329.compare-item { 7330 display: flex; 7331 align-items: center; 7332 gap: 0.75rem; 7333 padding: 0.75rem; 7334 border: 1px solid #e2e8f0; 7335 border-radius: 8px; 7336 background: #ffffff; 7337 transition: all 0.2s ease; 7338} 7339 7340.compare-item:hover { 7341 border-color: #cbd5e1; 7342 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 7343} 7344 7345.compare-item-image { 7346 flex-shrink: 0; 7347 width: 48px; 7348 height: 48px; 7349 border-radius: 6px; 7350 overflow: hidden; 7351 border: 1px solid #e2e8f0; 7352} 7353 7354.compare-item-image img { 7355 width: 100%; 7356 height: 100%; 7357 object-fit: cover; 7358} 7359 7360.compare-item-info { 7361 flex: 1; 7362 min-width: 0; 7363} 7364 7365.compare-item-name { 7366 font-weight: 500; 7367 font-size: 0.875rem; 7368 color: #1e293b; 7369 margin-bottom: 0.25rem; 7370 line-height: 1.3; 7371 display: -webkit-box; 7372 -webkit-line-clamp: 2; 7373 -webkit-box-orient: vertical; 7374 overflow: hidden; 7375} 7376 7377.compare-item-price { 7378 font-weight: 600; 7379 font-size: 0.8rem; 7380 color: #059669; 7381} 7382 7383.compare-item-remove { 7384 flex-shrink: 0; 7385 display: flex; 7386 align-items: center; 7387 justify-content: center; 7388 width: 28px; 7389 height: 28px; 7390 border: none; 7391 border-radius: 4px; 7392 background: transparent; 7393 color: #e2a642; 7394 cursor: pointer; 7395 transition: all 0.2s ease; 7396} 7397 7398.compare-item-remove:hover { 7399 background: #fef2f2; 7400 color: #a36422; 7401} 7402 7403.compare-actions { 7404 padding: 1rem; 7405 border-top: 1px solid #e2e8f0; 7406 display: flex; 7407 gap: 0.75rem; 7408} 7409 7410.compare-actions .btn { 7411 flex: 1; 7412 padding: 0.75rem 1rem; 7413 border-radius: 6px; 7414 font-weight: 600; 7415 font-size: 0.875rem; 7416 display: flex; 7417 align-items: center; 7418 justify-content: center; 7419 gap: 0.5rem; 7420 cursor: pointer; 7421 transition: all 0.2s ease; 7422} 7423 7424.btn-outline-danger { 7425 border: 1px solid #e2a642; 7426 background: transparent; 7427 color: #e2a642; 7428} 7429 7430.btn-outline-danger:hover { 7431 background: #e2a642; 7432 color: white; 7433} 7434 7435.btn-primary { 7436 border: 1px solid #3b82f6; 7437 background: #3b82f6; 7438 color: white; 7439} 7440 7441.btn-primary:hover { 7442 background: #2563eb; 7443 border-color: #2563eb; 7444} 7445 7446.btn-primary:disabled { 7447 opacity: 0.5;
7448 cursor: not-allowed; 7449} 7450 7451.btn-primary:disabled:hover { 7452 background: #3b82f6; 7453 border-color: #3b82f6; 7454} 7455 7456/* AI Comparison Modal */ 7457.ai-compare-modal { 7458 position: fixed; 7459 top: 0; 7460 left: 0; 7461 width: 100%; 7462 height: 100%; 7463 z-index: 10000; 7464 display: none; 7465 align-items: center; 7466 justify-content: center; 7467} 7468 7469.ai-compare-modal.open { 7470 display: flex; 7471} 7472 7473.ai-compare-modal .modal-backdrop { 7474 position: absolute; 7475 top: 0; 7476 left: 0; 7477 width: 100%; 7478 height: 100%; 7479 background: rgba(0, 0, 0, 0.6); 7480 backdrop-filter: blur(4px); 7481} 7482 7483.modal-container { 7484 position: relative; 7485 width: 90%; 7486 max-width: 1000px; 7487 max-height: 90vh; 7488 background: #ffffff; 7489 border-radius: 12px; 7490 box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); 7491 display: flex; 7492 flex-direction: column; 7493 overflow: hidden; 7494} 7495 7496.modal-header { 7497 display: flex; 7498 align-items: center; 7499 justify-content: space-between; 7500 padding: 1.5rem 2rem; 7501 border-bottom: 1px solid #e2e8f0; 7502 background: #f8fafc; 7503} 7504 7505.modal-title { 7506 display: flex; 7507 align-items: center; 7508 gap: 0.75rem; 7509 font-size: 1.25rem; 7510 font-weight: 600; 7511 color: #1e293b; 7512 margin: 0; 7513} 7514 7515.modal-close { 7516 display: flex; 7517 align-items: center; 7518 justify-content: center; 7519 width: 36px; 7520 height: 36px; 7521 border: none; 7522 border-radius: 8px; 7523 background: transparent; 7524 color: #64748b; 7525 cursor: pointer; 7526 transition: all 0.2s ease; 7527} 7528 7529.modal-close:hover { 7530 background: #e2e8f0; 7531 color: #374151; 7532} 7533 7534.modal-content { 7535 flex: 1; 7536 overflow-y: auto; 7537 padding: 2rem; 7538} 7539 7540/* AI Loading State */ 7541.ai-loading { 7542 display: flex; 7543 flex-direction: column; 7544 align-items: center; 7545 justify-content: center; 7546 padding: 3rem 2rem; 7547 text-align: center; 7548} 7549 7550.loading-animation { 7551 margin-bottom: 1rem; 7552} 7553 7554.typing-indicator { 7555 display: flex; 7556 align-items: center; 7557 justify-content: center; 7558 gap: 4px; 7559 margin-bottom: 1rem; 7560} 7561 7562.typing-indicator span { 7563 width: 8px; 7564 height: 8px; 7565 background: #3b82f6; 7566 border-radius: 50%; 7567 animation: typing 1.4s infinite ease-in-out; 7568} 7569 7570.typing-indicator span:nth-child(2) { 7571 animation-delay: 0.2s; 7572} 7573 7574.typing-indicator span:nth-child(3) { 7575 animation-delay: 0.4s; 7576} 7577 7578@keyframes typing { 7579 0%, 80%, 100% { 7580 opacity: 0.3; 7581 transform: scale(0.8); 7582 } 7583 40% { 7584 opacity: 1; 7585 transform: scale(1); 7586 } 7587} 7588 7589.ai-loading p { 7590 font-weight: 500; 7591 color: #1e293b; 7592 margin-bottom: 0.5rem; 7593} 7594 7595.ai-loading small { 7596 color: #64748b; 7597} 7598 7599/* AI Error State */ 7600.ai-error { 7601 display: flex; 7602 flex-direction: column; 7603 align-items: center; 7604 justify-content: center; 7605 padding: 3rem 2rem; 7606 text-align: center; 7607} 7608 7609.ai-error h4 { 7610 margin: 1rem 0 0.5rem; 7611 color: #e2a642; 7612} 7613 7614.ai-error p { 7615 color: #64748b; 7616 margin-bottom: 1.5rem; 7617} 7618 7619/* AI Results */ 7620.ai-results > div { 7621 margin-bottom: 2rem; 7622} 7623 7624.ai-summary-card, 7625.ai-comparison-table, 7626.ai-detailed-analysis, 7627.ai-recommendations, 7628.ai-tips { 7629 background: #f8fafc; 7630 border: 1px solid #e2e8f0; 7631 border-radius: 12px; 7632 padding: 1.5rem; 7633} 7634 7635.ai-results h4 { 7636 display: flex; 7637 align-items: center; 7638 gap: 0.5rem; 7639 font-size: 1.1rem; 7640 font-weight: 600; 7641 color: #1e293b; 7642 margin-bottom: 1rem; 7643} 7644 7645.summary-text { 7646 font-size: 1rem; 7647 line-height: 1.6; 7648 color: #374151; 7649 margin-bottom: 1rem; 7650} 7651 7652.winner-badge { 7653 padding: 1rem; 7654 background: linear-gradient(135deg, #10b981 0%, #059669 100%); 7655 color: white; 7656 border-radius: 8px; 7657} 7658 7659.winner-badge strong { 7660 display: block; 7661 margin-bottom: 0.25rem; 7662} 7663 7664.winner-badge small { 7665 opacity: 0.9; 7666 font-size: 0.875rem; 7667} 7668 7669/* Comparison Table */ 7670.comparison-grid { 7671 display: grid; 7672 gap: 1rem; 7673 margin-top: 1rem; 7674} 7675 7676.comparison-category { 7677 background: white; 7678 border: 1px solid #e2e8f0; 7679 border-radius: 8px; 7680 overflow: hidden; 7681} 7682 7683.category-header { 7684 background: #f1f5f9; 7685 padding: 0.75rem 1rem; 7686 font-weight: 600; 7687 color: #1e293b; 7688 border-bottom: 1px solid #e2e8f0; 7689} 7690 7691.category-products { 7692 display: grid; 7693 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 7694 gap: 1px; 7695 background: #e2e8f0; 7696} 7697 7698.product-score { 7699 background: white; 7700 padding: 1rem; 7701 text-align: center; 7702} 7703 7704.score-value { 7705 display: inline-flex; 7706 align-items: center; 7707 gap: 0.25rem; 7708 margin-bottom: 0.5rem; 7709} 7710 7711.score-stars { 7712 color: #C10DFF; 7713} 7714 7715.score-note { 7716 font-size: 0.8rem; 7717 color: #64748b; 7718 line-height: 1.3; 7719} 7720 7721/* Product name header in comparison table */ 7722.product-name-header { 7723 margin-bottom: 0.5rem; 7724} 7725 7726.product-name-header strong { 7727 font-size: .8rem; /* h5 size (18px) */ 7728 font-weight: 600; 7729 color: #1e293b; 7730 line-height: 1.3; 7731 display: block; 7732 word-break: break-word; 7733} 7734 7735.product-price { 7736 font-size: 0.8rem; 7737 font-weight: 600; 7738 color: #059669; 7739} 7740 7741/* Detailed Analysis */ 7742.products-analysis { 7743 display: grid; 7744 gap: 1rem; 7745 margin-top: 1rem; 7746} 7747 7748.product-analysis { 7749 background: white; 7750 border: 1px solid #e2e8f0; 7751 border-radius: 8px; 7752 padding: 1rem; 7753} 7754 7755.product-analysis h5 { 7756 color: #1e293b; 7757 margin-bottom: 0.75rem; 7758 font-weight: 600; 7759} 7760 7761.analysis-section { 7762 margin-bottom: 0.75rem; 7763} 7764 7765.analysis-section:last-child { 7766 margin-bottom: 0; 7767} 7768 7769.analysis-section h6 { 7770 font-size: 0.875rem; 7771 font-weight: 600; 7772 margin-bottom: 0.5rem; 7773} 7774 7775.analysis-section h6.strengths { 7776 color: #059669; 7777} 7778 7779.analysis-section h6.weaknesses { 7780 color: #e2a642; 7781} 7782 7783.analysis-section h6.best-for { 7784 color: #7c3aed; 7785} 7786 7787.analysis-section ul { 7788 list-style: none; 7789 padding: 0; 7790 margin: 0; 7791} 7792 7793.analysis-section li { 7794 display: flex; 7795 align-items: flex-start; 7796 gap: 0.5rem; 7797 margin-bottom: 0.25rem; 7798 font-size: 0.875rem; 7799 line-height: 1.4; 7800} 7801 7802.analysis-section li:before { 7803 content: "â¢"; 7804 color: #64748b; 7805 font-weight: bold; 7806 margin-top: 0.1rem; 7807} 7808 7809.best-for-text { 7810 font-size: 0.875rem; 7811 color: #374151; 7812 font-style: italic; 7813} 7814 7815/* Recommendations */ 7816.recommendations-grid { 7817 display: grid; 7818 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 7819 gap: 1rem; 7820 margin-top: 1rem; 7821} 7822 7823.recommendation-card { 7824 background: white; 7825 border: 1px solid #e2e8f0; 7826 border-radius: 8px; 7827 padding: 1rem; 7828 text-align: center; 7829} 7830 7831.recommendation-card h5 { 7832 font-size: 1rem; 7833 margin-bottom: 0.75rem; 7834 color: #1e293b; 7835} 7836 7837.recommendation-card p { 7838 font-size: 0.875rem; 7839 color: #374151; 7840 line-height: 1.5; 7841} 7842 7843/* Tips */ 7844.tips-list { 7845 list-style: none; 7846 padding: 0; 7847 margin: 1rem 0 0 0; 7848} 7849 7850.tips-list li { 7851 display: flex; 7852 align-items: flex-start; 7853 gap: 0.75rem; 7854 margin-bottom: 0.75rem; 7855 font-size: 0.9rem; 7856 line-height: 1.5; 7857 color: #374151; 7858} 7859 7860.tips-list li:before { 7861 content: "ð¡"; 7862 font-size: 1rem; 7863 margin-top: 0.1rem; 7864} 7865 7866/* Modal Footer */ 7867.modal-footer { 7868 display: flex; 7869 justify-content: space-between; 7870 align-items: center; 7871 padding: 1.5rem 2rem; 7872 border-top: 1px solid #e2e8f0; 7873 background: #f8fafc; 7874} 7875 7876.btn-outline-secondary { 7877 border: 1px solid #64748b; 7878 background: transparent; 7879 color: #64748b; 7880 padding: 0.75rem 1.5rem; 7881 border-radius: 6px; 7882 font-weight: 600; 7883 display: flex; 7884 align-items: center; 7885 gap: 0.5rem; 7886 cursor: pointer; 7887 transition: all 0.2s ease; 7888} 7889 7890.btn-outline-secondary:hover { 7891 background: #64748b; 7892 color: white; 7893} 7894 7895/* Responsive for chip button */ 7896@media (max-width: 880px) { 7897 .pd-compare-section { 7898 margin: 1rem 0; 7899 display: block; 7900 } 7901 7902 .pd-compare-btn { 7903 width: 100%; 7904 font-size: 0.85rem; 7905 padding: 0.65rem 1rem; 7906 border-radius: 10px; 7907 justify-content: center; 7908 min-height: 44px;
7909 } 7910 7911 .pd-compare-btn .compare-icon { 7912 width: 16px; 7913 height: 16px; 7914 } 7915 7916 .pd-compare-info { 7917 margin-top: 0.5rem; 7918 display: block; 7919 text-align: center; 7920 } 7921 7922 .compare-note { 7923 font-size: 0.7rem; 7924 display: block; 7925 } 7926} 7927 7928/* Responsive Design */ 7929@media (max-width: 768px) { 7930 .compare-sidebar { 7931 width: 100%; 7932 right: -100%; 7933 /* Fix for mobile viewport height changes */ 7934 height: 100vh; 7935 height: 100dvh; /* Dynamic viewport height for modern browsers */ 7936 } 7937 7938 .compare-actions { 7939 /* Ensure actions are always visible */ 7940 position: sticky; 7941 bottom: 0; 7942 background: white; 7943 border-top: 2px solid #e2e8f0; 7944 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); 7945 z-index: 10; 7946 } 7947 7948 .compare-actions .btn { 7949 /* Larger touch targets for mobile */ 7950 min-height: 48px; 7951 font-size: 0.9rem; 7952 font-weight: 700; 7953 } 7954 7955 .modal-container { 7956 width: 95%; 7957 max-height: 95vh; 7958 max-height: 95dvh; /* Dynamic viewport height */ 7959 } 7960 7961 .modal-header, 7962 .modal-content, 7963 .modal-footer { 7964 padding: 1rem; 7965 } 7966 7967 .recommendations-grid { 7968 grid-template-columns: 1fr; 7969 } 7970 7971 .category-products { 7972 grid-template-columns: 1fr; 7973 } 7974 7975 .modal-footer { 7976 flex-direction: column; 7977 gap: 0.75rem; 7978 } 7979 7980 .modal-footer .btn { 7981 width: 100%; 7982 } 7983 7984 /* Ensure compare button is visible on mobile */ 7985 .pd-compare-section { 7986 display: block !important; 7987 visibility: visible !important; 7988 opacity: 1 !important; 7989 margin: 1rem 0; 7990 padding: 0; 7991 position: relative !important; 7992 z-index: 1 !important; 7993 max-width: 100%; 7994 overflow: hidden; 7995 } 7996 7997 .pd-compare-btn { 7998 display: flex !important; 7999 visibility: visible !important; 8000 opacity: 1 !important; 8001 width: 100%; 8002 padding: 0.75rem 1rem; 8003 font-size: 0.9rem; 8004 min-height: 48px; /* Minimum touch target for mobile */ 8005 border: 2px solid #e2e8f0 !important; 8006 background: #fff !important; 8007 position: relative !important; 8008 z-index: 2 !important; 8009 box-sizing: border-box; 8010 white-space: normal !important; /* Allow text wrapping on mobile */ 8011 flex-wrap: wrap; 8012 gap: 0.5rem; 8013 } 8014 8015 .pd-compare-btn .compare-text { 8016 display: inline !important; 8017 visibility: visible !important; 8018 font-weight: 600; 8019 color: #475569 !important; 8020 } 8021 8022 .pd-compare-btn .compare-remove { 8023 display: none !important; /* Hidden by default */ 8024 } 8025 8026 .pd-compare-btn.added .compare-remove { 8027 display: flex !important; /* Only show when added */ 8028 flex-wrap: wrap; 8029 align-items: center; 8030 gap: 0.4rem; 8031 white-space: normal !important; 8032 word-break: break-word; 8033 } 8034 8035 .pd-compare-btn .compare-icon { 8036 display: inline-block !important; 8037 width: 18px; 8038 height: 18px; 8039 flex-shrink: 0; 8040 } 8041 8042 .pd-compare-info { 8043 margin-top: 0.5rem; 8044 } 8045 8046 .compare-note { 8047 font-size: 0.85rem !important; 8048 display: block !important; 8049 visibility: visible !important; 8050 color: #64748b !important; 8051 } 8052} 8053 8054/* iOS Safari specific fixes */ 8055@media (max-width: 480px) { 8056 #compareBtn { 8057 width:46px; 8058 height:46px; 8059 } 8060 8061 .pd-compare-section { 8062 -webkit-transform: translateZ(0); 8063 transform: translateZ(0); 8064 } 8065 8066 .pd-compare-btn { 8067 -webkit-appearance: none !important; 8068 appearance: none !important; 8069 -webkit-tap-highlight-color: transparent; 8070 touch-action: manipulation; 8071 } 8072 8073 /* Fix sidebar height for iOS address bar */ 8074 .compare-sidebar { 8075 /* Use fixed positioning that accounts for iOS address bar */ 8076 height: 100vh; 8077 height: -webkit-fill-available; /* iOS Safari compatibility */ 8078 min-height: 100vh; 8079 min-height: -webkit-fill-available; 8080 } 8081 8082 /* Ensure compare actions stay at bottom */ 8083 .compare-actions { 8084 position: sticky; 8085 bottom: 0; 8086 background: white; 8087 border-top: 2px solid #e2e8f0; 8088 box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); 8089 padding: 1.25rem 1rem; 8090 z-index: 20; 8091 } 8092 8093 .compare-actions .btn { 8094 min-height: 52px; /* Larger for easier tapping */ 8095 font-size: 1rem; 8096 font-weight: 700; 8097 border-radius: 8px; 8098 } 8099 8100 /* Adjust content padding to account for sticky actions */ 8101 .compare-content { 8102 padding-bottom: 6rem; /* Extra space for sticky actions */ 8103 } 8104} 8105 8106/* High-resolution mobile devices (iPhone 12 Pro, etc.) */ 8107@media (-webkit-min-device-pixel-ratio: 2) and (max-width: 430px) { 8108 .pd-compare-section { 8109 display: block !important; 8110 } 8111 8112 .pd-compare-btn { 8113 display: flex !important; 8114 min-height: 44px;
8115 font-size: 16px; /* Prevent zoom on iOS */ 8116 } 8117 8118 /* Enhanced fixes for iPhone 12 Pro and similar devices */ 8119 .compare-sidebar { 8120 /* Ensure full height coverage */ 8121 height: 100vh; 8122 height: 100dvh; /* Dynamic viewport height */ 8123 height: -webkit-fill-available; /* iOS fallback */ 8124 max-height: 100vh; 8125 max-height: 100dvh; 8126 max-height: -webkit-fill-available; 8127 overflow: hidden; /* Prevent scroll issues */ 8128 } 8129 8130 .compare-content { 8131 /* Flexible content area */ 8132 flex: 1; 8133 overflow-y: auto; 8134 -webkit-overflow-scrolling: touch; 8135 padding: 1rem 1rem 8rem 1rem; /* Extra bottom padding */ 8136 } 8137 8138 .compare-actions { 8139 /* Force actions to stay visible */ 8140 position: fixed; 8141 bottom: 0; 8142 left: 0; 8143 right: 0; 8144 width: 100%; 8145 background: white; 8146 border-top: 2px solid #e2e8f0; 8147 box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); 8148 padding: 1rem; 8149 z-index: 9999; 8150 display: flex !important; 8151 gap: 0.75rem; 8152 } 8153 8154 .compare-actions .btn { 8155 flex: 1; 8156 min-height: 30px; /* Extra large for high-DPI screens */ 8157 font-size: .8rem; 8158 font-weight: 600; 8159 border-radius: 12px; 8160 display: flex !important; 8161 align-items: center; 8162 justify-content: center; 8163 gap: 0.5rem; 8164 } 8165 8166 /* Ensure AI analyze button is always visible and prominent */ 8167 #compareAnalyze { 8168 background: #3b82f6 !important; 8169 border-color: #3b82f6 !important; 8170 color: white !important; 8171 box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); 8172 } 8173 8174 #compareAnalyze:not(:disabled) { 8175 background: #2563eb !important; 8176 border-color: #2563eb !important; 8177 transform: scale(1.02); 8178 } 8179 8180 #compareAnalyze:disabled { 8181 opacity: 0.6 !important; 8182 background: #9ca3af !important; 8183 border-color: #9ca3af !important; 8184 } 8185} 8186 8187/* AI Disclaimer Styles */ 8188.ai-disclaimer { 8189 display: flex; 8190 align-items: flex-start; 8191 gap: 0.75rem; 8192 padding: 1rem; 8193 background: #fef3c7; 8194 border: 1px solid #C10DFF; 8195 border-radius: 8px; 8196 margin: 0; 8197} 8198 8199.disclaimer-icon { 8200 font-size: 1.25rem; 8201 flex-shrink: 0; 8202 margin-top: 0.125rem; 8203} 8204 8205.disclaimer-text { 8206 font-size: 0.85rem; 8207 line-height: 1.4; 8208 color: #ae7e21; 8209} 8210 8211.disclaimer-text strong { 8212 color: #765212; 8213 font-weight: 600; 8214} 8215 8216/* Compare Disclaimer in Sidebar */ 8217.compare-disclaimer { 8218 margin-top: 1rem; 8219 padding: 1rem; 8220 background: #f0f9ff; 8221 border: 1px solid #0ea5e9; 8222 border-radius: 8px; 8223} 8224 8225.compare-disclaimer h6 { 8226 display: flex; 8227 align-items: center; 8228 gap: 0.5rem; 8229 font-size: 0.875rem; 8230 font-weight: 600; 8231 color: #0c4a6e; 8232 margin-bottom: 0.5rem; 8233} 8234 8235.compare-disclaimer p { 8236 font-size: 0.8rem; 8237 line-height: 1.4; 8238 color: #075985; 8239 margin-bottom: 0.5rem; 8240} 8241 8242.compare-disclaimer p:last-child { 8243 margin-bottom: 0; 8244} 8245 8246.compare-disclaimer ul { 8247 list-style: none; 8248 padding: 0; 8249 margin: 0.5rem 0 0 0; 8250} 8251 8252.compare-disclaimer li { 8253 display: flex; 8254 align-items: flex-start; 8255 gap: 0.5rem; 8256 margin-bottom: 0.25rem; 8257 font-size: 0.75rem; 8258 line-height: 1.3; 8259 color: #075985; 8260} 8261 8262.compare-disclaimer li:before { 8263 content: "â¢"; 8264 color: #0ea5e9; 8265 font-weight: bold; 8266 margin-top: 0.1rem; 8267 flex-shrink: 0; 8268} 8269 8270/* Mobile Browser Address Bar Fixes */ 8271@supports (height: 100dvh) { 8272 /* Modern browsers with dynamic viewport support */ 8273 @media (max-width: 768px) { 8274 .compare-sidebar { 8275 height: 100dvh !important; 8276 max-height: 100dvh !important; 8277 } 8278 8279 .modal-container { 8280 max-height: 95dvh !important; 8281 } 8282 } 8283} 8284 8285@supports (height: -webkit-fill-available) { 8286 /* iOS Safari specific support */ 8287 @media (max-width: 768px) { 8288 .compare-sidebar { 8289 height: -webkit-fill-available !important; 8290 max-height: -webkit-fill-available !important; 8291 } 8292 } 8293} 8294 8295/* Fallback for older browsers */ 8296@media (max-width: 768px) and (orientation: portrait) { 8297 .compare-sidebar { 8298 /* Fixed height that works across different mobile browsers */ 8299 min-height: 600px; 8300 height: calc(100vh - env(keyboard-inset-height, 0px)); 8301 height: calc(100vh - env(safe-area-inset-bottom, 0px)); 8302 } 8303 8304 /* Ensure actions are always accessible */ 8305 .compare-actions { 8306 position: sticky !important; 8307 bottom: env(safe-area-inset-bottom, 0px) !important; 8308 margin-bottom: 0 !important; 8309 padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)) !important; 8310 } 8311} 8312 8313/* PWA and fullscreen app support */ 8314@media (display-mode: standalone) { 8315 .compare-sidebar { 8316 height: 100vh !important; 8317 height: 100dvh !important; 8318 } 8319} 8320 8321/* Utilities */
8322.btn-sm { 8323 padding: 0.5rem 0.75rem; 8324 font-size: 0.8rem; 8325} 8326 8327[hidden] { 8328 display: none !important; 8329} 8330 8331/* ===== UX ENHANCEMENTS ===== */ 8332 8333/* Compare Button Highlight Animation */ 8334.pulse-highlight { 8335 animation: pulse-glow 2s ease-in-out 3; 8336 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8337} 8338 8339@keyframes pulse-glow { 8340 0% { 8341 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); 8342 } 8343 50% { 8344 box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); 8345 } 8346 100% { 8347 box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); 8348 } 8349} 8350 8351/* Flying Product Animation */ 8352.flying-product { 8353 position: fixed; 8354 z-index: 9999; 8355 pointer-events: none; 8356} 8357 8358.flying-product img { 8359 border-radius: 8px; 8360 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 8361} 8362 8363/* Sticky Compare Bar - REMOVED (using only fly-to-navbar animation) */ 8364 8365/* Inline Compare Preview - REMOVED (keeping it simple and clean) */ 8366</style> 8367 <style data-inlined="product-card-actions.css"> 8368/** 8369 * Product Card Quick Actions - 3 Dot Menu + Bottom Sheet 8370 * Tokopedia-inspired design 8371 */ 8372 8373/* ===== 3 Dot Button on Product Card ===== */ 8374.prod-card { 8375 position: relative; 8376} 8377 8378.prod-card-actions-btn { 8379 position: absolute; 8380 top: 8px; 8381 right: 8px; 8382 width: 32px; 8383 height: 32px; 8384 border-radius: 50%; 8385 background: rgba(255, 255, 255, 0.95); 8386 border: 1px solid rgba(0, 0, 0, 0.08); 8387 display: inline-flex;
8388 align-items: center; 8389 justify-content: center; 8390 cursor: pointer; 8391 z-index: 3; 8392 transition: all 0.2s ease; 8393 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 8394 -webkit-tap-highlight-color: transparent; 8395} 8396 8397.prod-card-actions-btn:hover { 8398 background: #fff; 8399 border-color: rgba(0, 0, 0, 0.15); 8400 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 8401 transform: scale(1.05); 8402} 8403 8404.prod-card-actions-btn:active { 8405 transform: scale(0.95); 8406} 8407 8408.prod-card-actions-btn svg { 8409 width: 20px; 8410 height: 20px; 8411 color: #64748b; 8412 display: block; 8413} 8414 8415/* Desktop: Show on hover */ 8416@media (min-width: 641px) { 8417 .prod-card-actions-btn { 8418 opacity: 0; 8419 pointer-events: none; 8420 } 8421 8422 .prod-card:hover .prod-card-actions-btn { 8423 opacity: 1; 8424 pointer-events: auto; 8425 } 8426} 8427 8428/* Mobile: Always visible but smaller */ 8429@media (max-width: 640px) { 8430 .prod-card-actions-btn { 8431 width: 28px; 8432 height: 28px; 8433 top: 6px; 8434 right: 6px; 8435 opacity: 95%; 8436 } 8437 8438 .prod-card-actions-btn svg { 8439 width: 18px; 8440 height: 18px; 8441 } 8442} 8443 8444/* ===== Bottom Sheet Overlay ===== */ 8445.pca-overlay { 8446 position: fixed; 8447 inset: 0; 8448 background: rgba(15, 23, 42, 0.6); 8449 backdrop-filter: blur(4px); 8450 z-index: 99998; 8451 opacity: 0; 8452 visibility: hidden; 8453 transition: all 0.3s ease; 8454} 8455 8456.pca-overlay.active { 8457 opacity: 1; 8458 visibility: visible; 8459} 8460 8461/* ===== Bottom Sheet Container ===== */ 8462.pca-bottom-sheet { 8463 position: fixed; 8464 bottom: 0; 8465 left: 0; 8466 right: 0; 8467 background: #fff; 8468 border-radius: 20px 20px 0 0; 8469 box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); 8470 z-index: 99999; 8471 max-width: 600px; 8472 margin: 0 auto; 8473 transform: translateY(100%); 8474 transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 8475} 8476 8477.pca-bottom-sheet.active { 8478 transform: translateY(0); 8479} 8480 8481/* Drag Handle */ 8482.pca-sheet-handle { 8483 width: 40px; 8484 height: 4px; 8485 background: #cbd5e1; 8486 border-radius: 999px; 8487 margin: 12px auto 8px; 8488} 8489 8490/* Product Preview */ 8491.pca-sheet-product { 8492 display: flex; 8493 gap: 12px; 8494 padding: 12px 20px; 8495 border-bottom: 1px solid #f1f5f9; 8496} 8497 8498.pca-product-image { 8499 width: 60px; 8500 height: 60px; 8501 border-radius: 8px; 8502 object-fit: cover; 8503 flex-shrink: 0; 8504 border: 1px solid #e2e8f0; 8505} 8506 8507.pca-product-info { 8508 flex: 1; 8509 min-width: 0; 8510} 8511 8512.pca-product-name { 8513 font-size: 14px; 8514 font-weight: 600; 8515 color: #0f172a; 8516 margin: 0 0 4px 0; 8517 display: -webkit-box; 8518 -webkit-line-clamp: 2; 8519 -webkit-box-orient: vertical; 8520 overflow: hidden; 8521 line-height: 1.3; 8522} 8523 8524.pca-product-price { 8525 font-size: 15px; 8526 font-weight: 700; 8527 color: #0ea5e9; 8528 margin: 0; 8529} 8530 8531.pca-product-stock { 8532 font-size: 12px; 8533 color: #64748b; 8534 margin: 2px 0 0 0; 8535} 8536 8537.pca-product-stock.in { 8538 color: #16a34a; 8539} 8540 8541.pca-product-stock.low { 8542 color: #C10DFF; 8543} 8544 8545.pca-product-stock.out { 8546 color: #e2a642; 8547} 8548 8549/* Action Menu */ 8550.pca-sheet-actions { 8551 padding: 8px 0 20px; 8552} 8553 8554.pca-action-item { 8555 display: flex;
8556 align-items: center; 8557 gap: 16px; 8558 padding: 16px 20px; 8559 background: transparent; 8560 border: 0; 8561 width: 100%; 8562 cursor: pointer; 8563 transition: background-color 0.15s ease; 8564 -webkit-tap-highlight-color: transparent; 8565} 8566 8567.pca-action-item:hover { 8568 background: #f8fafc; 8569} 8570 8571.pca-action-item:active { 8572 background: #f1f5f9; 8573} 8574 8575.pca-action-item:disabled { 8576 opacity: 0.4; 8577 cursor: not-allowed; 8578} 8579 8580.pca-action-item:disabled:hover { 8581 background: transparent; 8582} 8583 8584.pca-action-icon { 8585 width: 24px; 8586 height: 24px; 8587 color: #475569; 8588 flex-shrink: 0; 8589} 8590 8591.pca-action-content { 8592 flex: 1; 8593 text-align: left; 8594} 8595 8596.pca-action-label { 8597 font-size: 15px; 8598 font-weight: 600; 8599 color: #0f172a; 8600 margin: 0 0 2px 0; 8601} 8602 8603.pca-action-desc { 8604 font-size: 12px; 8605 color: #64748b; 8606 margin: 0; 8607} 8608 8609.pca-action-badge { 8610 display: inline-flex; 8611 align-items: center; 8612 justify-content: center; 8613 min-width: 20px; 8614 height: 20px; 8615 padding: 0 6px; 8616 background: #0ea5e9; 8617 color: #fff; 8618 font-size: 11px; 8619 font-weight: 700; 8620 border-radius: 999px; 8621 margin-left: auto; 8622} 8623 8624.pca-action-badge.compare { 8625 background: #8b5cf6; 8626} 8627 8628/* Action Item Colors */ 8629.pca-action-item.cart .pca-action-icon { 8630 color: #0ea5e9; 8631} 8632 8633.pca-action-item.compare .pca-action-icon { 8634 color: #8b5cf6; 8635} 8636 8637.pca-action-item.compare.active { 8638 background: rgba(139, 92, 246, 0.05); 8639} 8640 8641.pca-action-item.compare.active .pca-action-label { 8642 color: #8b5cf6; 8643} 8644 8645.pca-action-item.compare.active .pca-action-icon { 8646 color: #8b5cf6; 8647} 8648 8649.pca-action-item.share .pca-action-icon { 8650 color: #64748b; 8651} 8652 8653/* Loading State */ 8654.pca-action-item.loading { 8655 pointer-events: none; 8656} 8657 8658.pca-action-item.loading .pca-action-icon { 8659 animation: spin 1s linear infinite; 8660} 8661 8662@keyframes spin { 8663 from { transform: rotate(0deg); } 8664 to { transform: rotate(360deg); } 8665} 8666 8667/* Success State Animation */ 8668.pca-action-item.success .pca-action-icon { 8669 animation: checkmark 0.5s ease; 8670} 8671 8672@keyframes checkmark { 8673 0% { transform: scale(0); } 8674 50% { transform: scale(1.2); } 8675 100% { transform: scale(1); } 8676} 8677 8678/* ===== Safe Area for Mobile Devices ===== */ 8679@supports (padding-bottom: env(safe-area-inset-bottom)) { 8680 .pca-sheet-actions { 8681 padding-bottom: calc(20px + env(safe-area-inset-bottom)); 8682 } 8683} 8684 8685/* ===== Responsive Tweaks ===== */ 8686@media (max-width: 640px) { 8687 .pca-bottom-sheet { 8688 border-radius: 16px 16px 0 0; 8689 } 8690 8691 .pca-sheet-product { 8692 padding: 10px 16px; 8693 } 8694 8695 .pca-product-image { 8696 width: 50px; 8697 height: 50px; 8698 } 8699 8700 .pca-product-name { 8701 font-size: 13px; 8702 } 8703 8704 .pca-product-price { 8705 font-size: 14px; 8706 } 8707 8708 .pca-action-item { 8709 padding: 14px 16px; 8710 gap: 12px; 8711 } 8712 8713 .pca-action-icon { 8714 width: 22px; 8715 height: 22px; 8716 } 8717 8718 .pca-action-label { 8719 font-size: 14px; 8720 } 8721} 8722 8723/* ===== Animation for Flying Product ===== */ 8724.flying-product { 8725 position: fixed; 8726 pointer-events: none; 8727 z-index: 99999; 8728 animation: fadeOut 0.8s ease forwards; 8729} 8730 8731@keyframes fadeOut { 8732 to { 8733 opacity: 0; 8734 transform: scale(0.5); 8735 } 8736} 8737 8738/* ===== Desktop Dropdown (Alternative) ===== */ 8739@media (min-width: 1024px) { 8740 /* Optional: Could add desktop dropdown menu here */ 8741 /* For now, using bottom sheet for consistency */ 8742} 8743 8744/* ===== Print Styles ===== */ 8745@media print { 8746 .prod-card-actions-btn, 8747 .pca-overlay, 8748 .pca-bottom-sheet { 8749 display: none !important; 8750 } 8751} 8752 8753</style> 8754 <style data-inlined="modal.css"> 8755/* Modern Modal Dialog for Cart Operations */ 8756.modal-overlay { 8757 position: fixed; 8758 top: 0; 8759 left: 0; 8760 right: 0; 8761 bottom: 0; 8762 background: rgba(0, 0, 0, 0.5); 8763 backdrop-filter: blur(4px); 8764 display: flex; 8765 align-items: center; 8766 justify-content: center; 8767 z-index: 9999; 8768 opacity: 0; 8769 visibility: hidden; 8770 transition: all 0.2s ease; 8771} 8772 8773.modal-overlay.show { 8774 opacity: 1;
8775 visibility: visible; 8776} 8777 8778.modal-dialog { 8779 background: white; 8780 border-radius: 16px; 8781 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15); 8782 max-width: 400px; 8783 width: 90%; 8784 max-height: 90vh; 8785 overflow-y: auto; 8786 transform: scale(0.95); 8787 transition: transform 0.2s ease; 8788} 8789 8790.modal-overlay.show .modal-dialog { 8791 transform: scale(1); 8792} 8793 8794.modal-header { 8795 padding: 24px 24px 16px; 8796 border-bottom: 1px solid #f1f5f9; 8797} 8798 8799.modal-title { 8800 font-size: 20px; 8801 font-weight: 700; 8802 color: #0f172a; 8803 margin: 0; 8804 display: flex; 8805 align-items: center; 8806 gap: 12px; 8807} 8808 8809.modal-body { 8810 padding: 16px 24px 24px; 8811} 8812 8813.modal-message { 8814 font-size: 16px; 8815 line-height: 1.5; 8816 color: #475569; 8817 margin: 0; 8818} 8819 8820.modal-actions { 8821 display: flex; 8822 gap: 12px; 8823 margin-top: 24px; 8824} 8825 8826.modal-btn { 8827 flex: 1; 8828 padding: 12px 16px; 8829 border: none; 8830 border-radius: 8px; 8831 font-size: 14px; 8832 font-weight: 600; 8833 cursor: pointer; 8834 transition: all 0.2s ease; 8835 text-align: center; 8836} 8837 8838.modal-btn-secondary { 8839 background: #f8fafc; 8840 color: #64748b; 8841 border: 1px solid #e2e8f0; 8842} 8843 8844.modal-btn-secondary:hover { 8845 background: #f1f5f9; 8846 border-color: #cbd5e1; 8847} 8848 8849.modal-btn-danger { 8850 background: #e2a642; 8851 color: white; 8852} 8853 8854.modal-btn-danger:hover { 8855 background: #e2a642; 8856} 8857 8858.modal-btn-primary { 8859 background: #3b82f6; 8860 color: white; 8861} 8862 8863.modal-btn-primary:hover { 8864 background: #2563eb; 8865} 8866 8867.modal-icon { 8868 font-size: 24px; 8869 width: 32px; 8870 height: 32px; 8871 display: flex; 8872 align-items: center; 8873 justify-content: center; 8874 border-radius: 8px; 8875} 8876 8877.modal-icon.danger { 8878 background: #fef2f2; 8879 color: #e2a642; 8880} 8881 8882.modal-icon.warning { 8883 background: #fffbeb; 8884 color: #C10DFF; 8885} 8886 8887.modal-icon.success { 8888 background: #f0fdf4; 8889 color: #22c55e; 8890} 8891 8892.modal-icon.info { 8893 background: #eff6ff; 8894 color: #3b82f6; 8895} 8896 8897/* Mobile responsive */ 8898@media (max-width: 640px) { 8899 .modal-dialog { 8900 margin: 16px; 8901 max-width: none; 8902 width: auto; 8903 } 8904 8905 .modal-header, 8906 .modal-body { 8907 padding-left: 20px; 8908 padding-right: 20px; 8909 } 8910 8911 .modal-actions { 8912 flex-direction: column; 8913 } 8914 8915 .modal-btn { 8916 flex: none; 8917 } 8918} 8919</style> 8920 8921 <style data-inlined="reviews-points.css"> 8922/** 8923 * Reviews & Points System - Styles 8924 * Product reviews, rating display, and point redemption UI 8925 */ 8926 8927/* =================================== 8928 PRODUCT REVIEWS 8929 =================================== */ 8930 8931.product-tabs { 8932 display: flex; 8933 border-bottom: 2px solid #e5e7eb; 8934 margin: 24px 0 16px; 8935 gap: 8px; 8936} 8937 8938.tab-btn { 8939 padding: 12px 20px; 8940 border: none; 8941 background: none; 8942 cursor: pointer; 8943 font-size: 15px; 8944 font-weight: 500; 8945 color: #6b7280; 8946 border-bottom: 3px solid transparent; 8947 transition: all 0.2s ease; 8948 position: relative; 8949 top: 2px; 8950} 8951 8952.tab-btn:hover { 8953 color: #111827; 8954} 8955 8956.tab-btn.active { 8957 color: #2563eb; 8958 border-bottom-color: #2563eb; 8959} 8960 8961.tab-content { 8962 padding: 20px 0; 8963} 8964 8965/* Review Statistics */ 8966.review-stats { 8967 display: grid; 8968 grid-template-columns: 200px 1fr; 8969 gap: 32px; 8970 padding: 24px; 8971 background: #f9fafb; 8972 border-radius: 12px; 8973 margin-bottom: 24px; 8974} 8975 8976.rating-summary { 8977 text-align: center; 8978} 8979 8980.avg-rating { 8981 display: block; 8982 font-size: 48px; 8983 font-weight: 700; 8984 color: #111827; 8985 line-height: 1; 8986} 8987 8988.rating-summary .stars { 8989 font-size: 24px; 8990 color: #C10DFF; 8991 margin: 8px 0; 8992} 8993 8994.total-reviews { 8995 display: block; 8996 font-size: 14px; 8997 color: #6b7280; 8998 margin-top: 4px; 8999} 9000 9001.rating-breakdown { 9002 display: flex; 9003 flex-direction: column; 9004 gap: 8px; 9005} 9006 9007.rating-bar { 9008 display: flex; 9009 align-items: center; 9010 gap: 12px; 9011} 9012 9013.rating-label { 9014 font-size: 14px; 9015 color: #374151; 9016 width: 32px; 9017 text-align: right; 9018} 9019 9020.bar-container { 9021 flex: 1; 9022 height: 8px; 9023 background: #e5e7eb; 9024 border-radius: 4px; 9025 overflow: hidden; 9026} 9027 9028.bar-fill { 9029 height: 100%; 9030 background: linear-gradient(90deg, #C10DFF 0%, #C10DFF 100%); 9031 transition: width 0.3s ease; 9032} 9033 9034.rating-count { 9035 font-size: 14px; 9036 color: #6b7280; 9037 width: 40px; 9038 text-align: right; 9039} 9040 9041/* Review Filters */ 9042.review-filters { 9043 display: flex; 9044 justify-content: space-between; 9045 align-items: center; 9046 margin-bottom: 20px; 9047} 9048 9049.review-sort { 9050 padding: 8px 12px; 9051 border: 1px solid #d1d5db; 9052 border-radius: 8px; 9053 font-size: 14px; 9054 background: white; 9055} 9056 9057.write-review-btn { 9058 padding: 10px 20px; 9059 background: #2563eb; 9060 color: white; 9061 border: none; 9062 border-radius: 8px; 9063 font-weight: 500;
9064 cursor: pointer; 9065 transition: background 0.2s; 9066} 9067 9068.write-review-btn:hover { 9069 background: #1d4ed8; 9070} 9071 9072/* Review List */ 9073.reviews-list { 9074 display: flex; 9075 flex-direction: column; 9076 gap: 16px; 9077} 9078 9079.review-item { 9080 padding: 20px; 9081 background: white; 9082 border: 1px solid #e5e7eb; 9083 border-radius: 12px; 9084 transition: box-shadow 0.2s; 9085} 9086 9087.review-item:hover { 9088 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 9089} 9090 9091.review-header { 9092 display: flex; 9093 justify-content: space-between; 9094 align-items: flex-start; 9095 margin-bottom: 8px; 9096} 9097 9098.review-author { 9099 display: flex; 9100 align-items: center; 9101 gap: 8px; 9102} 9103 9104.review-author strong { 9105 font-size: 15px; 9106 color: #111827; 9107} 9108 9109.verified-badge { 9110 display: inline-flex; 9111 align-items: center; 9112 gap: 4px; 9113 font-size: 12px; 9114 color: #059669; 9115 background: #d1fae5; 9116 padding: 2px 8px; 9117 border-radius: 4px; 9118 font-weight: 500; 9119} 9120 9121.anonymous-badge { 9122 display: inline-flex; 9123 align-items: center; 9124 font-size: 12px; 9125 color: #6b7280; 9126 background: #f3f4f6; 9127 padding: 2px 6px; 9128 border-radius: 4px; 9129 margin-left: 4px; 9130 cursor: help; 9131} 9132 9133.review-rating .stars { 9134 font-size: 16px; 9135 color: #C10DFF; 9136} 9137 9138.review-date { 9139 font-size: 13px; 9140 color: #9ca3af; 9141 margin-bottom: 12px; 9142} 9143 9144.review-text { 9145 font-size: 15px; 9146 line-height: 1.6; 9147 color: #374151; 9148 margin-bottom: 12px; 9149} 9150 9151.review-images { 9152 display: flex; 9153 gap: 8px; 9154 margin-bottom: 12px; 9155 flex-wrap: wrap; 9156} 9157 9158.review-img { 9159 width: 80px; 9160 height: 80px; 9161 object-fit: cover; 9162 border-radius: 8px; 9163 cursor: pointer; 9164 transition: transform 0.2s; 9165} 9166 9167.review-img:hover { 9168 transform: scale(1.05); 9169} 9170 9171.review-actions { 9172 display: flex; 9173 gap: 12px; 9174 padding-top: 12px; 9175 border-top: 1px solid #f3f4f6; 9176} 9177 9178.btn-helpful { 9179 display: flex; 9180 align-items: center; 9181 gap: 6px; 9182 padding: 6px 12px; 9183 background: #f9fafb; 9184 border: 1px solid #e5e7eb; 9185 border-radius: 6px; 9186 font-size: 13px; 9187 color: #6b7280; 9188 cursor: pointer; 9189 transition: all 0.2s; 9190} 9191 9192.btn-helpful:hover { 9193 background: #f3f4f6; 9194 color: #111827; 9195 border-color: #d1d5db; 9196} 9197 9198.btn-helpful.voted { 9199 background: #dbeafe; 9200 color: #1d4ed8; 9201 border-color: #93c5fd; 9202} 9203 9204.helpful-icon { 9205 font-size: 14px; 9206} 9207 9208.no-reviews { 9209 text-align: center; 9210 padding: 60px 20px; 9211 color: #9ca3af; 9212 font-size: 15px; 9213} 9214 9215.load-more-btn { 9216 display: block; 9217 margin: 24px auto 0; 9218 padding: 12px 32px; 9219 background: white; 9220 border: 1px solid #d1d5db; 9221 border-radius: 8px; 9222 font-size: 14px; 9223 font-weight: 500; 9224 color: #374151; 9225 cursor: pointer; 9226 transition: all 0.2s; 9227} 9228 9229.load-more-btn:hover { 9230 background: #f9fafb; 9231 border-color: #9ca3af; 9232} 9233 9234/* =================================== 9235 POINT BALANCE WIDGET (LEGACY - kept for backward compatibility) 9236 =================================== */ 9237 9238.point-balance-card { 9239 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 9240 color: white; 9241 padding: 24px; 9242 border-radius: 16px; 9243 margin-bottom: 24px; 9244 box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); 9245} 9246 9247.point-header { 9248 display: flex; 9249 justify-content: space-between; 9250 align-items: center; 9251 margin-bottom: 12px; 9252} 9253 9254.point-header h3 { 9255 font-size: 18px; 9256 font-weight: 600; 9257 margin: 0; 9258 display: flex; 9259 align-items: center; 9260 gap: 8px; 9261} 9262 9263.point-header .balance { 9264 font-size: 32px; 9265 font-weight: 700; 9266} 9267 9268.point-value { 9269 font-size: 14px; 9270 opacity: 0.9; 9271 margin: 8px 0 16px; 9272} 9273 9274.point-stats { 9275 display: flex; 9276 gap: 20px; 9277 font-size: 13px; 9278 opacity: 0.85; 9279 margin-bottom: 16px; 9280} 9281 9282.btn-view-history { 9283 display: inline-block; 9284 padding: 10px 20px; 9285 background: rgba(255, 255, 255, 0.2); 9286 color: white; 9287 text-decoration: none; 9288 border-radius: 8px; 9289 font-size: 14px; 9290 font-weight: 500; 9291 transition: background 0.2s; 9292} 9293 9294.btn-view-history:hover { 9295 background: rgba(255, 255, 255, 0.3); 9296} 9297 9298/* =================================== 9299 CHECKOUT POINT REDEMPTION 9300 =================================== */ 9301 9302.checkout-points { 9303 padding: 16px; 9304 background: #fef3c7; 9305 border: 2px solid #C10DFF; 9306 border-radius: 12px; 9307 margin-bottom: 16px; 9308} 9309 9310.points-header { 9311 display: flex; 9312 align-items: center; 9313 gap: 10px; 9314 margin-bottom: 12px; 9315} 9316 9317.points-header input[type="checkbox"] { 9318 width: 18px;
9319 height: 18px; 9320 cursor: pointer; 9321} 9322 9323.points-header label { 9324 font-size: 15px; 9325 color: #765212; 9326 cursor: pointer; 9327 margin: 0; 9328} 9329 9330#points-slider { 9331 padding: 16px; 9332 background: white; 9333 border-radius: 8px; 9334 margin-top: 12px; 9335} 9336 9337#points-slider label { 9338 display: block; 9339 font-size: 14px; 9340 color: #374151; 9341 margin-bottom: 8px; 9342 font-weight: 500; 9343} 9344 9345#points-amount { 9346 width: 100%; 9347 margin-bottom: 8px; 9348} 9349 9350#points-display { 9351 display: flex; 9352 justify-content: space-between; 9353 font-size: 14px; 9354 color: #111827; 9355 margin-top: 8px; 9356} 9357 9358#points-display .discount-amount { 9359 color: #059669; 9360 font-weight: 600; 9361} 9362 9363.price-row.point-discount { 9364 color: #059669; 9365 font-weight: 500; 9366} 9367 9368/* =================================== 9369 REVIEWABLE ORDERS 9370 =================================== */ 9371 9372.reviewable-orders { 9373 background: white; 9374 border: 1px solid #e5e7eb; 9375 border-radius: 12px; 9376 padding: 20px; 9377 margin-bottom: 24px; 9378} 9379 9380.reviewable-orders h3 { 9381 font-size: 18px; 9382 margin-bottom: 16px; 9383 color: #111827; 9384} 9385 9386.order-card { 9387 display: flex; 9388 gap: 16px; 9389 padding: 16px; 9390 background: #f9fafb; 9391 border-radius: 8px; 9392 margin-bottom: 12px; 9393} 9394 9395.order-card:last-child { 9396 margin-bottom: 0; 9397} 9398 9399.order-card img { 9400 width: 80px; 9401 height: 80px; 9402 object-fit: cover; 9403 border-radius: 8px; 9404} 9405 9406.order-info { 9407 flex: 1; 9408} 9409 9410.order-info h4 { 9411 font-size: 15px; 9412 margin: 0 0 4px; 9413 color: #111827; 9414} 9415 9416.order-info p { 9417 font-size: 13px; 9418 color: #6b7280; 9419 margin: 0 0 12px; 9420} 9421 9422.btn-review { 9423 padding: 8px 16px; 9424 background: #059669; 9425 color: white; 9426 border: none; 9427 border-radius: 6px; 9428 font-size: 14px; 9429 font-weight: 500; 9430 cursor: pointer; 9431 transition: background 0.2s; 9432} 9433 9434.btn-review:hover { 9435 background: #047857; 9436} 9437 9438/* =================================== 9439 TOAST NOTIFICATIONS 9440 =================================== */ 9441 9442.review-toast { 9443 position: fixed; 9444 bottom: 24px; 9445 right: 24px; 9446 padding: 14px 20px; 9447 background: #111827; 9448 color: white; 9449 border-radius: 8px; 9450 font-size: 14px; 9451 line-height: 1.4; 9452 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); 9453 transform: translateY(100px); 9454 opacity: 0; 9455 transition: all 0.3s ease; 9456 z-index: 10000; 9457 max-width: 400px; 9458 word-wrap: break-word; 9459} 9460 9461.review-toast.show { 9462 transform: translateY(0); 9463 opacity: 1; 9464} 9465 9466/* Mobile: Position above bottom-nav */ 9467@media (max-width: 768px) { 9468 .review-toast { 9469 bottom: 80px; /* Above bottom-nav (50px height + padding + safe margin) */ 9470 right: 16px; 9471 left: 16px; 9472 max-width: calc(100% - 32px); 9473 } 9474} 9475 9476.review-toast.toast-success { 9477 background: #059669; 9478} 9479 9480.review-toast.toast-error { 9481 background: #e2a642; 9482} 9483 9484.review-toast.toast-info { 9485 background: #2563eb; 9486} 9487 9488/* =================================== 9489 RESPONSIVE 9490 =================================== */ 9491 9492@media (max-width: 768px) { 9493 .point-balance-card { 9494 border-radius: 0; 9495 } 9496 9497 .review-stats { 9498 grid-template-columns: 70px 1fr; 9499 gap: 20px; 9500 padding: 16px; 9501 } 9502 9503 .rating-summary { 9504 text-align: center; 9505 } 9506 9507 .rating-summary .avg-rating { 9508 font-size: 32px; 9509 } 9510 9511 .rating-summary .stars { 9512 font-size: 18px; 9513 margin: 4px 0; 9514 } 9515 9516 .total-reviews { 9517 font-size: 11px; 9518 } 9519 9520 /* Rating breakdown - more compact */ 9521 .rating-breakdown { 9522 gap: 2px; 9523 } 9524 9525 .rating-breakdown .rating-bar { 9526 gap: 4px; 9527 } 9528 9529 .rating-breakdown .rating-label { 9530 font-size: 11px; 9531 width: 16px; 9532 min-width: 16px; 9533 text-align: left; 9534 } 9535 9536 .rating-breakdown .bar-container { 9537 height: 6px; 9538 } 9539 9540 .rating-breakdown .rating-count { 9541 font-size: 11px; 9542 width: 16px; 9543 min-width: 16px; 9544 } 9545 9546 .review-header { 9547 flex-direction: column; 9548 gap: 8px; 9549 align-items: flex-start; 9550 } 9551 9552 .review-rating { 9553 order: -1; 9554 margin-bottom: 4px; 9555 } 9556 9557 .review-rating .stars { 9558 font-size: 14px; 9559 } 9560 9561 .review-filters { 9562 flex-direction: column; 9563 gap: 12px;
9564 align-items: stretch; 9565 } 9566 9567 .review-sort { 9568 width: 100%; 9569 } 9570 9571 .point-header .balance { 9572 font-size: 24px; 9573 } 9574 9575 .point-stats { 9576 flex-direction: column; 9577 gap: 8px; 9578 } 9579 9580 .order-card { 9581 flex-direction: column; 9582 } 9583 9584 .order-card img { 9585 width: 100%; 9586 height: 200px; 9587 } 9588} 9589 9590/* =================================== 9591 LOADING & ERROR STATES 9592 =================================== */ 9593 9594.review-loading { 9595 text-align: center; 9596 padding: 40px; 9597 color: #9ca3af; 9598 font-size: 14px; 9599} 9600 9601.review-error { 9602 text-align: center; 9603 padding: 40px; 9604 color: #e2a642; 9605 font-size: 14px; 9606 background: #fee2e2; 9607 border-radius: 8px; 9608} 9609 9610.review-empty { 9611 text-align: center; 9612 padding: 10px; 9613 color: #6b7280; 9614 font-size: 14px; 9615} 9616 9617/* =================================== 9618 REVIEWABLE ORDERS LIST 9619 =================================== */ 9620 9621.reviewable-orders-list { 9622 display: flex; 9623 flex-direction: column; 9624 gap: 16px; 9625} 9626 9627.reviewable-order-item { 9628 display: grid; 9629 grid-template-columns: 80px 1fr auto; 9630 gap: 16px; 9631 padding: 16px; 9632 background: #fff; 9633 border: 1px solid #e5e7eb; 9634 border-radius: 12px; 9635 align-items: center; 9636 transition: box-shadow 0.2s; 9637} 9638 9639.reviewable-order-item:hover { 9640 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 9641} 9642 9643.reviewable-order-item .roi-image { 9644 width: 80px; 9645 height: 80px; 9646 object-fit: cover; 9647 border-radius: 8px; 9648 border: 1px solid #e5e7eb; 9649} 9650 9651.reviewable-order-item .roi-info { 9652 display: flex; 9653 flex-direction: column; 9654 gap: 4px; 9655} 9656 9657.reviewable-order-item .roi-info h4 { 9658 margin: 0; 9659 font-size: 15px; 9660 font-weight: 600; 9661 color: #111827; 9662 line-height: 1.4; 9663} 9664 9665.reviewable-order-item .roi-meta { 9666 margin: 0; 9667 font-size: 13px; 9668 color: #6b7280; 9669} 9670 9671.reviewable-order-item .btn-write-review { 9672 white-space: nowrap; 9673 padding: 10px 20px; 9674 font-size: 14px; 9675} 9676 9677@media (max-width: 640px) { 9678 .reviewable-order-item { 9679 grid-template-columns: 60px 1fr; 9680 gap: 12px; 9681 } 9682 9683 .reviewable-order-item .roi-image { 9684 width: 60px; 9685 height: 60px; 9686 } 9687 9688 .reviewable-order-item .btn-write-review { 9689 grid-column: 1 / -1; 9690 width: 100%; 9691 } 9692} 9693 9694/* =================================== 9695 REVIEW SUBMISSION MODAL 9696 =================================== */ 9697 9698/* Modal Overlay Background */ 9699#review-modal-overlay { 9700 position: fixed; 9701 top: 0; 9702 left: 0; 9703 right: 0; 9704 bottom: 0; 9705 background: rgba(0, 0, 0, 0.6) !important; 9706 backdrop-filter: blur(8px); 9707 display: flex; 9708 align-items: center; 9709 justify-content: center; 9710 z-index: 99999 !important; 9711 padding: 20px; 9712} 9713 9714.review-modal { 9715 max-width: 600px !important; 9716 width: 90% !important; 9717 background: white; 9718 border-radius: 16px !important; 9719 box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); 9720 max-height: 90vh; 9721 display: flex; 9722 flex-direction: column; 9723 overflow: hidden !important; 9724} 9725 9726#review-modal-overlay .modal-content, 9727.review-modal .modal-content { 9728 padding: 0 !important; 9729 flex: initial !important; 9730 overflow-y: visible !important; 9731} 9732 9733.review-modal .modal-header { 9734 display: flex; 9735 justify-content: space-between; 9736 align-items: center; 9737 padding: 20px 24px; 9738 border-bottom: 1px solid #e5e7eb; 9739} 9740 9741.review-modal .modal-title { 9742 margin: 0; 9743 font-size: 20px; 9744 font-weight: 700; 9745 color: #111827; 9746} 9747 9748.review-modal .modal-close { 9749 background: none; 9750 border: none; 9751 cursor: pointer; 9752 padding: 4px; 9753 color: #6b7280; 9754 transition: color 0.2s; 9755 display: flex; 9756 align-items: center; 9757 justify-content: center; 9758} 9759
9760.review-modal .modal-close:hover { 9761 color: #111827; 9762} 9763 9764.review-modal .modal-body { 9765 padding: 24px; 9766 max-height: calc(90vh - 140px); 9767 overflow-y: auto; 9768} 9769 9770/* Custom Scrollbar for Modal */ 9771.review-modal .modal-body::-webkit-scrollbar { 9772 width: 8px; 9773} 9774 9775.review-modal .modal-body::-webkit-scrollbar-track { 9776 background: #f1f5f9; 9777 border-radius: 4px; 9778} 9779 9780.review-modal .modal-body::-webkit-scrollbar-thumb { 9781 background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); 9782 border-radius: 4px; 9783} 9784 9785.review-modal .modal-body::-webkit-scrollbar-thumb:hover { 9786 background: linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%); 9787} 9788 9789/* Firefox Scrollbar */ 9790.review-modal .modal-body { 9791 scrollbar-width: thin; 9792 scrollbar-color: #3b82f6 #f1f5f9; 9793} 9794 9795.review-modal .modal-footer { 9796 display: flex; 9797 gap: 12px; 9798 margin-top: 24px; 9799 padding-top: 20px; 9800 border-top: 1px solid #f1f5f9; 9801} 9802 9803.review-product-info { 9804 display: flex; 9805 gap: 16px; 9806 padding: 16px; 9807 background: #f9fafb; 9808 border-radius: 8px; 9809 margin-bottom: 24px; 9810} 9811 9812.review-product-thumb { 9813 width: 80px; 9814 height: 80px; 9815 object-fit: cover; 9816 border-radius: 6px; 9817 border: 1px solid #e5e7eb; 9818} 9819 9820.review-product-details h4 { 9821 margin: 0 0 8px 0; 9822 font-size: 16px; 9823 font-weight: 600; 9824 color: #111827; 9825} 9826 9827.review-product-details p { 9828 margin: 0; 9829 font-size: 13px; 9830 color: #6b7280; 9831} 9832 9833/* Star Rating Input */ 9834.star-rating-input { 9835 display: inline-flex; 9836 gap: 8px; 9837 margin-right: 12px; 9838 vertical-align: middle; 9839} 9840 9841.star-btn { 9842 background: none; 9843 border: none; 9844 font-size: 32px; 9845 color: #d1d5db; 9846 cursor: pointer; 9847 transition: all 0.2s; 9848 padding: 0; 9849 line-height: 1; 9850} 9851 9852.star-btn:hover, 9853.star-btn.active { 9854 color: #C10DFF; 9855 transform: scale(1.1); 9856} 9857 9858.rating-label { 9859 display: inline-block; 9860 font-size: 15px; 9861 color: #6b7280; 9862 margin: 0; 9863 vertical-align: middle; 9864 min-width: 150px; 9865} 9866 9867/* Form Elements */ 9868.review-form .form-group { 9869 margin-bottom: 20px; 9870} 9871 9872.review-form .form-label { 9873 display: block; 9874 font-size: 14px; 9875 font-weight: 600; 9876 color: #374151; 9877 margin-bottom: 8px; 9878} 9879 9880.review-form .form-control { 9881 width: 100%; 9882 padding: 12px; 9883 border: 1px solid #d1d5db; 9884 border-radius: 8px; 9885 font-size: 14px; 9886 font-family: inherit; 9887 transition: border-color 0.2s; 9888} 9889 9890.review-form .form-control:focus { 9891 outline: none; 9892 border-color: #2563eb; 9893 box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 9894} 9895 9896.review-form .form-hint { 9897 font-size: 13px; 9898 color: #6b7280; 9899 margin-top: 8px; 9900} 9901 9902/* Image Upload */ 9903.btn-upload-trigger { 9904 display: inline-flex; 9905 align-items: center; 9906 gap: 8px; 9907 padding: 10px 16px; 9908 background: #f9fafb; 9909 border: 2px dashed #d1d5db; 9910 border-radius: 8px; 9911 color: #374151; 9912 cursor: pointer; 9913 font-size: 14px; 9914 font-weight: 500; 9915 transition: all 0.2s; 9916} 9917 9918.btn-upload-trigger:hover { 9919 background: #f3f4f6; 9920 border-color: #2563eb; 9921 color: #2563eb; 9922} 9923 9924.image-preview-grid { 9925 display: grid; 9926 grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); 9927 gap: 12px; 9928 margin-top: 16px; 9929} 9930 9931.image-preview-item { 9932 position: relative; 9933 aspect-ratio: 1; 9934 border-radius: 8px; 9935 overflow: hidden; 9936 border: 1px solid #e5e7eb; 9937} 9938 9939.image-preview-item img { 9940 width: 100%; 9941 height: 100%; 9942 object-fit: cover; 9943} 9944 9945.remove-image { 9946 position: absolute; 9947 top: 4px; 9948 right: 4px; 9949 background: rgba(0, 0, 0, 0.7); 9950 border: none; 9951 color: white; 9952 width: 24px; 9953 height: 24px; 9954 border-radius: 50%; 9955 cursor: pointer; 9956 display: flex; 9957 align-items: center; 9958 justify-content: center; 9959 transition: background 0.2s; 9960} 9961 9962.remove-image:hover { 9963 background: rgba(0, 0, 0, 0.9); 9964} 9965 9966/* Point Reward Info */ 9967.point-reward-info { 9968 background: linear-gradient(135deg, #fef3c7 0%, #C10DFF 100%); 9969 border: 2px solid #C10DFF; 9970 border-radius: 12px; 9971 padding: 16px; 9972 margin: 20px 0; 9973} 9974 9975.pri-header { 9976 display: flex; 9977 align-items: center; 9978 gap: 8px; 9979 font-size: 16px; 9980 font-weight: 700;
9981 color: #ae7e21; 9982 margin-bottom: 12px; 9983} 9984 9985.pri-list { 9986 list-style: none; 9987 padding: 0; 9988 margin: 0 0 12px 0; 9989} 9990 9991.pri-list li { 9992 padding: 6px 0; 9993 font-size: 14px; 9994 color: #765212; 9995 transition: opacity 0.2s; 9996} 9997 9998.pri-total { 9999 padding-top: 12px; 10000 border-top: 2px solid rgba(245,202,86,0.3); 10001 font-size: 16px; 10002 color: #ae7e21; 10003 font-weight: 600; 10004} 10005 10006.form-error { 10007 background: #fee2e2; 10008 border: 1px solid #e2a642; 10009 color: #a36422; 10010 padding: 12px; 10011 border-radius: 8px; 10012 font-size: 14px; 10013 margin-top: 16px; 10014} 10015 10016/* Buttons */ 10017.review-form .btn { 10018 padding: 12px 24px; 10019 border: none; 10020 border-radius: 8px; 10021 font-size: 15px; 10022 font-weight: 600; 10023 cursor: pointer; 10024 transition: all 0.2s; 10025} 10026 10027.review-form .btn-primary { 10028 background: #2563eb; 10029 color: white; 10030} 10031 10032.review-form .btn-primary:hover { 10033 background: #1d4ed8; 10034} 10035 10036.review-form .btn-primary:disabled { 10037 background: #94a3b8; 10038 cursor: not-allowed; 10039} 10040 10041.review-form .btn-secondary { 10042 background: #f1f5f9; 10043 color: #64748b; 10044} 10045 10046.review-form .btn-secondary:hover { 10047 background: #e2e8f0; 10048 color: #475569; 10049} 10050 10051@media (max-width: 768px) { 10052 #review-modal-overlay { padding: 0; } 10053 .review-modal { max-width: 100%; width: 100% !important; } 10054} 10055 10056/* =================================== 10057 POINT SUMMARY GRID (Profile Page) 10058 =================================== */ 10059 10060.point-summary-grid { 10061 display: grid; 10062 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 10063 gap: 12px; 10064 margin-bottom: 32px; 10065} 10066 10067.point-summary-card { 10068 background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%); 10069 border: 1px solid #e5e7eb; 10070 border-radius: 12px; 10071 padding: 16px 20px; 10072 display: flex; 10073 align-items: center; 10074 justify-content: space-between; 10075 gap: 12px; 10076 transition: all 0.2s; 10077} 10078 10079.point-summary-card:hover { 10080 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 10081 transform: translateY(-2px); 10082} 10083 10084.point-summary-card .psc-left { 10085 display: flex; 10086 flex-direction: column; 10087 gap: 2px; 10088} 10089 10090.point-summary-card .psc-icon { 10091 font-size: 24px; 10092 margin-bottom: 4px; 10093} 10094 10095.point-summary-card .psc-label { 10096 font-size: 13px; 10097 color: #6b7280; 10098 font-weight: 500; 10099 text-transform: uppercase; 10100 letter-spacing: 0.5px; 10101} 10102 10103.point-summary-card .psc-value { 10104 font-size: 26px; 10105 font-weight: 700; 10106 color: #111827; 10107 flex-shrink: 0; 10108} 10109 10110/* Point History Section */ 10111.point-history-section { 10112 margin-top: 32px; 10113} 10114 10115.point-history-section h3 { 10116 font-size: 18px; 10117 font-weight: 600; 10118 color: #111827; 10119 margin-bottom: 16px; 10120} 10121 10122.point-history-list { 10123 display: flex; 10124 flex-direction: column; 10125 gap: 12px; 10126} 10127 10128.point-history-item { 10129 display: grid; 10130 grid-template-columns: 40px 1fr auto; 10131 gap: 16px; 10132 padding: 16px; 10133 background: white; 10134 border: 1px solid #e5e7eb; 10135 border-radius: 10px; 10136 align-items: center; 10137 transition: all 0.2s; 10138} 10139 10140.point-history-item:hover { 10141 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); 10142} 10143 10144.point-history-item.credit { 10145 border-left: 3px solid #10b981; 10146} 10147 10148.point-history-item.debit { 10149 border-left: 3px solid #C10DFF; 10150} 10151 10152.point-history-item .phi-icon { 10153 width: 40px; 10154 height: 40px; 10155 display: flex; 10156 align-items: center; 10157 justify-content: center; 10158 font-size: 20px; 10159 border-radius: 50%; 10160 background: #f9fafb; 10161} 10162 10163.point-history-item.credit .phi-icon { 10164 background: #d1fae5; 10165} 10166 10167.point-history-item.debit .phi-icon { 10168 background: #fef3c7; 10169} 10170
10171.point-history-item .phi-details { 10172 display: flex; 10173 flex-direction: column; 10174 gap: 4px; 10175} 10176 10177.point-history-item .phi-desc { 10178 font-size: 15px; 10179 font-weight: 500; 10180 color: #111827; 10181} 10182 10183.point-history-item .phi-date { 10184 font-size: 13px; 10185 color: #9ca3af; 10186} 10187 10188.point-history-item .phi-amount { 10189 font-size: 18px; 10190 font-weight: 700; 10191} 10192 10193.point-history-item .phi-amount.credit { 10194 color: #10b981; 10195} 10196 10197.point-history-item .phi-amount.debit { 10198 color: #C10DFF; 10199} 10200 10201@media (max-width: 640px) { 10202 .point-summary-grid { 10203 grid-template-columns: 1fr; 10204 } 10205 10206 .point-summary-card .psc-value { 10207 font-size: 22px; 10208 } 10209 10210 .point-history-item { 10211 grid-template-columns: 36px 1fr; 10212 gap: 12px; 10213 } 10214 10215 .point-history-item .phi-amount { 10216 grid-column: 2; 10217 text-align: right; 10218 margin-top: 8px; 10219 } 10220} 10221 10222/* ââ Pending points: compact widget indicator ââ */ 10223.pbc-pending { 10224 font-size: 12px; 10225 opacity: 0.85; 10226 margin-top: 4px; 10227 color: #C10DFF; 10228} 10229 10230/* ââ Pending points: summary card ââ */ 10231.point-summary-card.psc-pending-card { 10232 background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); 10233 border-color: #C10DFF; 10234} 10235 10236.point-summary-card.psc-pending-card .psc-value { 10237 color: #daa837; 10238} 10239 10240.point-summary-card .psc-sub { 10241 font-size: 11px; 10242 color: #9ca3af; 10243 margin-top: 4px; 10244} 10245 10246/* ââ Pending points: history item ââ */ 10247.point-history-item.phi-pending { 10248 opacity: 0.75; 10249 border-left-color: #C10DFF; 10250 background: #fffbeb; 10251} 10252 10253.point-history-item.phi-pending .phi-icon { 10254 background: #fef3c7; 10255} 10256 10257.phi-pending-badge { 10258 display: inline-block; 10259 font-size: 11px; 10260 font-weight: 600; 10261 padding: 2px 7px; 10262 border-radius: 10px; 10263 background: #fef3c7; 10264 color: #ae7e21; 10265 border: 1px solid #C10DFF; 10266 margin-left: 8px; 10267 vertical-align: middle; 10268} 10269 10270/* ââ Reversed points: history item ââ */ 10271.point-history-item.phi-reversed { 10272 opacity: 0.55; 10273 border-left-color: #d1d5db; 10274 background: #f9fafb; 10275} 10276 10277.point-history-item.phi-reversed .phi-icon { 10278 background: #f3f4f6; 10279} 10280 10281.point-history-item.phi-reversed .phi-amount { 10282 text-decoration: line-through; 10283 color: #9ca3af !important; 10284} 10285 10286.phi-reversed-badge { 10287 display: inline-block; 10288 font-size: 11px; 10289 font-weight: 600; 10290 padding: 2px 7px; 10291 border-radius: 10px; 10292 background: #fee2e2; 10293 color: #a36422; 10294 border: 1px solid #e2a642; 10295 margin-left: 8px; 10296 vertical-align: middle; 10297} 10298 10299</style> 10300 10301</head> 10302<body> 10303 10304<header class="topbar"> 10305 <div class="container topbar-inner"> 10306 <div class="topbar-left"> 10307 NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif 10308 </div> 10309 <div class="topbar-right"> 10310 <a href="https://sunnypork.com/">NAGARP</a> 10311 <a href="https://sunnypork.com/">NAGARP SITUS</a> 10312 <a href="https://sunnypork.com/">Perubahan Teknologi Digital</a> 10313 <a href="https://sunnypork.com/">Pengalaman Platform Interaktif</a> 10314 </div> 10315 </div> 10316</header> 10317<nav class="navbar"> 10318 <nav class="navbar-logo" style="width: 100%;"> 10319 <div class="container nav-inner" style="justify-items: center; justify-content: center;"> 10320 <a href="https://sunnypork.com/" class="logo-mobile" title="NAGARP"> 10321 <img src="https://sunnypork.com/assets/images/logo-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="NAGARP" height="40"> 10322 </a> 10323 </div> 10324</nav> 10325 <div class="container nav-inner"> 10326 <a href="https://sunnypork.com/" class="logo" title="NAGARP"> 10327 <img src="https://sunnypork.com/assets/images/logo-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="NAGARP" height="40"> 10328 </a> 10329 10330 10331 <form class="search" method="get" action="#"> 10332 <input type="hidden" name="page" value="produk"> 10333 10334 <!-- Search Icon (Desktop) --> 10335 <button type="submit" class="search-icon-btn" id="searchBtn" aria-label="Cari"> 10336 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> 10337 <circle cx="11" cy="11" r="8"/> 10338 <path d="m21 21-4.35-4.35"/> 10339 </svg> 10340 </button> 10341 10342 <input type="text" name="q" id="searchInput" placeholder="PENCARIAN NAGARP" 10343 value="" autocomplete="off"> 10344 10345 <div class="search-suggest" id="searchSuggest" hidden></div> 10346 </form> 10347 10348 <div class="nav-actions"> 10349 <!--<button class="icon-btn" id="notifBtn" title="Notifikasi">ð</button>--> 10350 <button class="icon-btn cart-indicator" id="cartBtn" title="Keranjang"> 10351 ð <span id="cartCount" class="badge" data-role="cart-badge" aria-live="polite">0</span> 10352 </button> 10353 10354 <button class="icon-btn compare-indicator" id="compareBtn" title="Perbandingan"> 10355 âï¸ <span id="compareCount" class="badge" data-role="compare-badge" aria-live="polite">0</span> 10356 </button> 10357
10357<script src="/https://www.mothercare.co/etc.clientlibs/samsung/clientlibs/consumer/global/clientlib-common/global/js/crypto-js.min.6fa2276cf659f30cabde72a6fc4521e4.js"></script>
10357 10358 <!-- Dynamic login/logout button --> 10359 <button class="login-btn" id="loginBtn" aria-label="Masuk"> 10360 <span class="login-text">Masuk</span> 10361 <span class="login-ico" aria-hidden="true">ð</span> 10362 </button> 10363 </div> 10364 </div> 10365 10366 <div class="container" style="display: flex; align-items: center;"> 10367 10368 10369 <div class="mega-cats-wrapper"> 10370 <div class="mega-cats container" id="megaCatsScroll" style="padding-left: 5px !important;"> 10371 <a class="" href="https://sunnypork.com/"> 10372 NAGARP </a> 10373 <a class="" href="https://sunnypork.com/"> 10374 NAGARP SLOT </a> 10375 <a class="" href="https://sunnypork.com/"> 10376 NAGARP Pengalaman Platform Interaktif </a> 10377 <a class="" href="https://sunnypork.com/"> 10378 NAGARP SLOT88 </a> 10379 <a class="" href="https://sunnypork.com/"> 10380 SLOT </a> 10381 <a class="" href="https://sunnypork.com/"> 10382 Platform Interaktif </a> 10383 </div> 10384 <div class="mega-cats-fade" id="megaCatsFade"></div> 10385 </div> 10386 </div> 10387</nav> 10388 10389<!-- Panel Mega Menu - Outside navbar for full-screen --> 10390<div class="mm2-panel" data-mm2="panel" role="dialog" aria-label="Kategori"> 10391 <div class="mm2-view is-active" data-mm2="top"></div> 10392 <div class="mm2-view" data-mm2="detail"></div> 10393</div> 10394 10395<!-- Backdrop - Outside navbar for full-screen --> 10396<div class="mm2-backdrop" data-mm2="backdrop"></div> 10397 10398<!-- Mobile Bottom Navigation --> 10399<nav class="bottom-nav"> 10400 <div class="bottom-nav-inner"> 10401 <!-- Home --> 10402 <a href="/" class="bottom-nav-item" id="bottomHomeBtn" data-page="home"> 10403 <div class="bottom-nav-item-icon"> 10404 <!-- Outlined icon (default) --> 10405 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10406 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 10407 <polyline points="9 22 9 12 15 12 15 22"/> 10408 </svg> 10409 <!-- Filled icon (active) --> 10410 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10411 <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"/> 10412 <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"/> 10413 </svg> 10414 </div> 10415 <span class="bottom-nav-item-label">Home</span> 10416 </a> 10417 10418 <!-- Kategori --> 10419 <button class="bottom-nav-item" id="bottomCategoryBtn" data-mm2="trigger" aria-label="Kategori"> 10420 <div class="bottom-nav-item-icon"> 10421 <!-- Outlined icon (default) --> 10422 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10423 <line x1="3" y1="12" x2="21" y2="12"/> 10424 <line x1="3" y1="6" x2="21" y2="6"/> 10425 <line x1="3" y1="18" x2="21" y2="18"/> 10426 </svg> 10427 <!-- Filled icon (active) --> 10428 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10429 <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"/> 10430 </svg> 10431 </div> 10432 <span class="bottom-nav-item-label">Kategori</span> 10433 </button> 10434 10435 <!-- Keranjang --> 10436 <button class="bottom-nav-item" id="bottomCartBtn"> 10437 <div class="bottom-nav-item-icon"> 10438 <!-- Outlined icon (default) --> 10439 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10440 <circle cx="9" cy="21" r="1"/> 10441 <circle cx="20" cy="21" r="1"/> 10442 <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/> 10443 </svg> 10444 <!-- Filled icon (active) -->
10445 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10446 <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"/> 10447 </svg> 10448 <span class="bottom-nav-item-badge" id="bottomCartCount">0</span> 10449 </div> 10450 <span class="bottom-nav-item-label">Keranjang</span> 10451 </button> 10452 10453 <!-- Compare --> 10454 <button class="bottom-nav-item" id="bottomCompareBtn"> 10455 <div class="bottom-nav-item-icon"> 10456 <!-- Outlined icon (default) --> 10457 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10458 <polyline points="17 11 21 7 17 3"/> 10459 <path d="M21 7H9"/> 10460 <polyline points="7 21 3 17 7 13"/> 10461 <path d="M15 17H3"/> 10462 </svg> 10463 <!-- Filled icon (active) --> 10464 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10465 <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"/> 10466 </svg> 10467 <span class="bottom-nav-item-badge" id="bottomCompareCount" style="display:none;">0</span> 10468 </div> 10469 <span class="bottom-nav-item-label">Compare</span> 10470 </button> 10471 10472 <!-- Chat - Always show, check login in JavaScript --> 10473 <button class="bottom-nav-item" id="bottomChatBtn" data-page="chat"> 10474 <div class="bottom-nav-item-icon"> 10475 <!-- Outlined icon (default) --> 10476 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" opacity="0.85"> 10477 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 10478 </svg> 10479 <!-- Filled icon (active) --> 10480 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10481 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/> 10482 </svg> 10483 <span class="bottom-nav-item-badge" id="bottomChatCount" style="display:none;">0</span> 10484 </div> 10485 <span class="bottom-nav-item-label">Chat</span> 10486 </button> 10487 10488 <!-- Akun --> 10489 <button class="bottom-nav-item" id="bottomLoginBtn"> 10490 <div class="bottom-nav-item-icon"> 10491 <!-- Outlined icon (default) --> 10492 <svg class="icon-outlined" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 10493 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 10494 <circle cx="12" cy="7" r="4"/> 10495 </svg> 10496 <!-- Filled icon (active) --> 10497 <svg class="icon-filled" viewBox="0 0 24 24" fill="currentColor"> 10498 <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"/> 10499 </svg> 10500 </div> 10501 <span class="bottom-nav-item-label">Login</span> 10502 </button> 10503 </div> 10504</nav> 10505 10506<!-- Mobile User Menu Bottom Sheet --> 10507
10508<script nonce="se3iyzEHMc9FQEB4wSyd0w=="> 10509(function(){ 10510 const btn = document.getElementById('megaMoreBtn'); 10511 const dd = document.getElementById('megaMoreDropdown'); 10512 if(!btn || !dd) return; 10513 10514 function openDD(){ 10515 dd.hidden = false; 10516 btn.setAttribute('aria-expanded','true'); 10517 document.addEventListener('click', outside, {capture:true}); 10518 document.addEventListener('keydown', esc); 10519 } 10520 function closeDD(){ 10521 if(dd.hidden) return; 10522 dd.hidden = true; 10523 btn.setAttribute('aria-expanded','false'); 10524 document.removeEventListener('click', outside, {capture:true}); 10525 document.removeEventListener('keydown', esc); 10526 } 10527 function toggle(){ dd.hidden ? openDD() : closeDD(); } 10528 function outside(e){ 10529 if(!dd.contains(e.target) && e.target !== btn){ 10530 closeDD(); 10531 } 10532 } 10533 function esc(e){ 10534 if(e.key === 'Escape'){ 10535 closeDD(); 10536 btn.focus(); 10537 } 10538 } 10539 btn.addEventListener('click', toggle); 10540})(); 10541 10542// ========== HORIZONTAL SCROLL FADE INDICATOR ========== 10543(function(){ 10544 const megaCats = document.getElementById('megaCatsScroll'); 10545 const fade = document.getElementById('megaCatsFade'); 10546 if (!megaCats || !fade) return; 10547 10548 function updateFade() { 10549 const scrollLeft = megaCats.scrollLeft; 10550 const scrollWidth = megaCats.scrollWidth; 10551 const clientWidth = megaCats.clientWidth; 10552 const maxScroll = scrollWidth - clientWidth; 10553 10554 // Hide fade when scrolled to end (within 10px tolerance) 10555 if (maxScroll - scrollLeft <= 10) { 10556 fade.style.opacity = '0'; 10557 } else { 10558 fade.style.opacity = '1'; 10559 } 10560 } 10561 10562 // Update on scroll 10563 megaCats.addEventListener('scroll', updateFade, { passive: true }); 10564 10565 // Update on resize 10566 window.addEventListener('resize', updateFade, { passive: true }); 10567 10568 // Initial check 10569 setTimeout(updateFade, 100); 10570})(); 10571 10572document.addEventListener('DOMContentLoaded', function () { 10573 var input = document.getElementById('searchInput'); 10574 if (!input) return; 10575 input.addEventListener('keydown', function (e) { 10576 if (e.key === 'Enter') { 10577 e.preventDefault(); 10578 var form = input.form; 10579 if (!form) return; 10580 if (typeof form.requestSubmit === 'function') form.requestSubmit(); 10581 else form.submit(); 10582 } 10583 }); 10584}); 10585 10586 10587(function(){ 10588 const cartBtn = document.getElementById('cartBtn'); 10589 if (cartBtn) { 10590 cartBtn.addEventListener('click', function(e) { 10591 e.preventDefault(); 10592 10593 const checkLogin = () => { 10594 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10595 }; 10596 10597 if (checkLogin()) { 10598 window.location.href = '/keranjang'; 10599 } else { 10600 showCartLoginModal(); 10601 } 10602 }); 10603 } 10604 10605 const loginBtn = document.getElementById('loginBtn'); 10606 if (loginBtn) { 10607 loginBtn.addEventListener('click', function(e) { 10608 e.preventDefault(); 10609 window.location.href = 'https://sunnypork.com/'; 10610 }); 10611 } 10612 10613 const userMenuBtn = document.getElementById('userMenuBtn'); 10614 const userDropdown = document.getElementById('userDropdown'); 10615 10616 if (userMenuBtn && userDropdown) { 10617 function openDropdown() { 10618 userDropdown.hidden = false; 10619 userMenuBtn.setAttribute('aria-expanded', 'true'); 10620 document.addEventListener('click', closeOnOutside, { capture: true }); 10621 document.addEventListener('keydown', closeOnEscape); 10622 } 10623 10624 function closeDropdown() { 10625 if (userDropdown.hidden) return; 10626 userDropdown.hidden = true; 10627 userMenuBtn.setAttribute('aria-expanded', 'false'); 10628 document.removeEventListener('click', closeOnOutside, { capture: true }); 10629 document.removeEventListener('keydown', closeOnEscape); 10630 } 10631 10632 function closeOnOutside(e) { 10633 if (!userDropdown.contains(e.target) && e.target !== userMenuBtn) { 10634 closeDropdown(); 10635 } 10636 } 10637 10638 function closeOnEscape(e) { 10639 if (e.key === 'Escape') { 10640 closeDropdown(); 10641 userMenuBtn.focus(); 10642 } 10643 } 10644 10645 userMenuBtn.addEventListener('click', function(e) { 10646 e.preventDefault(); 10647 userDropdown.hidden ? openDropdown() : closeDropdown(); 10648 });
10649 10650 // Close dropdown when clicking profile links 10651 userDropdown.querySelectorAll('[data-close-dropdown]').forEach(link => { 10652 link.addEventListener('click', function() { 10653 closeDropdown(); 10654 }); 10655 }); 10656 } 10657 10658 // Bottom nav profile button - toggle mobile bottom sheet 10659 const bottomProfileBtn = document.getElementById('bottomProfileBtn'); 10660 const mobileUserSheet = document.getElementById('mobileUserSheet'); 10661 const mobileUserSheetBackdrop = document.getElementById('mobileUserSheetBackdrop'); 10662 10663 if (bottomProfileBtn && mobileUserSheet) { 10664 function openMobileSheet() { 10665 mobileUserSheet.hidden = false; 10666 document.body.style.overflow = 'hidden'; 10667 // Trigger animation 10668 requestAnimationFrame(() => { 10669 mobileUserSheet.classList.add('active'); 10670 }); 10671 } 10672 10673 function closeMobileSheet() { 10674 mobileUserSheet.classList.remove('active'); 10675 document.body.style.overflow = ''; 10676 setTimeout(() => { 10677 mobileUserSheet.hidden = true; 10678 }, 300); 10679 } 10680 10681 bottomProfileBtn.addEventListener('click', function(e) { 10682 e.preventDefault(); 10683 openMobileSheet(); 10684 }); 10685 10686 if (mobileUserSheetBackdrop) { 10687 mobileUserSheetBackdrop.addEventListener('click', closeMobileSheet); 10688 } 10689 10690 // Close on escape key 10691 document.addEventListener('keydown', function(e) { 10692 if (e.key === 'Escape' && !mobileUserSheet.hidden) { 10693 closeMobileSheet(); 10694 } 10695 }); 10696 10697 // Close sheet when clicking profile links 10698 mobileUserSheet.querySelectorAll('[data-close-sheet]').forEach(link => { 10699 link.addEventListener('click', function() { 10700 closeMobileSheet(); 10701 }); 10702 }); 10703 } 10704 10705 // Handle Google avatar error in bottom-nav (fallback to initials) 10706 const bottomNavAvatar = document.querySelector('.bottom-nav-avatar-img'); 10707 if (bottomNavAvatar) { 10708 bottomNavAvatar.addEventListener('error', function() { 10709 const initials = this.getAttribute('data-fallback-initials') || 'U'; 10710 const initialsDiv = document.createElement('div'); 10711 initialsDiv.className = 'bottom-nav-avatar-initials'; 10712 initialsDiv.textContent = initials; 10713 this.parentNode.replaceChild(initialsDiv, this); 10714 }); 10715 } 10716 10717 // Mobile logout button 10718 const mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); 10719 if (mobileLogoutBtn) { 10720 mobileLogoutBtn.addEventListener('click', async function(e) { 10721 e.preventDefault(); 10722 10723 // Check push notification before logout 10724 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10725 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10726 if (!confirmLogout) { 10727 return; // User cancelled logout 10728 } 10729 } 10730 10731 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10732 10733 try { 10734 const response = await fetch('/auth/logout', { 10735 method: 'POST', 10736 credentials: 'same-origin', 10737 headers: { 10738 'Content-Type': 'application/json', 10739 'X-CSRF-Token': getCsrfToken() 10740 }, 10741 body: JSON.stringify({ 10742 csrf_token: getCsrfToken() 10743 }) 10744 }); 10745 10746 if (response.ok) { 10747 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10748 window.guestCart.clear(); 10749 } 10750 window.location.href = '/login?logged_out=1'; 10751 } else { 10752 console.error('Logout failed'); 10753 window.location.href = '/logout'; 10754 } 10755 } catch (error) { 10756 console.error('Logout error:', error); 10757 window.location.href = '/logout'; 10758 } 10759 }); 10760 } 10761 10762 const logoutBtn = document.getElementById('logoutBtn'); 10763 if (logoutBtn) { 10764 logoutBtn.addEventListener('click', async function(e) { 10765 e.preventDefault(); 10766 10767 // Check push notification before logout 10768 if (window.ChatNotification && typeof window.ChatNotification.confirmLogoutWithPushNotification === 'function') { 10769 const confirmLogout = await window.ChatNotification.confirmLogoutWithPushNotification(); 10770 if (!confirmLogout) { 10771 return; // User cancelled logout 10772 } 10773 } 10774 10775 const getCsrfToken = () => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; 10776
10777 try { 10778 const response = await fetch('/auth/logout', { 10779 method: 'POST', 10780 credentials: 'same-origin', 10781 headers: { 10782 'Content-Type': 'application/json', 10783 'X-CSRF-Token': getCsrfToken() 10784 }, 10785 body: JSON.stringify({ 10786 csrf_token: getCsrfToken() 10787 }) 10788 }); 10789 10790 if (response.ok) { 10791 if (window.guestCart && typeof window.guestCart.clear === 'function') { 10792 window.guestCart.clear(); 10793 } 10794 10795 window.location.href = '/login?logged_out=1'; 10796 } else { 10797 console.error('Logout failed'); 10798 window.location.href = '/logout'; 10799 } 10800 } catch (error) { 10801 console.error('Logout error:', error); 10802 window.location.href = '/logout'; 10803 } 10804 }); 10805 } 10806 10807 const wishlistBtn = document.getElementById('wishlistBtn'); 10808 const notifBtn = document.getElementById('notifBtn'); 10809 10810 if (wishlistBtn) { 10811 wishlistBtn.addEventListener('click', function(e) { 10812 e.preventDefault(); 10813 alert('Fitur wishlist akan segera tersedia!'); 10814 }); 10815 } 10816 10817 if (notifBtn) { 10818 notifBtn.addEventListener('click', function(e) { 10819 e.preventDefault(); 10820 alert('Fitur notifikasi akan segera tersedia!'); 10821 }); 10822 } 10823})(); 10824 10825// ========== MOBILE BOTTOM NAVIGATION ========== 10826(function initBottomNav() { 10827 // Sync cart badge with bottom nav 10828 function syncBottomCartBadge() { 10829 const mainBadge = document.getElementById('cartCount'); 10830 const bottomBadge = document.getElementById('bottomCartCount'); 10831 if (mainBadge && bottomBadge) { 10832 const count = mainBadge.textContent; 10833 bottomBadge.textContent = count; 10834 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10835 } 10836 } 10837 10838 // Sync compare badge with bottom nav 10839 function syncBottomCompareBadge() { 10840 const mainBadge = document.getElementById('compareCount'); 10841 const bottomBadge = document.getElementById('bottomCompareCount'); 10842 if (mainBadge && bottomBadge) { 10843 const count = mainBadge.textContent; 10844 bottomBadge.textContent = count; 10845 bottomBadge.style.display = parseInt(count) > 0 ? 'flex' : 'none'; 10846 } 10847 } 10848 10849 // Initial sync 10850 syncBottomCartBadge(); 10851 syncBottomCompareBadge(); 10852 10853 // Observe badge changes 10854 const observer = new MutationObserver(function() { 10855 syncBottomCartBadge(); 10856 syncBottomCompareBadge(); 10857 }); 10858 10859 const cartBadge = document.getElementById('cartCount'); 10860 const compareBadge = document.getElementById('compareCount'); 10861 10862 if (cartBadge) observer.observe(cartBadge, { childList: true, characterData: true, subtree: true }); 10863 if (compareBadge) observer.observe(compareBadge, { childList: true, characterData: true, subtree: true }); 10864 10865 // Bottom Cart Button 10866 const bottomCartBtn = document.getElementById('bottomCartBtn'); 10867 if (bottomCartBtn) { 10868 bottomCartBtn.addEventListener('click', function(e) { 10869 e.preventDefault(); 10870 10871 const checkLogin = () => { 10872 return window.isUserLoggedIn ? window.isUserLoggedIn() : false; 10873 }; 10874 10875 if (checkLogin()) { 10876 window.location.href = '/keranjang'; 10877 } else { 10878 showCartLoginModal(); 10879 } 10880 }); 10881 } 10882 10883 // Bottom Compare Button 10884 const bottomCompareBtn = document.getElementById('bottomCompareBtn'); 10885 if (bottomCompareBtn) { 10886 bottomCompareBtn.addEventListener('click', function(e) { 10887 e.preventDefault(); 10888 10889 // Close chat widget if open 10890 if (window.plazaChat && window.plazaChat.isWidgetOpen) { 10891 window.plazaChat.closeWidget(); 10892 } 10893 10894 const mainCompareBtn = document.getElementById('compareBtn'); 10895 if (mainCompareBtn) { 10896 mainCompareBtn.click(); 10897 } 10898 }); 10899 } 10900 10901 // Bottom Category Button - triggers mega menu 10902 const bottomCategoryBtn = document.getElementById('bottomCategoryBtn'); 10903 if (bottomCategoryBtn) { 10904 bottomCategoryBtn.addEventListener('click', function(e) { 10905 e.preventDefault(); 10906 // Trigger desktop mega menu 10907 const desktopTrigger = document.querySelector('.mm2-trigger:not(#bottomCategoryBtn)'); 10908 if (desktopTrigger) { 10909 desktopTrigger.click(); 10910 } 10911 });
10912 } 10913 10914 // Bottom Login Button 10915 const bottomLoginBtn = document.getElementById('bottomLoginBtn'); 10916 if (bottomLoginBtn) { 10917 bottomLoginBtn.addEventListener('click', function(e) { 10918 e.preventDefault(); 10919 window.location.href = '/login'; 10920 }); 10921 } 10922 10923 // Bottom Chat Button 10924 const bottomChatBtn = document.getElementById('bottomChatBtn'); 10925 if (bottomChatBtn) { 10926 bottomChatBtn.addEventListener('click', function(e) { 10927 e.preventDefault(); 10928 10929 // Trigger chat widget (try multiple times if needed) 10930 const triggerChat = () => { 10931 const chatToggle = document.getElementById('chat-toggle-btn'); 10932 if (chatToggle) { 10933 chatToggle.click(); 10934 return true; 10935 } 10936 return false; 10937 }; 10938 10939 // Immediate attempt 10940 if (!triggerChat()) { 10941 // Retry after 100ms if not found 10942 setTimeout(() => { 10943 if (!triggerChat()) { 10944 // Last resort: manually show popup 10945 const popup = document.getElementById('chat-popup'); 10946 if (popup) { 10947 popup.style.display = 'block'; 10948 } 10949 } 10950 }, 100); 10951 } 10952 }); 10953 } 10954 10955 // Sync chat unread badge 10956 function syncBottomChatBadge() { 10957 const chatBadge = document.getElementById('chat-unread-badge'); 10958 const bottomChatBadge = document.getElementById('bottomChatCount'); 10959 if (chatBadge && bottomChatBadge) { 10960 const count = chatBadge.textContent; 10961 bottomChatBadge.textContent = count; 10962 bottomChatBadge.style.display = chatBadge.style.display; 10963 } 10964 } 10965 10966 // Observe chat badge changes 10967 const chatBadge = document.getElementById('chat-unread-badge'); 10968 if (chatBadge) { 10969 observer.observe(chatBadge, { attributes: true, childList: true, characterData: true, subtree: true }); 10970 syncBottomChatBadge(); 10971 } 10972 10973 // Active state management based on current page 10974 function updateActiveState() { 10975 const currentPath = window.location.pathname; 10976 const bottomNavItems = document.querySelectorAll('.bottom-nav-item'); 10977 10978 bottomNavItems.forEach(item => { 10979 item.classList.remove('active'); 10980 10981 const itemId = item.id; 10982 const page = item.getAttribute('data-page'); 10983 const href = item.getAttribute('href'); 10984 10985 // Check by ID for specific pages 10986 if (itemId === 'bottomHomeBtn' && currentPath === '/') { 10987 item.classList.add('active'); 10988 } else if (itemId === 'bottomCartBtn' && (currentPath === '/keranjang' || currentPath === '/cart')) { 10989 item.classList.add('active'); 10990 } else if (itemId === 'bottomProfileBtn' && currentPath.startsWith('/profile')) { 10991 item.classList.add('active'); 10992 } else if (itemId === 'bottomCategoryBtn' && currentPath.startsWith('/kategori')) { 10993 item.classList.add('active'); 10994 } else if (itemId === 'bottomCompareBtn' && currentPath === '/compare') { 10995 item.classList.add('active'); 10996 } else if (itemId === 'bottomChatBtn' && currentPath === '/chat') { 10997 item.classList.add('active'); 10998 } 10999 // Fallback: check by data-page attribute 11000 else if (page === 'home' && currentPath === '/') { 11001 item.classList.add('active'); 11002 } else if (page === 'profile' && currentPath.startsWith('/profile')) { 11003 item.classList.add('active'); 11004 } 11005 // Fallback: check by href 11006 else if (href && currentPath === href) { 11007 item.classList.add('active'); 11008 } 11009 }); 11010 } 11011 11012 // Update active state on load 11013 updateActiveState(); 11014 11015 // Bottom nav always visible - auto-hide disabled for better UX 11016 // const bottomNav = document.querySelector('.bottom-nav'); 11017 // if (bottomNav && window.innerWidth <= 640) { 11018 // // Auto-hide feature disabled 11019 // } 11020})(); 11021// Cart Login Modal Handler (Global scope) 11022function showCartLoginModal() { 11023 const modal = document.getElementById('cartLoginModal'); 11024 if (!modal) return; 11025 11026 modal.classList.add('show'); 11027 11028 const confirmBtn = document.getElementById('cartLoginConfirm'); 11029 const registerBtn = document.getElementById('cartRegisterBtn');
11030 const cancelBtn = document.getElementById('cartLoginCancel'); 11031 11032 const handleLogin = () => { 11033 window.location.href = '/login?return=' + encodeURIComponent('/keranjang'); 11034 }; 11035 11036 const handleRegister = () => { 11037 window.location.href = '/register?return=' + encodeURIComponent('/keranjang'); 11038 }; 11039 11040 const handleCancel = () => { 11041 modal.classList.remove('show'); 11042 confirmBtn.removeEventListener('click', handleLogin); 11043 registerBtn.removeEventListener('click', handleRegister); 11044 cancelBtn.removeEventListener('click', handleCancel); 11045 }; 11046 11047 confirmBtn.removeEventListener('click', handleLogin); 11048 registerBtn.removeEventListener('click', handleRegister); 11049 cancelBtn.removeEventListener('click', handleCancel); 11050 11051 confirmBtn.addEventListener('click', handleLogin); 11052 registerBtn.addEventListener('click', handleRegister); 11053 cancelBtn.addEventListener('click', handleCancel); 11054 11055 modal.addEventListener('click', (e) => { 11056 if (e.target === modal) { 11057 handleCancel(); 11058 } 11059 }); 11060} 11061</script>
vendor: 2 bytes, line 11061
11061
11062<script data-inlined="cart-badge.js"> 11063(function(){ 11064 function selBadge(){ 11065 return document.querySelector('[data-role="cart-badge"]') || document.getElementById('cartCount'); 11066 } 11067 function setBadge(n){ 11068 const el = selBadge(); 11069 if (!el) return; 11070 const count = Math.max(0, parseInt(n||0,10) || 0); 11071 el.textContent = String(count); 11072 // Sembunyikan jika 0? Jika ingin selalu terlihat, hapus 2 baris di bawah 11073 if (count <= 0) { el.classList.add('is-empty'); } 11074 else { el.classList.remove('is-empty'); } 11075 } 11076 async function refresh(){ 11077 try{ 11078 // Primary: Use server-side endpoint (handles both logged and guest users) 11079 const res = await fetch('/api/cart/get-simple.php', { credentials:'same-origin' }); 11080 if (res.ok) { 11081 const data = await res.json(); 11082 if (data && data.ok) { 11083 setBadge(data.items_count || 0); 11084 return; 11085 } 11086 } 11087 11088 // Fallback hanya untuk guest user: check localStorage 11089 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 11090 document.body.classList.contains('logged-in') || 11091 window.userLoggedIn === true; 11092 11093 if (!isLoggedIn && window.guestCart && typeof window.guestCart.getItemsCount === 'function') { 11094 setBadge(window.guestCart.getItemsCount()); 11095 } 11096 }catch(e){ 11097 // Final fallback hanya untuk guest 11098 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true'; 11099 if (!isLoggedIn) { 11100 try { 11101 const guestCartData = JSON.parse(localStorage.getItem('plazait_guest_cart') || '{"items":{}}'); 11102 const items = guestCartData.items || {}; 11103 const count = Object.values(items).reduce((total, item) => total + (item.qty || 0), 0); 11104 setBadge(count); 11105 } catch(e2) { 11106 setBadge(0); 11107 } 11108 } else { 11109 setBadge(0); 11110 } 11111 } 11112 } 11113 11114 // Ekspos untuk dipanggil dari skrip lain (opsional) 11115 window.CartBadge = { set: setBadge, refresh }; 11116 window.updateCartCounter = refresh; // Alias for compatibility 11117 11118 // Update saat halaman load 11119 if (document.readyState === 'loading') { 11120 document.addEventListener('DOMContentLoaded', refresh); 11121 } else { 11122 refresh(); 11123 } 11124 11125 // Dengarkan event global dari add/remove/update 11126 document.addEventListener('cart:updated', function(ev){ 11127 const c = ev && ev.detail && typeof ev.detail.items_count === 'number' 11128 ? ev.detail.items_count : null; 11129 if (c !== null) setBadge(c); 11130 else refresh(); 11131 }); 11132 11133 // Dengarkan event khusus cart remove untuk update badge 11134 document.addEventListener('cart:removed', function(ev){ 11135 // Langsung refresh untuk memastikan data terbaru 11136 refresh(); 11137 }); 11138 11139 // Jika addToCart sudah ada sebelum file ini, coba wrap agar badge ter-update. 11140 const prevAdd = window.addToCart; 11141 if (typeof prevAdd === 'function'){ 11142 window.addToCart = async function(pid, qty, vid, meta){ 11143 const res = await prevAdd(pid, qty, vid, meta); 11144 try{ 11145 const cnt = res && res.cart ? res.cart.items_count : null; 11146 if (cnt !== null) setBadge(cnt); 11147 // tetap siarkan event agar listener lain ikut sync 11148 document.dispatchEvent(new CustomEvent('cart:updated', { detail: res.cart || {} })); 11149 }catch(_){} 11150 return res; 11151 }; 11152 } 11153 11154 // Helper function to check if user is logged in 11155 function isUserLoggedIn() { 11156 const metaTag = document.querySelector('meta[name="user-logged-in"]'); 11157 return metaTag ? metaTag.getAttribute('content') === 'true' : false; 11158 } 11159 11160 // Make login check globally accessible 11161 window.isUserLoggedIn = isUserLoggedIn; 11162})(); 11163</script>
11163 11164 11165<!-- Product Compare Sidebar --> 11166<div class="compare-sidebar" id="compareSidebar"> 11167 <div class="compare-header"> 11168 <h4 class="compare-title"> 11169 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 11170 <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"/> 11171 </svg> 11172 Bandingkan Produk 11173 <span class="compare-count" id="compareCountSidebar">(0)</span> 11174 </h4> 11175 <button class="compare-close" id="compareClose" aria-label="Tutup"> 11176 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11177 <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"/> 11178 </svg> 11179 </button> 11180 </div> 11181 11182 <div class="compare-content"> 11183 <div class="compare-empty" id="compareEmpty"> 11184 <div class="empty-icon"> 11185 <svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor" opacity="0.3"> 11186 <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"/> 11187 </svg> 11188 </div> 11189 <h5>Belum ada produk dipilih</h5> 11190 <p>Pilih produk yang ingin dibandingkan dengan klik tombol "Bandingkan Produk" di halaman produk.</p> 11191 </div> 11192 11193 <div class="compare-list" id="compareList"> 11194 <!-- Products will be populated here --> 11195 </div> 11196 </div> 11197 11198 <div class="compare-actions" id="compareActions"> 11199 <button class="btn btn-outline-danger btn-sm" id="compareClearAll"> 11200 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11201 <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"/> 11202 </svg> 11203 Hapus Semua 11204 </button> 11205 <button class="btn btn-primary btn-sm" id="compareAnalyze" disabled> 11206 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 11207 <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"/> 11208 </svg> 11209 Analisis AI 11210 </button> 11211 </div> 11212 11213 <!-- AI Disclaimer in Sidebar --> 11214 <div class="compare-disclaimer"> 11215 <div class="ai-disclaimer"> 11216 <div class="disclaimer-text"> 11217 <strong>ð¡ Catatan:</strong> Analisis ini dihasilkan secara otomatis dan dapat memiliki keterbatasan. 11218 Gunakan sebagai referensi tambahan dengan tetap mempertimbangkan kebutuhan Anda. 11219 Untuk informasi lebih lanjut, silakan hubungi tim NAGARP. 11220 </div> 11221 </div> 11222 </div> 11223</div><style data-inlined="pd-lightbox.css"> 11224/* PlazaIT Product Lightbox (vanilla, no CDN) â v4 */ 11225.pd-lb { 11226 position: fixed; inset: 0; z-index: 99999; 11227 background: rgba(0,0,0,.95); 11228 display: none; 11229 color: #fff; 11230 /* Transition untuk swipe-down feedback */ 11231 transition: transform .18s ease, opacity .18s ease; 11232} 11233.pd-lb.show { display: block; } 11234 11235/* Canvas di bawah (z:1), Controls di atas (z:2) */ 11236.pd-lb-canvas { 11237 position: absolute; inset: 56px 0 56px 0; 11238 display: flex; align-items: center; justify-content: center; 11239 overflow: hidden; 11240 touch-action: none; 11241 z-index: 1; 11242} 11243 11244.pd-lb-img { 11245 max-width: 96vw; max-height: 86vh; 11246 will-change: transform; 11247 user-select: none; 11248 pointer-events: none; 11249 transform-origin: 50% 50%; 11250} 11251 11252.pd-lb-close, .pd-lb-prev, .pd-lb-next, .pd-lb-counter { 11253 position: absolute; 11254 z-index: 2; 11255} 11256 11257.pd-lb-close, .pd-lb-prev, .pd-lb-next { 11258 background: rgba(17,24,39,.7); 11259 color: #fff; border: 0; cursor: pointer; 11260 width: 40px; height: 40px; border-radius: 999px; 11261 display: inline-flex; align-items: center; justify-content: center; 11262 transition: background-color .15s ease, transform .05s ease; 11263 -webkit-tap-highlight-color: transparent; 11264} 11265.pd-lb-close:hover, .pd-lb-prev:hover, .pd-lb-next:hover { background: rgba(31,41,55,.85); } 11266.pd-lb-close:active, .pd-lb-prev:active, .pd-lb-next:active { transform: translateY(.5px); } 11267 11268.pd-lb-close { top: max(12px, env(safe-area-inset-top, 12px)); right: max(12px, env(safe-area-inset-right, 12px)); } 11269.pd-lb-prev { top: 50%; left: 10px; transform: translateY(-50%); } 11270.pd-lb-next { top: 50%; right: 10px; transform: translateY(-50%); } 11271 11272.pd-lb-counter { 11273 top: max(16px, env(safe-area-inset-top, 16px)); left: 50%; 11274 transform: translateX(-50%); 11275 font-weight: 700; font-size: 13px; letter-spacing: .4px;
11276 background: rgba(17,24,39,.65); 11277 padding: 6px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.1); 11278} 11279 11280@media (max-width: 640px){ 11281 .pd-lb-img { max-width: 98vw; max-height: 80vh; } 11282 .pd-lb-prev, .pd-lb-next { width: 36px; height: 36px; } 11283 .pd-lb-close { width: 38px; height: 38px; } 11284} 11285</style> 11286<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;cursor: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(255,255,255,.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(255,255,255,.8);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:2px solid rgba(245,202,86,.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(245,202,86,.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:#C10DFF;margin-bottom:2px}
11286.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,#C10DFF,#C10DFF);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(245,202,86,.25)}.pey-link:hover{transform:translateX(2px);box-shadow:0 4px 12px rgba(245,202,86,.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> 11287<style nonce="se3iyzEHMc9FQEB4wSyd0w=="> 11288.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;display: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} 11289 11290.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)}} 11291 11292.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} 11293 11294.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} 11295 11296.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)} 11297 11298.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,#e2a642 0%,#c9832a 100%);color:#fff}.pd-share-icon-copy{background:linear-gradient(135deg,#C10DFF 0%,#C10DFF 100%);color:#fff}.pd-share-icon-pdf{background:linear-gradient(135deg,#e2a642 0%,#e2a642 100%);color:#fff}.pd-share-name{font-size:12px;font-weight:600;color:#475569;text-align:center} 11299 11300.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)}
11300.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} 11301 11302@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}} 11303 11304.mf-share{background:#fff;color:#C10DFF;border:2px solid #C10DFF;display:inline-flex;align-items:center;justify-content:center}.mf-share:active{transform:scale(.95)}.mf-share svg{display:block} 11305 11306@media(max-width:640px){.pd-share-btn{display:none!important}} 11307 11308.stock-badge.in{background:#e6f9ec;color:#15803d;border-radius:999px;padding:4px 10px;font-weight:700}.stock-badge.out{background:#e2a642;color:#14b1d7;border-radius:999px;padding:4px 10px;font-weight:700} 11309 11310.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} 11311 11312.stock-popover{position:absolute;z-index:9999;min-width:280px;max-width:92vw;background:#0b1220;color:#eef3ff;border:1px solid rgba(255,255,255,.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:#0b1220;border-left:1px solid rgba(255,255,255,.08);border-top:1px solid rgba(255,255,255,.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} 11313 11314.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(255,255,255,.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} 11315 11316 11317.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} 11318 11319.sp-wa{ 11320 grid-column:3; justify-self:end; 11321 width:32px; height:32px; min-width:32px; min-height:32px; 11322 box-sizing:border-box; 11323 padding:0; line-height:0; text-decoration:none; outline:0; 11324 display:inline-flex; align-items:center; justify-content:center; 11325 border-radius:999px; border:1px solid rgba(37,211,102,.60); /* 1px untuk hasil âcrispâ */ 11326 background:rgba(37,211,102,.08); color:#25D366; 11327 transition:background-color .15s ease, border-color .15s ease, transform .05s ease; 11328 -webkit-tap-highlight-color:transparent; 11329} 11330.sp-wa:hover{background:rgba(37,211,102,.16);border-color:rgba(37,211,102,.75)} 11331.sp-wa:active{transform:translateY(.5px) scale(.98)} 11332 11333.sp-wa svg{width:18px;height:18px;display:block;vertical-align:middle} 11334.stock-popover a{text-decoration:none} 11335 11336#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}} 11337 11338.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:#C10DFF}.stock-dot.out{background:#e2a642}.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}} 11339 11340.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}} 11341 11342.pwp-trigger-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 4px;background:linear-gradient(135deg,#C10DFF 0%,#C10DFF 100%);color:#765212;border-radius:6px;font-size:.65rem;font-weight:600;border:0}.pwp-trigger-badge svg{flex-shrink:0;width:12px;height:12px} 11343.pwp-deals-section{margin:0;padding:12px;background:linear-gradient(135deg,#fef3c7 0%,#C10DFF 100%);border-radius:12px;border:2px solid #C10DFF}.pwp-deals-header{display:flex;gap:10px;margin-bottom:10px}.pwp-icon{flex-shrink:0;color:#C10DFF;width:18px;height:18px}.pwp-text .pwp-title{margin:0 0 2px 0;font-size:14px;font-weight:700;color:#765212}
11343.pwp-text .pwp-subtitle{margin:0;font-size:11px;color:#ae7e21;opacity:0.9}.pwp-deals-loading{text-align:center;padding:40px 0;color:#ae7e21}.pwp-spinner{width:40px;height:40px;border:3px solid #C10DFF;border-top-color:#C10DFF;border-radius:50%;animation:spin 0.8s linear infinite;margin:0 auto 12px}@keyframes spin{to{transform:rotate(360deg)}} 11344.pwp-deals-grid{display:flex;flex-direction:column;gap:6px}.pwp-deal-card{background:#fff;border-radius:6px;overflow:hidden;border:1px solid #C10DFF;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:#e2a642;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:#e2a642;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} 11345@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}} 11346 11347.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:#e2a642;font-size:14px}.preorder-info-note{margin-top:10px;padding:8px 10px;background:#fef3c7;border-left:2px solid #C10DFF;font-size:12px;color:#765212;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}} 11348</style> 11349 11350<div class="pd-sticky-header" id="pd-sticky-header"><div class="pd-sticky-header-container"><div class="pd-sticky-thumb"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP" id="pd-sticky-thumb-img"></div><div class="pd-sticky-info"><h2 class="pd-sticky-title" id="pd-sticky-title">NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif</h2><div class="pd-sticky-price-row"><span class="pd-sticky-price" id="pd-sticky-price">Rp 20.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">
11350<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> 11351 11352<main class="product-single product-single--v3"><nav class="pd-breadcrumb" aria-label="breadcrumb"><a href="https://sunnypork.com/">NAGARP</a><span>-</span><a href="https://sunnypork.com/">NAGARP ONLINE</a><span>-</span><a href="https://sunnypork.com/">NAGARP SITUS</a><span>-</span><a href="https://sunnypork.com/">NAGARP SLOT</a><span>-</span><a href="https://sunnypork.com/">NAGARP SLOT88</a><span>-</span><span class="current">NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif 11353 11354</span></nav> 11355 11356<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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="Teknologi Digital NAGARP" data-slug="" loading="eager"> 11357</figure> 11358 11359<div class="pd-mobile-slider-wrapper"> 11360<div class="pd-mobile-slider" id="pd-mobile-slider" aria-label="Galeri gambar (geser)"><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="eager"></div><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></div><div class="pd-mslide"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></div></div><div class="pd-gallery-counter" id="pd-gallery-counter">1/6</div></div> 11361<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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt="NAGARP ONLINE" data-slug="" aria-label="Gambar 1"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt
11361="NAGARP ONLINE" data-slug="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" aria-label="Gambar 2"><img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt="NAGARP 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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt="NAGARP 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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt="NAGARP 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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE" loading="lazy"></button><button type="button" class="pd-thumb-btn " data-full="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" data-alt="NAGARP 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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP 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"> 11362 <h1 class="pd-title" id="pd-title">NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif</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> 11363<div id="pd-price-anchor"> 11364</div> 11365<div class="pd-price-qr-container"> 11366 <div class="pd-price-block" id="pd-price-block"> 11367 <div class="pd-price-row"> 11368 <span class="pd-price-current" id="pd-price-current" >Rp 20.000</span> 11369 <span class="pd-price-original" id="pd-price-original">Rp 50.000</span> 11370 <span class="pd-discount-badge" id="pd-discount-badge">-90%</span></div> 11371 <div class="pd-meta-row"> 11372 <span class="sold-inline" id="pd-sold-inline" data-role="sold-chip">Terjual Terjual 5</span> 11373 <span class="pwp-trigger-badge" id="pwp-trigger-badge" hidden> 11374 <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"> 11375 <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
11375-.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> 11376 <div class="pd-meta-inline"><span class="stock-badge out" id="pd-stock-badge">NAGARP</span> 11377 <span class="sku">SKU: <span id="pd-sku">NAGARP SITUS</span></span></div> 11378<div class="paylater-promo paylater-promo-sm paylater-mobile-only"> 11379 <span class="paylater-icon">ð³</span> 11380 <span class="paylater-text"> 11381 <strong>NAGARP</strong> 11382 <small>ONLINE</small> 11383 </span> 11384</div> 11385 11386<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"><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"> 11387<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> 11388 11389 <li class="sp-item"> 11390 <div class="sp-left"> 11391 <span class="sp-dot" style="--dot:#34d399;"></span> 11392 <span class="sp-label">Medan Fair</span> 11393 </div> 11394 <span class="sp-qty" id="sp-qty-mdf">0</span> 11395 <a class="sp-wa" 11396 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" 11397 target="_blank" rel="noopener" aria-label="WhatsApp Medan Fair"> 11398 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11399 <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"/> 11400 </svg> 11401 </a> 11402 </li> 11403 11404 <li class="sp-item"> 11405 <div class="sp-left">
11406 <span class="sp-dot" style="--dot:#C10DFF;"></span> 11407 <span class="sp-label">Banda Aceh</span> 11408 </div> 11409 <span class="sp-qty" id="sp-qty-bda">0</span> 11410 <a class="sp-wa" 11411 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" 11412 target="_blank" rel="noopener" aria-label="WhatsApp Banda Aceh"> 11413 <svg class="ico-wa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"> 11414 <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"/> 11415 </svg> 11416 </a> 11417 </li> 11418 </ul> 11419 </div> 11420 11421 </div> 11422 <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> 11423 </div> 11424 <div id="pd-variants-anchor"></div> 11425 11426 11427 11428 <div class="pd-compare-section"> 11429 <button type="button" class="pd-compare-btn" id="pd-compare-btn" data-product-id="325"> 11430 <svg class="compare-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11431 <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/> 11432 </svg> 11433 <span class="compare-text">Bandingkan Produk</span> 11434 <span class="compare-remove" hidden> 11435 <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"> 11436 <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"/> 11437 </svg> 11438 Hapus dari Perbandingan 11439 </span> 11440 </button> 11441 <div class="pd-compare-info"> 11442 <small class="compare-note" id="compare-note">Bandingkan hingga 5 produk dengan bantuan AI</small> 11443 </div> 11444 </div> 11445 11446 <div class="pd-short-desc"><p><strong>Informasi NAGARP:</strong></p><ul><li>Minimal Deposit: Rp.10.000,-</li><li>Jenis Permainan: NAGARP Pengalaman Platform Interaktif</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: 399.999.999</li><li>Rating: âââââ</li><li>Daftar Buruan: <a href="https://sunny-401.sunnypork.com/">Klik Disini</a></li></ul></div><div class="Perubahan-Teknologi-Digital-v1 mini"> 11447<div class="Perubahan-Teknologi-Digital"> 11448 11449 <!-- HEADER --> 11450<div class="Perubahan-Teknologi-Digital-v3 mini"> 11451 <div class="Pengalaman-Platform-Interaktif-v3"> 11452 <h2 class="NAGARP-title-v3">ð¯ PREDIKSI NAGARP RESMI (SIMULASI) ð¯</h2> 11453
11454 <div class="NAGARP-controls-v3"> 11455 <label> 11456 PASARAN 11457 <select id="marketFilter"> 11458 <option value="ALL">ALL PASARAN</option> 11459 </select> 11460 </label> 11461 11462 <label> 11463 TIPE 11464 <select id="typeFilter"> 11465 <option value="4D">4D</option> 11466 <option value="3D">3D</option> 11467 <option value="2D">2D</option> 11468 </select> 11469 </label> 11470 11471 <label> 11472 MIN CONF 11473 <input id="minConf" type="number" min="50" max="99" step="1" value="70"> 11474 </label> 11475 11476 <button id="btnNAGARPRandom" type="button"> 11477 <span class="icon">ð²</span> GENERATE 11478 </button> 11479 </div> 11480 11481 <div class="NAGARP-main-result-v3" id="NAGARPResult"> 11482 <div class="NAGARP-status-v3" id="NAGARPStatus">â READY â</div> 11483 11484 <div class="NAGARP-highlight-box"> 11485 <div class="NAGARP-col-left"> 11486 <div class="k">Pasaran Rekomendasi</div> 11487 <div class="v-game" id="rMarket">-</div> 11488 11489 <div class="provider-info"> 11490 <span class="k-small">Tipe:</span> 11491 <span class="v-small" id="rType">-</span> 11492 <span class="k-small" style="margin-left:10px;">Conf:</span> 11493 <span class="v-small" id="rConf">-</span> 11494 </div> 11495 </div> 11496 11497 <div class="NAGARP-col-right"> 11498 <div class="k-rtp">Angka Utama</div> 11499 <div class="v-rtp-huge" id="rAngka">-</div> 11500 </div> 11501 </div> 11502 11503 <div class="NAGARP-details-v3"> 11504 <div class="NAGARP-detail-item"> 11505 <div class="k-detail">BBFS</div> 11506 <div class="v-detail" id="rBBFS">-</div> 11507 </div> 11508 <div class="NAGARP-detail-item"> 11509 <div class="k-detail">Angka Ikut</div> 11510 <div class="v-detail" id="rIkut">-</div> 11511 </div> 11512 <div class="NAGARP-detail-item"> 11513 <div class="k-detail">Shio</div> 11514 <div class="v-detail" id="rShio">-</div> 11515 </div> 11516 </div> 11517 11518 <div class="NAGARP-tip-v3" id="rTip"> 11519 Tips: Ini simulasi hiburan. Tetap bijak & disiplin. 11520 </div> 11521 </div> 11522 11523 <div class="NAGARP-alts-v3"> 11524 <div class="NAGARP-alts-title-v3">3 Alternatif Angka</div> 11525 <div class="NAGARP-alt-list-v3" id="NAGARPAltList"></div> 11526 </div> 11527 11528 <div class="NAGARP-note-v3"> 11529 Disclaimer: Hasil di halaman ini random (simulasi), bukan acuan hasil resmi. 11530 </div> 11531 </div> 11532 </div> 11533 11534 <style> 11535 @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@600;700;800&display=swap"); 11536 11537 /* =============================== 11538 WRAPPER 11539================================ */ 11540 .Perubahan-Teknologi-Digital-v3 { 11541 display: flex; 11542 justify-content: center; 11543 padding: 16px 10px; 11544 font-family: "Orbitron", "Poppins", system-ui, sans-serif; 11545 } 11546 11547 /* =============================== 11548 MACHINE CORE (#c9832a ELEGANT) 11549================================ */ 11550 .Pengalaman-Platform-Interaktif-v3 { 11551 position: relative; 11552 width: 100%; 11553 max-width: 520px; 11554 padding: 16px; 11555 border-radius: 16px; 11556 overflow: hidden; 11557 isolation: isolate; 11558 background: radial-gradient(circle at 15% 0%, rgba(163,100,34,1), transparent 48%), radial-gradient(circle at 85% 100%, rgba(201,131,42,0.18), transparent 55%), linear-gradient(123deg, #000000, #000000); 11559 border: 1px solid rgba(201,131,42,1); 11560 box-shadow: inset 0 0 18px rgba(201,131,42,1), 0 0 18px rgba(201,131,42,1); 11561 color: #dbdbdb; 11562 } 11563 11564 /* HUD SWEEP */ 11565 .Pengalaman-Platform-Interaktif-v3::before { 11566 content: ""; 11567 position: absolute; 11568 inset: 0; 11569 background: linear-gradient(120deg, transparent 40%, rgba(226,166,66,.10), transparent 65%); 11570 animation: hudSweep 7s linear infinite; 11571 pointer-events: none; 11572 opacity: .8; 11573 } 11574 11575 /* =============================== 11576 TITLE 11577================================ */ 11578 .NAGARP-title-v3 { 11579 text-align: center; 11580 margin: 0 0 12px; 11581 font-size: 15px; 11582 font-weight: 900; 11583 letter-spacing: 1px; 11584 text-shadow: 0 0 6px rgba(163,100,34,90%);
11585 } 11586 11587 /* =============================== 11588 CONTROLS 11589================================ */ 11590 .NAGARP-controls-v3 { 11591 display: grid; 11592 grid-template-columns: 1fr 1fr; 11593 gap: 10px; 11594 padding: 12px; 11595 border-radius: 14px; 11596 11597 background: rgba(0, 0, 0, 0.4); 11598 border: 1px solid rgba(226,166,66,0.45); 11599 box-shadow: inset 0 0 14px rgba(201,131,42,22%); 11600 } 11601 11602 .NAGARP-controls-v3 label { 11603 display: grid; 11604 gap: 6px; 11605 font-size: 10px; 11606 letter-spacing: .8px; 11607 text-transform: uppercase; 11608 color: #c9832a; 11609 } 11610 11611 .NAGARP-controls-v3 select, 11612 .NAGARP-controls-v3 input { 11613 background: rgba(0 0 0 / 92%); 11614 color: #c5c5c5; 11615 border: 1px solid rgba(201,131,42,1); 11616 border-radius: 10px; 11617 padding: 7px 9px; 11618 font-weight: 700; 11619 font-size: 12px; 11620 outline: none; 11621 box-shadow: inset 0 0 10px rgba(201,131,42,1); 11622 } 11623 11624 /* BUTTON */ 11625 #btnNAGARPRandom { 11626 grid-column: span 2; 11627 display: flex; 11628 align-items: center; 11629 justify-content: center; 11630 gap: 8px; 11631 11632 background: linear-gradient(145deg, #c9832a, #c9832a); 11633 border: none; 11634 color: #000000; 11635 font-weight: 900; 11636 letter-spacing: .8px; 11637 border-radius: 14px; 11638 cursor: pointer; 11639 padding: 10px 12px; 11640 font-size: 12px; 11641 11642 box-shadow: 0 0 16px rgba(226,166,66,0.75); 11643 transition: .2s ease; 11644 } 11645 11646 #btnNAGARPRandom:active { 11647 transform: scale(.98); 11648 } 11649 11650 #btnNAGARPRandom:disabled { 11651 opacity: .75; 11652 cursor: not-allowed; 11653 } 11654 11655 /* =============================== 11656 MAIN RESULT 11657================================ */ 11658 .NAGARP-main-result-v3 { 11659 margin-top: 12px; 11660 padding: 12px; 11661 border-radius: 14px; 11662 11663 background: rgba(18,12,8,0.78); 11664 border: 1px solid rgba(226,166,66,1); 11665 box-shadow: inset 0 0 16px rgba(226,166,66,1); 11666 } 11667 11668 .NAGARP-status-v3 { 11669 text-align: center; 11670 font-weight: 800; 11671 letter-spacing: .8px; 11672 color: #c9832a; 11673 margin-bottom: 10px; 11674 font-size: 11px; 11675 } 11676 11677 .NAGARP-highlight-box { 11678 display: flex; 11679 justify-content: space-between; 11680 gap: 12px; 11681 padding-bottom: 10px; 11682 margin-bottom: 10px; 11683 border-bottom: 1px dashed rgba(226,166,66,0.3); 11684 } 11685 11686 .k, 11687 .k-rtp { 11688 font-size: 10px; 11689 color: #c7c7c7; 11690 text-transform: uppercase; 11691 } 11692 11693 .v-game { 11694 font-size: 14px; 11695 font-weight: 900; 11696 color: #ffffff; 11697 margin-top: 6px; 11698 line-height: 1.2; 11699 } 11700 11701 .provider-info { 11702 margin-top: 8px; 11703 font-size: 11px; 11704 color: #d9d9d9; 11705 opacity: .95; 11706 } 11707 11708 .k-small { 11709 color: #c9832a; 11710 font-size: 10px; 11711 } 11712 11713 .v-small { 11714 color: #fff0f0; 11715 font-weight: 900; 11716 } 11717 11718 .v-rtp-huge { 11719 font-size: 2.4em; 11720 font-weight: 900; 11721 color: #e2a642; 11722 text-shadow: 0 0 10px rgba(0 0 0); 11723 line-height: 1; 11724 letter-spacing: 1px; 11725 } 11726 11727 /* DETAILS */ 11728 .NAGARP-details-v3 { 11729 display: grid; 11730 grid-template-columns: repeat(3, 1fr); 11731 gap: 8px; 11732 background: rgba(0, 0, 0, .30); 11733 padding: 10px; 11734 border-radius: 12px; 11735 } 11736 11737 .k-detail { 11738 font-size: 9px; 11739 color: #c9832a; 11740 text-transform: uppercase; 11741 } 11742 11743 .v-detail { 11744 font-size: 12px; 11745 font-weight: 800; 11746 color: #ffffff; 11747 margin-top: 4px; 11748 letter-spacing: .8px; 11749 } 11750 11751 .NAGARP-tip-v3 { 11752 margin-top: 10px; 11753 font-size: 10px; 11754 color: #d9d9d9; 11755 opacity: .9; 11756 text-align: center; 11757 } 11758 11759 /* ALTS */ 11760 .NAGARP-alts-v3 { 11761 margin-top: 12px; 11762 padding: 12px; 11763 border-radius: 14px; 11764 background: rgba(0, 0, 0, .40); 11765 border: 1px solid rgba(226,166,66,1); 11766 } 11767 11768 .NAGARP-alts-title-v3 { 11769 font-weight: 900; 11770 color: #c9832a; 11771 margin-bottom: 10px; 11772 font-size: 12px; 11773 } 11774 11775 .NAGARP-alt-list-v3 { 11776 display: grid; 11777 grid-template-columns: 1fr; 11778 gap: 8px; 11779 } 11780 11781 .NAGARP-alt-item { 11782 background: rgba(18,12,8,.86); 11783 border-radius: 12px; 11784 padding: 10px; 11785 border-left: 3px solid rgba(163,100,34,90%);
11786 } 11787 11788 .NAGARP-alt-item .g { 11789 font-weight: 900; 11790 color: #c5c5c5; 11791 font-size: 12px; 11792 } 11793 11794 .NAGARP-alt-item .p { 11795 font-size: 10px; 11796 color: #c9832a; 11797 margin-top: 4px; 11798 } 11799 11800 .NAGARP-alt-item .r { 11801 margin-top: 6px; 11802 font-weight: 900; 11803 color: #e2a642; 11804 letter-spacing: .8px; 11805 font-size: 12px; 11806 } 11807 11808 .NAGARP-note-v3 { 11809 margin-top: 10px; 11810 font-size: 10px; 11811 color: #d9d9d9; 11812 opacity: .85; 11813 text-align: center; 11814 } 11815 11816 /* MOBILE */ 11817 @media(max-width:420px) { 11818 .NAGARP-highlight-box { 11819 flex-direction: column 11820 } 11821 11822 .NAGARP-details-v3 { 11823 grid-template-columns: 1fr 11824 } 11825 } 11826 11827 /* ANIM */ 11828 @keyframes hudSweep { 11829 0% { 11830 transform: translateX(-110%) 11831 } 11832 11833 100% { 11834 transform: translateX(110%) 11835 } 11836 } 11837 11838 @keyframes pulse { 11839 0% { 11840 opacity: .55 11841 } 11842 11843 50% { 11844 opacity: 1 11845 } 11846 11847 100% { 11848 opacity: .55 11849 } 11850 } 11851 </style> 11852 11853
11853<script> 11854 /* =============================== 11855 DATASET NAGARP (SIMULASI) 11856 ================================ */ 11857 const NAGARP_MARKETS = [ 11858 { market: "Singapore", code: "SGP", open: "17:45", zone: "WIB" }, 11859 { market: "Hongkong", code: "HK", open: "22:45", zone: "WIB" }, 11860 { market: "Sydney", code: "SDY", open: "13:45", zone: "WIB" }, 11861 { market: "Macau", code: "MC", open: "16:00", zone: "WIB" }, 11862 { market: "Cambodia", code: "KHM", open: "10:45", zone: "WIB" }, 11863 { market: "Japan", code: "JP", open: "11:30", zone: "WIB" } 11864 ]; 11865 11866 const SHIO = ["Tikus", "Kerbau", "Macan", "Kelinci", "Naga", "Ular", "Kuda", "Kambing", "Monyet", "Ayam", "Anjing", "Babi"]; 11867 11868 const $ = (q) => document.querySelector(q); 11869 const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; 11870 11871 function uniqueMarkets() { 11872 return NAGARP_MARKETS.map(m => m.market); 11873 } 11874 11875 function genDigits(len) { 11876 let s = ""; 11877 for (let i = 0; i < len; i++) s += String(randInt(0, 9)); 11878 return s; 11879 } 11880 11881 function genBBFS() { 11882 const pool = [...Array(10)].map((_, i) => i); 11883 const out = []; 11884 for (let i = 0; i < 7; i++) { 11885 const idx = randInt(0, pool.length - 1); 11886 out.push(pool.splice(idx, 1)[0]); 11887 } 11888 return out.join(""); 11889 } 11890 11891 function genIkut() { 11892 const pool = [...Array(10)].map((_, i) => i); 11893 const out = []; 11894 for (let i = 0; i < 4; i++) { 11895 const idx = randInt(0, pool.length - 1); 11896 out.push(pool.splice(idx, 1)[0]); 11897 } 11898 return out.join(" ⢠"); 11899 } 11900 11901 function simulateConfidence() { 11902 return randInt(60, 96); 11903 } 11904 11905 function applyFilters() { 11906 const m = $("#marketFilter").value; 11907 const minC = parseFloat($("#minConf").value || "0"); 11908 11909 return NAGARP_MARKETS 11910 .filter(x => (m === "ALL" || x.market === m)) 11911 .map(x => ({ ...x, conf: simulateConfidence() })) 11912 .filter(x => x.conf >= minC); 11913 } 11914 11915 function pickRandom(arr, n = 1) { 11916 const copy = [...arr]; 11917 const out = []; 11918 for (let i = 0; i < n && copy.length; i++) { 11919 const idx = randInt(0, copy.length - 1); 11920 out.push(copy.splice(idx, 1)[0]); 11921 } 11922 return out; 11923 } 11924 11925 function formatAngka(type) { 11926 if (type === "4D") return genDigits(4); 11927 if (type === "3D") return genDigits(3); 11928 return genDigits(2); 11929 } 11930 11931 function renderMain(item, type) { 11932 $("#rMarket").textContent = `${item.market} (${item.code}) ⢠Open ${item.open} ${item.zone}`; 11933 $("#rType").textContent = type; 11934 $("#rConf").textContent = `${item.conf}%`; 11935 11936 $("#rAngka").textContent = formatAngka(type); 11937 $("#rBBFS").textContent = genBBFS(); 11938 $("#rIkut").textContent = genIkut(); 11939 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 11940 $("#rTip").textContent = "Tips: Ini simulasi hiburan. Tetap bijak & disiplin."; 11941 } 11942 11943 function renderAlts(list, type) { 11944 const wrap = $("#NAGARPAltList"); 11945 wrap.innerHTML = ""; 11946 list.forEach(x => { 11947 const el = document.createElement("div"); 11948 el.className = "NAGARP-alt-item"; 11949 el.innerHTML = ` 11950 <div class="g">${x.market} (${x.code})</div> 11951 <div class="p">Tipe ${type} ⢠Conf <strong>${x.conf}%</strong></div> 11952 <div class="r">Angka: <strong>${formatAngka(type)}</strong></div> 11953 `; 11954 wrap.appendChild(el); 11955 }); 11956 } 11957 11958 function spinButton(btn, on) { 11959 if (on) { 11960 btn.disabled = true; 11961 btn.dataset.text = btn.innerHTML; 11962 btn.innerHTML = `<span class="icon">ð</span> PROCESS...`; 11963 } else { 11964 btn.disabled = false; 11965 btn.innerHTML = btn.dataset.text || `<span class="icon">ð²</span> GENERATE`; 11966 } 11967 } 11968 11969 /* =============================== 11970 INIT 11971 ================================ */
11972 (function init() { 11973 const sel = $("#marketFilter"); 11974 uniqueMarkets().forEach(m => { 11975 const o = document.createElement("option"); 11976 o.value = m; o.textContent = m; 11977 sel.appendChild(o); 11978 }); 11979 11980 $("#btnNAGARPRandom").addEventListener("click", (e) => { 11981 const btn = e.currentTarget; 11982 const type = $("#typeFilter").value; 11983 const filtered = applyFilters(); 11984 const st = $("#NAGARPStatus"); 11985 11986 if (!filtered.length) { 11987 st.textContent = "â NO MATCH â"; 11988 st.style.color = "white"; 11989 renderAlts([], type); 11990 $("#rMarket").textContent = "-"; 11991 $("#rType").textContent = type; 11992 $("#rConf").textContent = "-"; 11993 $("#rAngka").textContent = "-"; 11994 $("#rBBFS").textContent = "-"; 11995 $("#rIkut").textContent = "-"; 11996 $("#rShio").textContent = "-"; 11997 return; 11998 } 11999 12000 spinButton(btn, true); 12001 st.textContent = "â GENERATING â"; 12002 st.style.animation = "pulse .6s linear infinite"; 12003 st.style.color = "white"; 12004 12005 const t0 = setInterval(() => { 12006 const temp = pickRandom(filtered, 1)[0]; 12007 $("#rMarket").textContent = `${temp.market} (${temp.code}) ⢠Open ${temp.open} ${temp.zone}`; 12008 $("#rType").textContent = type; 12009 $("#rConf").textContent = `${temp.conf}%`; 12010 $("#rAngka").textContent = formatAngka(type); 12011 $("#rBBFS").textContent = genBBFS(); 12012 $("#rIkut").textContent = genIkut(); 12013 $("#rShio").textContent = SHIO[randInt(0, SHIO.length - 1)]; 12014 }, 85); 12015 12016 setTimeout(() => { 12017 clearInterval(t0); 12018 12019 const picks = pickRandom(filtered, 4); 12020 renderMain(picks[0], type); 12021 renderAlts(picks.slice(1), type); 12022 12023 st.textContent = "â READY â"; 12024 st.style.animation = ""; 12025 st.style.color = "gold"; 12026 spinButton(btn, false); 12027 }, 1200); 12028 }); 12029 12030 renderAlts([], "4D"); 12031 })(); 12032 </script>
12032 12033
12034<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: 2 bytes, line 12034
12034
12035<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: 2 bytes, line 12035
12035
12036<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: 2 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":"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: 2 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":"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: 2 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":"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: 2 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":"51df3b17a746455f940c0b561e8c0c0b","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
12040 12041 12042<style> 12043body{background:#ebf2fc; font-family:Poppins,sans-serif; color:white;} 12044.Perubahan-Teknologi-Digital{ 12045max-width: 520px; 12046 margin: auto; 12047 padding: 15px; 12048 border-radius: 12px; 12049 background: #a36422; 12050 border: 2px solid #c9832a; 12051 box-shadow: 0 0 20px #e2a642; 12052} 12053 12054.NAGARP-header{ 12055 font-weight:900; 12056 text-align:center; 12057 color:#e2a642; 12058 text-shadow:0 0 12px #5f90d9; 12059 margin-bottom:12px; 12060} 12061 12062.NAGARP-controls{ 12063 display:grid; 12064 grid-template-columns:1fr 1fr; 12065 gap:10px; 12066 margin-bottom:12px; 12067} 12068.NAGARP-controls label{ 12069 display:flex; flex-direction:column; 12070 font-size:11px; color:#e2a642; 12071} 12072.NAGARP-controls select, 12073.NAGARP-controls input{ 12074 padding:8px; 12075 border-radius:8px; 12076 border:1px solid #e2a642; 12077 background:#120809; 12078 color:white; 12079} 12080#btnGenerate{ 12081 grid-column:span 2; 12082 padding:12px; 12083 background:#5f90d9; 12084 border:none; 12085 color:black; 12086 font-weight:900; 12087 cursor:pointer; 12088 margin-top:5px; 12089 box-shadow:0 0 15px #e2a642; 12090} 12091 12092.NAGARP-result{ 12093 margin-top:12px; 12094 padding:12px; 12095 border-radius:10px; 12096 background:rgba(226,166,66,0.05); 12097 border:1px solid #e2a642; 12098} 12099 12100.NAGARP-main div, .NAGARP-details div{ 12101 margin:5px 0; 12102} 12103 12104#rPrediction{ 12105 font-size:24px; 12106 font-weight:900; 12107 color:#5f90d9; 12108} 12109 12110.NAGARP-alts{ 12111 margin-top:10px; 12112} 12113.NAGARP-alts h4{margin-bottom:6px;} 12114#NAGARPAltList div{ 12115 background:#120809; 12116 padding:6px 10px; 12117 margin:3px 0; 12118 border-radius:8px; 12119 border-left:3px solid #5f90d9; 12120} 12121 .bank-tab.active, .bank-tab:hover { 12122 background: #e2a642; 12123 color: #fff; 12124 border-color: #e2a642; 12125 box-shadow: 0 3px 10px -4px rgb(226 166 66 / 60%); 12126} 12127 12128.NAGARP-disclaimer{ 12129 font-size:10px; 12130 margin-top:8px; 12131 text-align:center; 12132 color:#e2a642; 12133} 12134</style> 12135
12136<script> 12137const leagues = ["Premier League","La Liga","Serie A"]; 12138const tips = ["Analisis statistik tim","Performa terakhir","Cek head-to-head"]; 12139function rand(arr){return arr[Math.floor(Math.random()*arr.length)];} 12140 12141var _bgBtn=document.getElementById("btnGenerate");if(_bgBtn)_bgBtn.onclick=function(){ 12142 const type = document.getElementById("betTypeFilter").value; 12143 const league = rand(leagues); 12144 12145 let pred; 12146 if(type==="1X2") pred=rand(["1","X","2"]); 12147 else if(type==="OVER_UNDER") pred=rand(["Over","Under"]); 12148 else pred=rand(["Home -1","Away +1","Draw"]); 12149 12150 document.getElementById("rLeague").innerText=league; 12151 document.getElementById("rPrediction").innerText=pred; 12152 document.getElementById("rBetType").innerText=type; 12153 document.getElementById("rConf").innerText=Math.floor(Math.random()*40+60)+"%"; 12154 document.getElementById("rTip").innerText=rand(tips); 12155 document.getElementById("rOdds").innerText=(Math.random()*3+1).toFixed(2); 12156 12157 // alternatif 12158 const alt=document.getElementById("NAGARPAltList"); alt.innerHTML=""; 12159 for(let i=0;i<3;i++){let d=document.createElement("div"); d.innerText=rand(["1","X","2"]); alt.appendChild(d);} 12160 12161 document.getElementById("NAGARPStatus").innerText="â READY â"; 12162} 12163</script>
12163 12164 12165 12166 12167 <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> 12168 </div> 12169 </div> 12170 </section> 12171 12172 <aside class="pd-sidebar"> 12173 <div class="pd-side-card pd-buy-card"> 12174 <form class="pd-buy-form" id="pd-buy-form" data-pid="325"> 12175 <input type="hidden" name="variant_id" id="pd-variant-id" value="0"> 12176 <div> 12177 <label for="qty" class="qty-label">Qty</label> 12178 <input type="number" id="qty" name="qty" value="0" min="1" 12179 max="0" 12180 disabled> 12181 </div> 12182 <div class="pd-actions"> 12183 <button type="submit" class="btn-add-cart" id="btn-add-cart" disabled>+ Keranjang</button> 12184 <button type="button" class="btn-buy" id="btn-buy" data-id="278426" disabled >Beli</button> 12185 <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> 12186 </div> 12187 </form> 12188 <div class="paylater-promo paylater-promo-md paylater-desktop-only"> 12189 <span class="paylater-icon">ð³</span> 12190 <span class="paylater-text"> 12191 <strong>Autorized NAGARP</strong> 12192 <small>Situs Resmi NAGARP </small> 12193 </span> 12194</div> </div> 12195 12196 12197 <div class="pd-side-card qoala-banner-card"> 12198 <div class="qoala-banner"> 12199 <div class="qoala-banner-content"> 12200 <div class="qoala-icon"> 12201 <center><img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="NAGARP" class="qoala-shield-img"></center> 12202 </div> 12203 <div class="qoala-text"> 12204 <div class="qoala-title">Login NAGARP</div> 12205 <div class="qoala-subtitle">Verified</div> 12206 </div> 12207 </div> 12208 <a href="https://sunny-401.sunnypork.com/" class="qoala-cta" 12209 data-ga-event="qoala_banner_click" 12210 data-product-id="325" 12211 data-product-name="NAGARP" 12212 data-category-path="Komputer & Laptop/Laptop/Gaming" target="_blank">Info</a> 12213 </div> 12214<style> 12215.qoala-banner-card{padding:0;overflow:hidden} 12216 12217.qoala-banner{ 12218 background:linear-gradient(135deg, #c9832a 0%, #c9832a 100%); 12219 padding:12px 16px; 12220 display:flex; 12221 align-items:center; 12222 justify-content:space-between; 12223 color:white; 12224 text-decoration:none; 12225 min-height:60px; 12226} 12227 12228.qoala-banner-content{ 12229 display:flex; 12230 align-items:center; 12231 gap:12px; 12232} 12233 12234.qoala-icon{ 12235 display:flex; 12236 align-items:center; 12237 justify-content:center; 12238} 12239 12240.qoala-shield-img{ 12241 width:36px; 12242 height:36px; 12243 object-fit:contain; 12244 opacity:0.9; 12245} 12246 12247.qoala-title{ 12248 font-weight:600; 12249 font-size:14px; 12250 line-height:1.2; 12251} 12252 12253.qoala-subtitle{ 12254 font-size:12px; 12255 opacity:0.9; 12256 line-height:1.2; 12257} 12258 12259.qoala-cta{ 12260 background:rgba(255,255,255,0.15); 12261 color:white; 12262 padding:6px 12px; 12263 border-radius:6px; 12264 font-size:12px; 12265 font-weight:600; 12266 text-decoration:none; 12267 transition:background 0.2s ease; 12268 border:1px solid rgba(226,166,66,0.4); 12269} 12270 12271.qoala-cta:hover{ 12272 background:rgba(226,166,66,0.25); 12273 color:white; 12274} 12275 12276@media(max-width:768px){ 12277 .qoala-banner{padding:10px 12px} 12278 .qoala-title{font-size:17px} 12279 .qoala-subtitle{font-size:11px} 12280} 12281</style> 12282 </div> 12283 12284 <div class="pd-side-card pd-installment-card2" 12285 id="inst-card" 12286 data-base-price="20789000" 12287 data-zero-smart="0"> 12288 <div class="inst-head"> 12289 <h3>Promo & Fitur NAGARP</h3> 12290 </div> 12291 12292 <div class="inst-bank-tabs" role="tablist" aria-label="Bank"> 12293 <button type="button" class="bank-tab active" data-bank="Lucky Draw" aria-selected="true">Lucky Draw</button> 12294 <button type="button" class="bank-tab" data-bank="Lomba Panjat Tur
12294nover" aria-selected="false">Lomba Panjat Turnover</button> 12295 <button type="button" class="bank-tab" data-bank="Prediksi Jitu" aria-selected="false">Prediksi Jitu</button> 12296 </div> 12297 12298 <ul class="inst-month-list" id="inst-month-list" aria-label="Daftar cicilan per bulan"></ul> 12299 <div class="inst-note" id="inst-zero-note" style="display:none;">0% Cicilan khusus bank terpilih*</div> 12300 </div> 12301 12302 <!-- MECHA BLUE FINAL BOSS REVIEW SLIDER --> 12303<div class="tnt-review-mecha"> 12304 12305<div class="tnt-header"> 12306<h2>REVIEW PEMAIN NAGARP</h2> 12307 12308</div> 12309 12310<div class="tnt-slider" id="tntSlider"> 12311 12312<div class="tnt-slide active"> 12313 <div class="tnt-name">Perubahan Teknologi Digital â Batam</div> 12314 <div class="tnt-comment">"NAGARP X Sunny Pork berhasil menghadirkan nuansa baru lewat integrasi teknologi digital yang membuat platform interaktif terasa lebih hidup dan mudah digunakan.</div> 12315</div> 12316 12317<div class="tnt-slide"> 12318 <div class="tnt-name">Pengalaman Platform Interaktif â Pekanbaru</div> 12319 <div class="tnt-comment">"Saya cukup impressed dengan cara NAGARP X Sunny Pork membentuk pengalaman platform interaktif menggunakan teknologi digital terkini tanpa ribet."</div> 12320</div> 12321 12322<div class="tnt-slide"> 12323 <div class="tnt-name">Teknologi Digital â Jakarta</div> 12324 <div class="tnt-comment">"Platform NAGARP X Sunny Pork terasa modern berkat perubahan teknologi digital yang diterapkan secara konsisten dan user-friendly."</div> 12325</div> 12326<div class="tnt-slide"> 12327 <div class="tnt-name">Platform Interaktif â Medan</div> 12328 <div class="tnt-comment">"Review saya tentang NAGARP X Sunny Pork: pengalaman platform interaktifnya meningkat signifikan berkat pemanfaatan teknologi digital yang tepat."</div> 12329</div> 12330<div class="tnt-slide"> 12331 <div class="tnt-name">Membentuk Pengalaman Platform â Medan</div> 12332 <div class="tnt-comment">"NAGARP X Sunny Pork memberikan kesan positif karena teknologi digitalnya mampu membentuk pengalaman platform interaktif yang engaging sehari-hari."</div> 12333</div> 12334 12335 12336 12337</div> 12338</div> 12339 12340</aside> 12341</div> 12342 12343<style> 12344.tnt-review-mecha{ 12345 background: radial-gradient(circle at center, #e2a642, #2b1b0e); 12346 border: 2px solid #c9832a; 12347 border-radius: 14px; 12348 padding: 20px; 12349 color: #dbdbdb; 12350 font-family: 'Orbitron', sans-serif; 12351 position: relative; 12352 overflow: hidden; 12353 box-shadow: 0 0 25px #a36422, inset 0 0 20px rgba(226,166,66,0.2); 12354} 12355 12356/* scanline */ 12357.tnt-review-mecha::before{ 12358 content:""; 12359 position:absolute; 12360 inset:0; 12361 background: repeating-linear-gradient( 12362 to bottom, 12363 rgba(226,166,66,0.08), 12364 rgba(226,166,66,0.08) 1px, 12365 transparent 2px, 12366 transparent 4px 12367 ); 12368 pointer-events:none; 12369}.bank-tab.active, .bank-tab:hover 12370 12371 12372.tnt-header h2{ 12373 font-size: 18px; 12374 color:#000000; 12375 text-shadow:0 0 10px #e2a642, 0 0 20px #c9832a; 12376 margin-bottom:5px; 12377} 12378 12379.tnt-header p{ 12380 font-size:12px; 12381 opacity:0.85; 12382 margin-bottom:15px; 12383} 12384 12385.tnt-slider{ 12386 position:relative; 12387 height:90px; 12388} 12389 12390.tnt-slide{ 12391 position:absolute; 12392 width:100%; 12393 opacity:0; 12394 transform:translateX(40px); 12395 transition:all .6s ease; 12396} 12397 .tenor-badge { 12398 background: #e2a642; 12399 color: #fff; 12400 font-weight: 700; 12401 min-width: 42px; 12402 text-align: center; 12403 padding: .38rem .4rem; 12404 font-size: .8rem; 12405 border-radius: 10px; 12406 box-shadow: 0 3px 10px -4px rgb(226,166,66); 12407} 12408 12409.tnt-slide.active{ 12410 opacity:1; 12411 transform:translateX(0); 12412} 12413 12414.tnt-name{ 12415font-size: 14px; 12416 color: #000000; 12417 margin-bottom: 5px; 12418 text-shadow: 0 0 6px #120809; 12419} 12420 12421.tnt-comment{ 12422 font-size:13px; 12423 line-height:1.5; 12424} 12425 12426/* glowing border animation */ 12427@keyframes pulseGlow{ 12428 0%{box-shadow:0 0 10px #e2a642;} 12429 50%{box-shadow:0 0 25px #e2a642;} 12430 100%{box-shadow:0 0 10px #e2a642;} 12431} 12432 12433.tnt-review-mecha{ 12434 animation:pulseGlow 2.5s infinite; 12435} 12436</style> 12437
12438<script> 12439let slides = document.querySelectorAll('.tnt-slide'); 12440let index = 0; 12441 12442setInterval(()=>{ 12443 slides[index].classList.remove('active'); 12444 index = (index + 1) % slides.length; 12445 slides[index].classList.add('active'); 12446}, 3000); 12447</script>
12447 12448 12449 <div class="artikel-glow-KT"> 12450 12451 <div class="artikel-shine"></div> 12452 12453 <h1>NAGARP X Sunny Pork: Perubahan Teknologi Digital Membentuk Pengalaman Platform Interaktif</h1> 12454 Perubahan Teknologi Digital pada <a href="https://sunnypork.com/">NAGARP</a> X Sunny Pork membentuk pengalaman platform interaktif yang lebih responsif. Pengguna dapat merasakan evolusi layanan digital yang dinamis dan sesuai kebutuhan modern. 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: radial-gradient(circle at top left, rgba(201,131,42,1), transparent 20%), radial-gradient(circle at bottom right, rgb(43,27,14), transparent 28%), linear-gradient(135deg, #c9832a, #c9832a, #c9832a); 12469 border: 2px solid rgba(255, 255, 255, .55); 12470 box-shadow: 0 0 25px rgb(201,131,42), 0 0 55px rgb(226,166,66), 0 20px 45px rgba(0, 0, 0, .25), inset 0 0 35px rgba(163,100,34,1); 12471} 12472 12473.artikel-glow-KT:before{ 12474 content:""; 12475 position:absolute; 12476 inset:10px; 12477 border-radius:24px; 12478 border:1px solid rgba(255,255,255,.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(255,255,255,.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(201,131,42,95%), 12508 0 0 18px rgba(201,131,42,1), 12509 0 2px 8px rgba(163,100,34,1); 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(163,100,34,1); 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(255,255,255,.55); 12545 box-shadow: 12546 0 0 18px rgba(255,255,255,.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(255,255,255,.20); 12556 border:1px solid rgba(255,255,255,.45); 12557 box-shadow: 12558 inset 0 1px 0 rgba(255,255,255,.65), 12559 inset 0 0 22px rgba(255,255,255,.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(255,255,255,.35),rgba(255,255,255,.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(255,255,255,.16); 12587 color:#ffffff; 12588 line-height:1.6; 12589 box-shadow:inset 0 0 10px rgba(255,255,255,.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>â PREMIUM FAQ â </span> 12614 <h2>Pertanyaan Umum NAGARP</h2> 12615 12616 </div> 12617 12618 <div class="KT-rainbow-item"> 12619 <button type="button">Apa dampak perubahan teknologi digital pada platform interaktif NAGARP X Sunny Pork?</button> 12620 <div class="KT-rainbow-answer"> 12621 <p>Perubahan teknologi digital membantu NAGARP X Sunny Pork menciptakan pengalaman platform interaktif yang lebih responsif dan engaging bagi pengguna.</p> 12622 </div> 12623 </div> 12624 12625 <div class="KT-rainbow-item"> 12626 <button type="button">Bagaimana NAGARP X Sunny Pork memanfaatkan teknologi digital untuk platformnya?</button> 12627 <div class="KT-rainbow-answer"> 12628 <p>NAGARP X Sunny Pork mengintegrasikan teknologi digital terkini untuk membentuk pengalaman platform interaktif yang dinamis dan mudah diakses.</p> 12629 </div> 12630 </div> 12631 12632 <div class="KT-rainbow-item"> 12633 <button type="button">Apakah NAGARP X Sunny Pork menawarkan fitur interaktif berbasis teknologi digital?</button> 12634 <div class="KT-rainbow-answer"> 12635 <p>Ya, NAGARP X Sunny Pork fokus pada perubahan teknologi digital yang membentuk pengalaman platform interaktif dengan elemen visual dan navigasi canggih.</p> 12636 </div> 12637 </div> 12638 12639 <div class="KT-rainbow-item"> 12640 <button type="button">Mengapa pengalaman platform interaktif penting di NAGARP X Sunny Pork?</button> 12641 <div class="KT-rainbow-answer"> 12642 <p>Pengalaman platform interaktif di NAGARP X Sunny Pork menjadi prioritas karena perubahan teknologi digital memungkinkan interaksi yang lebih personal dan efisien.</p> 12643 </div> 12644 </div> 12645 <div class="KT-rainbow-item"> 12646 <button type="button">Apa keunggulan NAGARP X Sunny Pork dalam hal teknologi digital?</button> 12647 <div class="KT-rainbow-answer"> 12648 <p>Keunggulan NAGARP X Sunny Pork terletak pada kemampuannya memanfaatkan perubahan teknologi digital untuk menghadirkan pengalaman platform interaktif yang seamless.</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: radial-gradient(circle at 15% 10%, rgba(201,131,42,1), transparent 18%), radial-gradient(circle at 85% 15%, rgb(201,131,42), transparent 25%), radial-gradient(circle at 20% 90%, rgb(201,131,42), transparent 25%), linear-gradient(135deg, #c9832a, #a36422, #a36422, #c9832a); 12664 border: 2px solid rgba(201,131,42,1); 12665 box-shadow: 0 0 25px rgb(201,131,42), 0 0 55px rgba(226,166,66,1), inset 0 0 35px rgba(163,100,34,1); 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(255,255,255,.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(201,131,42,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(255,255,255,.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(201,131,42,1), 12712 0 0 22px rgba(201,131,42,1); 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(255,255,255,.20); 12727 border:1px solid rgba(255,255,255,.48); 12728 box-shadow: 12729 inset 0 1px 0 rgba(255,255,255,.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(255,255,255,.32),rgba(255,255,255,.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: #c9832a; 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,#c9832a, #a36422); 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(255,255,255,.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,#e2a642, #4d2e14); 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 #c9832a */ 12834 background: radial-gradient(circle at top, #14b1d7, #14b1d7 70%); 12835 12836 border-radius: 16px; 12837 border: 1px solid rgba(226,166,66,0.35); 12838 12839 box-shadow: 12840 0 0 25px rgba(163,100,34,0.35), 12841 inset 0 0 20px rgba(226,166,66,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(226,166,66,0.03), 12858 rgba(226,166,66,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 #c9832a */ 12870 color: #e2a642; 12871 12872 letter-spacing: 2px; 12873 text-shadow: 0 0 10px rgba(226,166,66,0.4); 12874 margin-bottom: 20px; 12875} 12876 12877.mecha-header h3 { 12878 text-align: center; 12879 font-size: 18px; 12880 color: #e2a642; 12881 text-shadow: 0 0 8px rgba(226,166,66,0.5); 12882 margin-bottom: 15px; 12883} 12884 12885.mecha-image img { 12886 width: 100%; 12887 border-radius: 10px; 12888 border: 1px solid rgba(226,166,66,0.4); 12889 box-shadow: 0 0 20px rgba(226,166,66,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: #e2a642; 12902 text-shadow: 0 0 6px rgba(226,166,66,0.5); 12903 font-weight: 600; 12904} 12905 12906.glow-strong { 12907 color: #e2a642; 12908 text-shadow: 12909 0 0 10px rgba(226,166,66,0.6), 12910 0 0 20px rgba(226,166,66,0.4); 12911 font-weight: 700; 12912} 12913 12914.final-line { 12915 margin-top: 18px; 12916 padding: 10px; 12917 border: 1px solid rgba(226,166,66,0.35); 12918 background: rgba(43,27,14,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://sunnypork.com/"> 12931 <div class="thumb"> 12932 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP"> 12933 <span class="mini-disc">-2%</span> </div> 12934 <div class="info"> 12935 <h3>NAGARP</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://sunnypork.com/"> 12944 <div class="thumb"> 12945 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="Teknologi Digital NAGARP"> 12946 <span class="mini-disc">-1%</span> </div> 12947 <div class="info"> 12948 <h3>NAGARP</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://sunnypork.com/"> 12957 <div class="thumb"> 12958 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE"> 12959 <span class="mini-disc">-1%</span> </div> 12960 <div class="info"> 12961 <h3>NAGARP ONLINE</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://sunnypork.com/"> 12970 <div class="thumb"> 12971 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP 4D"> 12972 <span class="mini-disc">-1%</span> </div> 12973 <div class="info"> 12974 <h3>NAGARP SITUS</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://sunnypork.com/"> 12983 <div class="thumb"> 12984 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="Teknologi Digital NAGARP 4D"> 12985 <span class="mini-disc">-1%</span> </div> 12986 <div class="info"> 12987 <h3>NAGARP LINK</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://sunnypork.com/"> 12996 <div class="thumb"> 12997 <img src="https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP ONLINE"> 12998 <span class="mini-disc">-1%</span> </div> 12999 <div class="info"> 13000 <h3>NAGARP SITUS ONLINE</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-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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg" alt="NAGARP" class="pd-share-preview-img"><div class="pd-share-preview-info"><h3 class="pd-share-preview-name">NAGARP</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://sunnypork.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:#0b1220;color:#eef3ff;border:1px solid rgba(255,255,255,.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 #C10DFF}.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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg"},{"url":"https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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-634.webp"},{"url":"https://sunnypork.com/assets/images/nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.jpg","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-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].forEac
13538h(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://sunnypork.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: 2 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: 2 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: 2 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);
15318 // 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
15410) : 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: 2 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: 2 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: 2 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: 2 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://sunnypork.com/" 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 NAGARP</h4> 16874 <ul class="ft-links"> 16875 <li><a href="https://sunnypork.com/">Tentang NAGARP</a></li> 16876 <li><a href="https://sunnypork.com/">Kontak NAGARP</a></li> 16877 <li><a href="https://sunnypork.com/">Lokasi Toko</a></li> 16878 <li><a href="https://sunnypork.com/">FAQ</a></li> 16879 </ul> 16880 <div class="ft-address"> 16881 <p class="company">NAGARP</p> 16882 <p>JL. Cengkareng Barat No. 56, Jakarta Timur</p> 16883 <p>Kec. Jakarta, Kota Jakarta, DKI JAKARTA, 11752</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>NAGARP © <span id="yearNow">2026</span></span> 16926 <span>NAGARP OFFICIAL. 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, #C10DFF 0%, #C10DFF 100%); 16967 border: none; 16968 color: white; 16969 cursor: pointer; 16970 box-shadow: 0 4px 12px rgba(245,202,86,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(245,202,86,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: #e2a642; 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, #C10DFF 0%, #C10DFF 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: #e2a642; 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: #C10DFF; 17238 border-radius: 3px; 17239 } 17240 17241 .chat-conversations-list::-webkit-scrollbar-thumb:hover { 17242 background: #C10DFF; 17243 } 17244 17245 /* Firefox */ 17246 .chat-conversations-list { 17247 scrollbar-width: thin; 17248 scrollbar-color: #C10DFF #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: #3a270a; 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, #C10DFF 0%, #C10DFF 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: #C10DFF; 17353 } 17354 17355 .chat-conversation-item.active .conversation-time { 17356 color: #C10DFF; 17357 } 17358 17359 .chat-new-btn { 17360 margin: 12px; 17361 padding: 12px; 17362 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 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(245,202,86,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(245,202,86,0.6); 17411 border-radius: 3px; 17412 } 17413 17414 .chat-messages-container::-webkit-scrollbar-thumb:hover { 17415 background: #C10DFF; 17416 } 17417 17418 /* Firefox */ 17419 .chat-messages-container { 17420 scrollbar-width: thin; 17421 scrollbar-color: rgba(245,202,86,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: #C10DFF !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: #e2a642 !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: #C10DFF; 17609 box-shadow: 0 0 0 3px rgba(245,202,86,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, #C10DFF 0%, #C10DFF 100%); 17630 color: white; 17631 } 17632 17633 .chat-new-form .btn-primary:hover { 17634 transform: translateY(-1px); 17635 box-shadow: 0 4px 8px rgba(245,202,86,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: #e2a642; 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: #3a270a; 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: #C10DFF; 17816 box-shadow: 0 0 0 3px rgba(245,202,86,0.1); 17817 } 17818 17819 .chat-send-btn { 17820 width: 40px; 17821 height: 40px; 17822 border-radius: 50%; 17823 background: linear-gradient(135deg, #C10DFF 0%, #C10DFF 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(245,202,86,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">
17945Ã</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="
1801913"></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">Anda 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' ? '#e2a642' : '#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="#e2a642"> 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: #765212; 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: #e2a642; border-color: #e2a642;">Login</button> 18880 <button class="modal-btn modal-btn-primary" id="cartRegisterBtn" style="background: #C10DFF; border-color: #C10DFF;">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: #e2a642;"> 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, #C10DFF 0%, #C10DFF 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 #C10DFF;"> 19530 <div style="font-size: 13px; color: #ae7e21; 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: #C10DFF; 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: 2 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: 2 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: 2 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: 2 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: '#e2a642' }, // 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: '#e2a642' }, // 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: '#e2a642' }, // 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 diketa
22019hui'; 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 }).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: 2 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:#e2a642;">*</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:#e2a642;">*</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 de
23800ngan 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 â #c9832a 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(163,100,34); 24403 border-radius: 18px 18px 0 0; 24404 box-shadow: 0 -12px 30px #2b1b0e, 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(255,255,255,.015), 24418 rgba(255,255,255,.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(226,166,66,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(201,131,42,1); 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(226,166,66,0.08); 24481 filter: drop-shadow(0 0 12px rgba(226,166,66,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(226,166,66,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(226,166,66,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(226,166,66,0.5); 24514 filter: drop-shadow(0 0 10px rgba(226,166,66,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%, #e2a642, rgba(0, 0, 0, 0) 70%), rgba(226,166,66,0.08); 24530 border: 1px solid rgba(226,166,66,0.35); 24531 box-shadow: 0 0 18px #e2a642, inset 0 0 16px rgba(226,166,66,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(226,166,66,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(226,166,66,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(226,166,66,0.1); 24564 box-shadow: inset 0 0 0 1px rgba(226,166,66,0.25); 24565} 24566 24567.sbo-top-fixed-footer a.is-active img{ 24568 filter: drop-shadow(0 0 18px rgba(226,166,66,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://sunny-401.sunnypork.com/" target="_blank" rel="nofollow noopener"> 24590 <img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="BONUS NAGARP"> 24591 Official 24592 </a> 24593 <a href="https://sunny-401.sunnypork.com/" target="_blank" rel="nofollow noopener"> 24594 <img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="LOGIN NAGARP"> 24595 Login 24596 </a> 24597 <a href="https://sunny-401.sunnypork.com/" target="_blank" rel="nofollow noopener"> 24598 <img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="Daftar NAGARP"> 24599 Daftar 24600 </a> 24601 <a href="https://sunny-401.sunnypork.com/" target="_blank" rel="nofollow noopener"> 24602 <img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="Daftar NAGARP"> 24603 Slot 24604 </a> 24605 <a href="https://sunny-401.sunnypork.com/" target="_blank" rel="nofollow noopener"> 24606 <img src="https://sunnypork.com/assets/images/icon-nagarp-x-sunny-pork-perubahan-teknologi-digital-membentuk-pengalaman-pla-674ab6.png" alt="Daftar NAGARP"> 24607 Pengalaman Platform Interaktif
24608 </a> 24609 24610</div>
24611<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: 2 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>
24612 24613 24614<style id="NAGARP-theme-override"> 24615 24616/* ========================================================= 24617 NAGARP EARTHY LUXURY THEME 24618 Espresso + Saffron + Copper + Wine + Ivory 24619 Distinct from previous black/gold/red treatment. 24620========================================================= */ 24621:root{ 24622 --brand-50:#fff7e8; 24623 --brand-100:#f8e5b8; 24624 --brand-500:#d8a62e; 24625 --brand-600:#b87a22; 24626 --brand-700:#7a481e; 24627 24628 --gray-900:#f9ecd4; 24629 --gray-700:#ddc8a8; 24630 --gray-600:#bca382; 24631 --gray-500:#8f765f; 24632 --gray-300:#77481b; 24633 --gray-200:#4d2e14; 24634 --gray-100:#120c08; 24635 --border:#77481b; 24636 24637 --bt-espresso:#130c09; 24638 --bt-coffee:#120c08; 24639 --bt-panel:#2b1b0e; 24640 --bt-panel2:#2b1b0e; 24641 24642 --bt-wine:#6f481d; 24643 --bt-brick:#9a6324; 24644 --bt-copper:#c66f2a; 24645 --bt-saffron:#d8a62e; 24646 --bt-sand:#e8c96f; 24647 --bt-ivory:#fff1d5; 24648 --bt-cream:#f9e7c6; 24649} 24650 24651html{ 24652 background:#130c09 !important; 24653} 24654 24655body{ 24656 color:#f9e7c6 !important; 24657 background: 24658 radial-gradient(circle at 48% -8%,rgba(216,166,46,.16),transparent 30%), 24659 radial-gradient(circle at 8% 31%,rgba(198,111,42,.12),transparent 35%), 24660 radial-gradient(circle at 93% 55%,rgba(119,72,27,.10),transparent 37%), 24661 linear-gradient(180deg,#2b1b0e 0%,#120c08 46%,#120b08 100%) !important; 24662 background-attachment:fixed !important; 24663} 24664 24665.topbar{ 24666 background:linear-gradient(90deg,#160d09,#4d2e14 50%,#160d09) !important; 24667 color:#e8c96f !important; 24668 border-bottom:1px solid rgba(216,166,46,.34) !important; 24669} 24670.topbar a{color:#f9e7c6 !important} 24671.topbar a:hover{color:#d8a62e !important} 24672 24673.navbar, 24674.navbar-logo{ 24675 background: 24676 radial-gradient(circle at 50% 0%,rgba(216,166,46,.10),transparent 43%), 24677 linear-gradient(135deg,rgba(43,27,14,.98),rgba(20,12,9,.99) 54%,rgba(43,27,14,.98)) !important; 24678 border-bottom:1px solid rgba(216,166,46,.42) !important; 24679 box-shadow: 24680 0 5px 22px rgba(0,0,0,.38), 24681 0 0 24px rgba(198,111,42,.08) !important; 24682} 24683.navbar::after{ 24684 background:linear-gradient( 24685 90deg, 24686 transparent, 24687 #77481b, 24688 #a36422, 24689 #c66f2a, 24690 #d8a62e, 24691 #e8c96f, 24692 #d8a62e, 24693 #c66f2a, 24694 #a36422, 24695 #77481b, 24696 transparent 24697 ) !important; 24698 background-size:220% 100% !important; 24699 opacity:.88 !important; 24700 animation:btEarthLine 5.6s linear infinite !important; 24701} 24702@keyframes btEarthLine{ 24703 from{background-position:185% 0} 24704 to{background-position:-85% 0} 24705} 24706 24707/* Search */ 24708.search, 24709.nav-inner .search{ 24710 background:#fff8ed !important; 24711 border-color:#dcb65a !important; 24712 box-shadow: 24713 0 4px 14px rgba(0,0,0,.17), 24714 0 0 15px rgba(198,111,42,.07) !important; 24715} 24716.search:focus-within, 24717.nav-inner .search:focus-within{ 24718 border-color:#c66f2a !important; 24719 box-shadow: 24720 0 0 0 3px rgba(216,166,46,.13), 24721 0 0 21px rgba(119,72,27,.08) !important; 24722} 24723.search input, 24724.nav-inner .search input{color:#2b1b0e !important} 24725.search input::placeholder, 24726.nav-inner .search input::placeholder{color:#957866 !important} 24727.search-icon-btn, 24728.nav-inner .search-icon-btn{color:#a36422 !important} 24729.search-icon-btn:hover{color:#c66f2a !important} 24730 24731/* Buttons / nav actions */ 24732.icon-btn, 24733.login-btn, 24734.pd-sticky-share-btn, 24735.pd-sticky-btn-cart, 24736.pd-sticky-btn-chat{ 24737 color:#e8c96f !important; 24738 border-color:rgba(216,166,46,.31) !important; 24739 background:linear-gradient(135deg,#2b1b0e,#130c09) !important; 24740 box-shadow: 24741 inset 0 1px 0 rgba(255,241,213,.05), 24742 0 3px 12px rgba(0,0,0,.27) !important; 24743} 24744.icon-btn:hover, 24745.login-btn:hover, 24746.pd-sticky-share-btn:hover, 24747.pd-sticky-btn-cart:hover, 24748.pd-sticky-btn-chat:hover{ 24749 color:#fff !important; 24750 border-color:#d8a62e !important; 24751 background:linear-gradient(135deg,#4d2e14,#120c08) !important; 24752 box-shadow:0 0 17px rgba(198,111,42,.14) !important; 24753} 24754 24755/* Categories */ 24756.mega-cats a{color:#ddc8a8 !important} 24757.mega-cats a:hover{ 24758 color:#fff !important; 24759 background:linear-gradient(135deg,rgba(163,100,34,.28),rgba(216,166,46,.12)) !important; 24760} 24761.mega-cats a.active, 24762.mega-more-grid a.active, 24763.mega-cats-more.active > .mega-more-btn{ 24764 color:#fff !important; 24765 background:linear-gradient(135deg,#77481b,#a36422 48%,#c66f2a 76%,#d8a62e) !important; 24766} 24767.mega-more-btn, 24768.mm2-trigger{ 24769 color:#e8c96f !important; 24770 background:#120c08 !important; 24771 border-color:#77481b !important; 24772} 24773.mega-more-dropdown, 24774.mm2-panel{ 24775 background:#120c08 !important; 24776 color:#f9e7c6 !important; 24777 border-color:#77481b !important; 24778} 24779 24780/* Main */ 24781.hero{ 24782 background: 24783 radial-gradient(circle at 10% 0%,rgba(216,166,46,.085),transparent 34%), 24784 linear-gradient(145deg,#2b1b0e,#140d0a) !important; 24785} 24786.usp-section, 24787.flash, 24788.products{ 24789 background: 24790 radial-gradient(circle at 92% 0%,rgba(198,111,42,.05),transparent 32%), 24791 linear-gradient(180deg,#120c08,#120c09) !important; 24792 border-color:rgba(216,166,46,.15) !important; 24793} 24794 24795.product-single{ 24796 --pd-border:#77481b; 24797 --pd-bg:#120c08; 24798 --pd-muted:#c9b08d; 24799 --pd-dark:#f9e7c6; 24800 --pd-accent:#d8a62e; 24801 --pd-soft:#2b1b0e; 24802 --pd-soft-border:#77481b; 24803 --pd-danger:#b77924; 24804 --pd-danger-bg:#493019; 24805 --pd-danger-text:#e4b767; 24806 --pd-shadow:0 8px 28px -10px rgba(0,0,0,.62);
24807 color:#f9e7c6 !important; 24808} 24809 24810.product-single--v3, 24811.pd-top-layout{background:transparent !important} 24812 24813.pd-sticky-header{ 24814 background:rgba(18,12,8,.96) !important; 24815 border-bottom-color:rgba(216,166,46,.32) !important; 24816 box-shadow: 24817 0 5px 24px rgba(0,0,0,.45), 24818 0 0 18px rgba(198,111,42,.07) !important; 24819} 24820.pd-sticky-title{color:#f9e7c6 !important} 24821.pd-sticky-price{color:#e8c96f !important} 24822.pd-sticky-thumb{ 24823 background:#120c08 !important; 24824 border-color:#77481b !important; 24825} 24826 24827/* Panels / cards */ 24828.pd-media-card, 24829.pd-info-card, 24830.pd-buy-card, 24831.pd-side-card, 24832.pd-full-description, 24833.pd-reviews-section, 24834.pd-related-full, 24835.pd-installment-card, 24836.pd-installment-card2, 24837.pd-compare-section, 24838.pd-short-desc, 24839.pd-variants, 24840.prod-card, 24841.usp-item, 24842.hero-box, 24843.search-suggest, 24844.user-dropdown, 24845.mobile-user-sheet-content{ 24846 background: 24847 radial-gradient(circle at 100% 0%,rgba(216,166,46,.055),transparent 36%), 24848 radial-gradient(circle at 0% 100%,rgba(163,100,34,.038),transparent 38%), 24849 linear-gradient(155deg,#2b1b0e,#140d0a) !important; 24850 color:#f9e7c6 !important; 24851 border-color:rgba(216,166,46,.18) !important; 24852 box-shadow: 24853 0 9px 28px rgba(0,0,0,.29), 24854 inset 0 1px 0 rgba(255,241,213,.024) !important; 24855} 24856.prod-card:hover, 24857.usp-item:hover, 24858.pd-media-card:hover, 24859.pd-info-card:hover{ 24860 border-color:rgba(216,166,46,.36) !important; 24861 box-shadow: 24862 0 12px 30px rgba(0,0,0,.33), 24863 0 0 19px rgba(198,111,42,.075) !important; 24864} 24865.prod-card .img-box, 24866.pd-main-image, 24867.pd-mslide, 24868.pd-thumb-btn{ 24869 background:#120c08 !important; 24870 border-color:#4d2e14 !important; 24871} 24872.pd-thumb-btn.active{ 24873 border-color:#d8a62e !important; 24874 box-shadow: 24875 0 0 0 2px rgba(216,166,46,.12), 24876 0 0 16px rgba(198,111,42,.07) !important; 24877} 24878 24879/* Heading / price */ 24880.pd-title, 24881.pd-full-desc-title, 24882.pd-related-title, 24883.hero-text h1, 24884.price strong, 24885.pd-price-current, 24886.text-brand, 24887.hero-text .text-brand{ 24888 color:#e8c96f !important; 24889 text-shadow:0 0 11px rgba(216,166,46,.08) !important; 24890} 24891.pd-price-original{color:#a88c73 !important} 24892 24893/* Primary CTA */ 24894.btn-primary, 24895.modal-btn-primary, 24896.review-form .btn-primary, 24897.pd-compare-btn, 24898.pd-sticky-btn-buy{ 24899 background:linear-gradient(135deg,#77481b 0%,#a36422 40%,#c66f2a 72%,#d8a62e 100%) !important; 24900 border-color:#e8c96f !important; 24901 color:#fff !important; 24902 box-shadow: 24903 0 5px 18px rgba(119,72,27,.16), 24904 0 0 15px rgba(216,166,46,.06) !important; 24905} 24906.btn-primary:hover, 24907.modal-btn-primary:hover, 24908.review-form .btn-primary:hover, 24909.pd-compare-btn:hover, 24910.pd-sticky-btn-buy:hover{ 24911 background:linear-gradient(135deg,#fff1d5,#e8c96f 48%,#c66f2a) !important; 24912 border-color:#fff1d5 !important; 24913 color:#2b1b0e !important; 24914} 24915 24916/* Badges */ 24917.badge, 24918.bottom-nav-item-badge, 24919.pd-discount-badge, 24920.mini-disc, 24921.discount, 24922.disc, 24923.sale-badge, 24924.product-single .stock-badge.out{ 24925 background:linear-gradient(135deg,#77481b,#a36422 52%,#c66f2a) !important; 24926 color:#fff !important; 24927 border-color:#d8a62e !important; 24928} 24929 24930/* Forms */ 24931.product-single input, 24932.product-single select, 24933.product-single textarea, 24934.modal input, 24935.modal select, 24936.modal textarea{ 24937 background:#120c08 !important; 24938 color:#f9e7c6 !important; 24939 border-color:#77481b !important; 24940} 24941.product-single input:focus, 24942.product-single select:focus, 24943.product-single textarea:focus{ 24944 border-color:#d8a62e !important; 24945 box-shadow:0 0 0 3px rgba(216,166,46,.10) !important; 24946} 24947 24948/* NAGARP machine */ 24949.Pengalaman-Platform-Interaktif-v3{ 24950 background: 24951 radial-gradient(circle at 15% 0%,rgba(198,111,42,.11),transparent 46%), 24952 radial-gradient(circle at 85% 100%,rgba(119,72,27,.08),transparent 55%), 24953 linear-gradient(135deg,#2b1b0e,#130c09) !important; 24954 border-color:rgba(216,166,46,.28) !important; 24955 box-shadow: 24956 inset 0 0 20px rgba(163,100,34,.04), 24957 0 0 18px rgba(216,166,46,.055) !important; 24958} 24959.NAGARP-controls-v3{ 24960 background:rgba(22,13,10,.74) !important; 24961 border-color:rgba(216,166,46,.24) !important; 24962} 24963.NAGARP-controls-v3 label{color:#e8c96f !important}
24964.NAGARP-controls-v3 select, 24965.NAGARP-controls-v3 input{ 24966 background:#120c08 !important; 24967 color:#f9e7c6 !important; 24968 border-color:rgba(216,166,46,.28) !important; 24969} 24970#btnNAGARPRandom{ 24971 background:linear-gradient(135deg,#77481b,#a36422 48%,#c66f2a 76%,#d8a62e) !important; 24972 color:#fff !important; 24973 border-color:#e8c96f !important; 24974} 24975 24976/* Footer */ 24977.bottom-nav{ 24978 background:rgba(20,12,9,.98) !important; 24979 border-top-color:#77481b !important; 24980 box-shadow:0 -8px 24px rgba(0,0,0,.45) !important; 24981} 24982.bottom-nav-item{color:#cfb28f !important} 24983.bottom-nav-item:hover{color:#e8c96f !important} 24984.bottom-nav-item.active{ 24985 color:#fff !important; 24986 background:linear-gradient(135deg,#77481b,#a36422 48%,#c66f2a 76%,#d8a62e) !important; 24987} 24988 24989.sbo-top-fixed-footer{ 24990 background: 24991 radial-gradient(circle at 50% 0%,rgba(216,166,46,.085),transparent 42%), 24992 linear-gradient(180deg,rgba(43,27,14,.98),rgba(18,11,8,.99)) !important; 24993 border-top-color:#8f5a22 !important; 24994 box-shadow: 24995 0 -12px 32px rgba(0,0,0,.50), 24996 inset 0 1px 0 rgba(232,201,111,.05) !important; 24997} 24998.sbo-top-fixed-footer::after{ 24999 background:linear-gradient( 25000 120deg, 25001 transparent, 25002 rgba(216,166,46,.05), 25003 rgba(232,201,111,.20), 25004 rgba(198,111,42,.09), 25005 transparent 25006 ) !important; 25007} 25008.sbo-top-fixed-footer a{color:#e8c96f !important} 25009.sbo-top-fixed-footer a:hover, 25010.sbo-top-fixed-footer a.is-active{ 25011 color:#fff !important; 25012 background:linear-gradient(135deg,rgba(119,72,27,.15),rgba(198,111,42,.075)) !important; 25013} 25014.sbo-top-fixed-footer img{ 25015 filter: 25016 drop-shadow(0 0 7px rgba(216,166,46,.28)) 25017 drop-shadow(0 0 11px rgba(198,111,42,.10)) !important; 25018} 25019.sbo-top-fixed-footer .hud-corner{ 25020 border-color:rgba(216,166,46,.30) !important; 25021 filter:drop-shadow(0 0 9px rgba(198,111,42,.09)) !important; 25022} 25023.sbo-top-fixed-footer .tada{ 25024 color:#fff !important; 25025 background: 25026 radial-gradient(circle at 50% 0%,rgba(198,111,42,.18),transparent 70%), 25027 rgba(119,72,27,.055) !important; 25028 border-color:rgba(216,166,46,.24) !important; 25029 box-shadow: 25030 0 0 15px rgba(198,111,42,.11), 25031 inset 0 0 14px rgba(216,166,46,.06) !important; 25032} 25033 25034</style> 25035 25036 25037<style id="NAGARP-balanced-adjust"> 25038 25039/* ========================================================= 25040 EARTHY FINISHING PASS 25041========================================================= */ 25042:root{ 25043 --brand-500:#d8a62e !important; 25044 --brand-600:#b87a22 !important; 25045 --brand-700:#7a481e !important; 25046} 25047 25048/* Existing animation retained, recolored and slowed slightly */ 25049.pd-media-card, 25050.pd-info-card, 25051.sbo-top-fixed-footer, 25052.navbar{ 25053 background-size:145% 145% !important; 25054 animation:btEarthShift 10s ease-in-out infinite alternate !important; 25055} 25056@keyframes btEarthShift{ 25057 from{background-position:0% 20%} 25058 to{background-position:100% 80%} 25059} 25060 25061.sbo-top-fixed-footer .tada::before{ 25062 background: 25063 radial-gradient(circle,rgba(216,166,46,.11),transparent 68%), 25064 radial-gradient(circle,rgba(119,72,27,.075),transparent 74%) !important; 25065 filter:blur(12px) !important; 25066 opacity:.48 !important; 25067 animation:btEarthPulse 4s ease-in-out infinite !important; 25068} 25069@keyframes btEarthPulse{ 25070 0%,100%{opacity:.26;transform:scale(.985)} 25071 50%{opacity:.60;transform:scale(1.035)} 25072} 25073 25074button:hover, 25075a:hover{ 25076 text-shadow:0 0 9px rgba(216,166,46,.20); 25077} 25078 25079@media(prefers-reduced-motion:reduce){ 25080 .navbar::after, 25081 .pd-media-card, 25082 .pd-info-card, 25083 .sbo-top-fixed-footer, 25084 .navbar, 25085 .sbo-top-fixed-footer .tada::before{ 25086 animation:none !important; 25087 } 25088} 25089 25090</style> 25091
25092<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"5aa68ce2394b457896bf8eb3d53f6f3d","r":1}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 25092
25092
25093<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496" integrity="sha512-ZE9pZaUXND66v380QUtch/5sE9tPFh2zg45pR2PB0CVkCtOREv2AJKkSidISWkysEuQ0EH8faUU5du78bx87UQ==" data-cf-beacon='{"version":"2024.11.0","token":"7fbda7d5e9cb448b912aa8e936e771c8","r":1}' crossorigin="anonymous"></script>
vendor: 2 bytes, line 25093
25093
25094<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":"de306536e07f47468f6bfb89ad0d03ca","r":1,"spa":2}' crossorigin="anonymous"></script>
25094 25095</body> 25096</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.