s
saltykisses.co lido-finances.us.com thecozy-restaurant.cz

ALAM87: Website Modern dengan Sistem Akses Super Cepat

    ALAM87: Website Modern dengan Sistem Akses Super Cepat
    ALAM87: Website Modern dengan Sistem Akses Super Cepat

    ALAM87 merupakan website modern yang dirancang untuk menghadirkan pengalaman akses yang cepat, stabil, dan nyaman bagi para pengguna. Dengan memanfaatkan teknologi web terkini, ALAM87 mampu memberikan performa yang responsif sehingga setiap halaman dapat dimuat dalam waktu singkat di berbagai perangkat, baik desktop maupun smartphone.

    Kecepatan akses menjadi salah satu faktor penting dalam kenyamanan pengguna. Oleh karena itu, ALAM87 mengoptimalkan setiap elemen pada platform agar tetap ringan tanpa mengurangi kualitas tampilan maupun fungsionalitasnya.

    ALAM87 mengusung desain antarmuka yang modern dan responsif. Pengguna dapat menikmati navigasi yang mudah dengan tata letak yang rapi sehingga proses menjelajahi berbagai fitur menjadi lebih praktis.

    Selain mengutamakan kecepatan, ALAM87 juga memperhatikan aspek keamanan pengguna. Berbagai teknologi perlindungan diterapkan untuk membantu menjaga stabilitas sistem dan memberikan pengalaman penggunaan yang lebih aman. Penerapan standar keamanan modern menjadi bagian penting dalam menjaga kualitas layanan sekaligus meningkatkan kepercayaan pengguna terhadap platform.

    ALAM87 terus melakukan pengembangan agar mampu memenuhi kebutuhan pengguna yang semakin berkembang. Mulai dari optimasi performa, peningkatan kecepatan, hingga penyempurnaan tampilan dilakukan secara berkala untuk memberikan pengalaman digital yang lebih baik. Dengan kombinasi desain modern, sistem yang cepat, serta teknologi yang terus diperbarui, ALAM87 menjadi salah satu website yang menawarkan kenyamanan akses bagi berbagai kalangan pengguna.

    FAQ ALAM87
    Apa itu ALAM87?

    ALAM87 adalah platform digital yang menyediakan layanan dengan sistem modern, antarmuka yang responsif, serta dukungan transaksi menggunakan QRIS agar proses pembayaran menjadi lebih cepat dan praktis.

    Apa keuntungan menggunakan deposit QRIS di ALAM87?

    Slot di ALAM87 Deposit melalui QRIS menawarkan proses pembayaran yang cepat, mudah, mendukung berbagai aplikasi pembayaran digital, serta dapat dilakukan tanpa perlu memasukkan nomor rekening secara manual.

    Apakah transaksi di ALAM87 tersedia selama 24 jam?

    Ya. Sistem transaksi ALAM87 dirancang agar dapat digunakan selama 24 jam, sehingga pengguna dapat melakukan aktivitas kapan saja sesuai kebutuhan.

    Perangkat apa saja yang dapat digunakan untuk mengakses ALAM87?

    ALAM87 dapat diakses melalui berbagai perangkat seperti smartphone, tablet, laptop, maupun komputer desktop dengan tampilan yang telah dioptimalkan.

    Apakah ALAM87 mendukung berbagai metode pembayaran QRIS?

    Ya. Selama aplikasi pembayaran yang digunakan telah mendukung standar QRIS, transaksi dapat dilakukan dengan mudah melalui proses pemindaian kode QR.

    ALAM87: Website Modern dengan Sistem Akses Super Cepat

      Product form
      Warranty Icon Garansi Pasti Jackpot
      Cicilan Iconakulaku kredivo Icon
      Unit Resmi Indonesia Icon Slot Resmi Indonesia
      Tukar Tambah Jaminan Garansi Kekalahan

      Rp 88.889

      Unit (Opsional)

      AKSES LANGSUNG

      ALAM87: Website Modern dengan Sistem Akses Super Cepat.

      Kamu sudah punya voucher tukar tambah

      Mencoba lagi akan membatalkan voucher diskonmu. Lanjutkan?

      Trade-in policy

      Please note:

      • Trade-in service is only available for in-store pickup.
      • At checkout, please select "Pick up" as your delivery method and choose "Carousell Mobile HQ".
      • Bring your old device within 7 days after purchase and collect your the device you're buying.

      Semua Bonus & Promo Yang Tersedia

      BONUS GARANSI KEKALAHAN 200%

      Anda Kalah Modal Kami Ganti 2 Kali Lipat!

      BONUS NEW MEMBER UP TO 200%

      Member Baru Langsung Di Berikan Modal Tambahan

      BONUS DEPOSIT 50%

      Tiap Deposit Dapat Tambahan Modal

      Bonus Scatter Rp 1.000.000,00

      Tiap Dapat Fitur Scatter Bisa Dapat Freechip Tambahan

      Freechip Setiap Hari

      Makin Aktif Bermain Makin Cuan

      BONUS BULANAN UP TO 3M

      Jadi Member Setia Sama Dengan Jadi Sultan

      BONUS ROLINGAN 10%

      Makin Aktif Bermain Makin Cuan

      BONUS CASHBACK 20%

      Tiap Minggu Kami Kembalikan Modal!

      BONUS TANPA POTONGAN

      Deposit By PULSA Tak Akan Ada Potongan

      METODE DEPOSIT TERLENGKAP

      Mau Deposit Via Apa Saja Kami Sedia!


        Kamu bisa bayar dengan:
        Bank BCABank BNIBank BRIBank MandiriBank PermataBank BSIDanaShopeePayGopayLinkAjaOVOMastercardVisaKredivoAkuLaku

        Deskripsi

        ALAM87 merupakan website modern yang dirancang untuk menghadirkan pengalaman digital yang cepat, praktis, dan nyaman bagi para penggunanya.
        NFC:No
        Radio:Yes
        USB:USB Type-C 2.0
        Charging:33W wired; 50% in 30 min
        Battery Type:Li-Po 5000 mAh; non-removable
        Speaker:Mono speaker
        Video:1080p@30fps
        Main Camera:50 MP (wide) + 2 MP (depth)
        Selfie Camera Definition:8 MP; f/2.0 (wide)
        Bluetooth:Bluetooth 5.2
        Wi-Fi:Wi-Fi 802.11 a/b/g/n/ac
        SIM:Dual SIM (Nano-SIM; dual stand-by)
        Chip Options:Qualcomm Snapdragon 695 5G (6 nm)
        Memory:6GB;8GB RAM
        Storage Description:UFS 2.1
        Resolution:1080 x 2400 pixels (~395 ppi density)
        Display Size:6.67 inches
        Display Type:IPS LCD; 90Hz
        Weight:186 g
        Dimension:165.7 x 76 x 7.9 mm
        OS:Android OS
        Google Reviews
        ${shouldTruncate ? `
        ${summaryToUse.substring(0, characterLimit)}...
        ` : `
        ${summaryToUse}
        ` }
        ${metaText ? `
        ${metaText}
        ` : ''}
        `; // Setup intersection observer for viewport-based animations setTimeout(() => setupAnimationObserver(cardId, animation), 100); return cardHtml; } // Animation observer for viewport-based triggers let animationObserver_ = null; // Setup intersection observer for AI summary animation function setupAnimationObserver(cardId, animation) { if (animation === 'none') return; const card = document.getElementById(cardId); if (!card) return; // Create observer if it doesn't exist if (!animationObserver_) { animationObserver_ = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const cardId = entry.target.id; const animation = entry.target.dataset.animation; applyTextAnimation(cardId, animation); animationObserver_.unobserve(entry.target); } }); }, { threshold: 0.3, // Trigger when 30% of the card is visible rootMargin: '0px' }); } // Add animation data attribute and observe the card card.dataset.animation = animation; animationObserver_.observe(card); } // Apply text animation to AI summary function applyTextAnimation(cardId, animation) { if (animation === 'none') return; const card = document.getElementById(cardId); if (!card) return; const textElement = card.querySelector('.gr-ai-summary-text .gr-text-preview, .gr-ai-summary-text .gr-text-full'); if (!textElement) return; switch (animation) { case 'slide-up': textElement.style.transform = 'translateY(60px)'; // Increased from 30px to 60px textElement.style.opacity = '0'; textElement.style.transition = 'transform 0.8s ease-out, opacity 0.8s ease-out'; setTimeout(() => { textElement.style.transform = 'translateY(0)'; textElement.style.opacity = '1'; }, 150); break; case 'slide-left': textElement.style.transform = 'translateX(80px)'; // Increased from 40px to 80px textElement.style.opacity = '0'; textElement.style.transition = 'transform 0.7s ease-out, opacity 0.7s ease-out'; setTimeout(() => { textElement.style.transform = 'translateX(0)'; textElement.style.opacity = '1'; }, 150); break; case 'typewriter': const originalText = textElement.textContent; textElement.textContent = ''; textElement.style.borderRight = '2px solid #667eea'; let i = 0; const typeInterval = setInterval(() => { textElement.textContent += originalText.charAt(i); i++; if (i >= originalText.length) { clearInterval(typeInterval); setTimeout(() => { textElement.style.borderRight = 'none'; }, 500); } }, 50); break; case 'reveal-lines': const text = textElement.textContent; const lines = text.split(/[.!?]+/).filter(line => line.trim().length > 0); textElement.innerHTML = ''; lines.forEach((line, index) => { const lineDiv = document.createElement('div'); lineDiv.textContent = line.trim() + (index { lineDiv.style.opacity = '1'; lineDiv.style.transform = 'translateY(0)'; }, (index + 1) * 300); }); break; case 'bounce-in': textElement.style.transform = 'scale(0.3)'; textElement.style.opacity = '0'; textElement.style.transition = 'transform 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.6s ease-out'; setTimeout(() => { textElement.style.transform = 'scale(1)'; textElement.style.opacity = '1'; }, 100); break; case 'scale-reveal': textElement.style.transform = 'scale(0)'; textElement.style.transformOrigin = 'center'; textElement.style.transition = 'transform 0.7s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; setTimeout(() => { textElement.style.transform = 'scale(1)'; }, 150); break; case 'flip-in': textElement.style.transform = 'rotateX(-90deg)'; textElement.style.opacity = '0'; textElement.style.transformOrigin = 'center bottom'; textElement.style.transition = 'transform 0.8s ease-out, opacity 0.6s ease-out'; setTimeout(() => { textElement.style.transform = 'rotateX(0deg)'; textElement.style.opacity = '1'; }, 100); break; case 'zoom-fade': textElement.style.transform = 'scale(0.8)'; textElement.style.opacity = '0'; textElement.style.transition = 'transform 0.8s ease-out, opacity 0.8s ease-out'; setTimeout(() => { textElement.style.transform = 'scale(1)'; textElement.style.opacity = '1'; }, 150); break; case 'highlight-sweep': textElement.style.position = 'relative'; textElement.style.overflow = 'hidden'; // Create highlight overlay const highlight = document.createElement('div'); highlight.style.position = 'absolute'; highlight.style.top = '0'; highlight.style.left = '-100%'; highlight.style.width = '100%'; highlight.style.height = '100%'; highlight.style.background = 'linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.4), transparent)'; highlight.style.transition = 'left 0.8s ease-in-out'; textElement.appendChild(highlight); // Multiple sweeps - sweep 3 times let sweepCount = 0; const performSweep = () => { sweepCount++; highlight.style.left = '100%'; if (sweepCount { highlight.style.transition = 'none'; highlight.style.left = '-100%'; setTimeout(() => { highlight.style.transition = 'left 0.8s ease-in-out'; performSweep(); }, 50); }, 800); } else { // Clean up highlight after final sweep setTimeout(() => { if (highlight.parentNode) { highlight.parentNode.removeChild(highlight); } }, 800); } }; // Start first sweep setTimeout(() => { performSweep(); }, 200); break; } } // Toggle AI summary text (show more/less functionality) window[`toggleAISummaryText_${instanceId.replace(/-/g, '_')}`] = function() { // If full text mode is enabled, show more buttons shouldn't exist if (window.layoutConfig.show_full_text ?? false) { return; } // Find AI summary cards (including clones for carousel mode) const cards = document.querySelectorAll(`#${instanceId} .gr-ai-summary-card`); if (!cards.length) return; // Find the original (non-cloned) card to determine current state let originalCard = null; let currentState = 'collapsed'; // default state cards.forEach(card => { const slide = card.closest('.splide__slide'); if (!slide || !slide.classList.contains('splide__slide--clone')) { originalCard = card; const full = card.querySelector('.gr-text-full'); currentState = (full && full.style.display === 'block') ? 'expanded' : 'collapsed'; } }); // If no original card found, use the first available card to determine state if (!originalCard && cards.length > 0) { const full = cards[0].querySelector('.gr-text-full'); currentState = (full && full.style.display === 'block') ? 'expanded' : 'collapsed'; } // Toggle state for all cards (originals and clones) const newState = currentState === 'collapsed' ? 'expanded' : 'collapsed'; cards.forEach(card => { const preview = card.querySelector('.gr-text-preview'); const full = card.querySelector('.gr-text-full'); const button = card.querySelector('.gr-read-more'); if (preview && full && button) { if (newState === 'expanded') { preview.style.display = 'none'; full.style.display = 'block'; button.textContent = translations.show_less; } else { preview.style.display = 'block'; full.style.display = 'none'; button.textContent = translations.show_more; } } }); }; // Privacy helper functions function getInitials(name) { if (!name) return 'U'; return name.split(' ').map(word => word.charAt(0)).join('').toUpperCase().substring(0, 2); } function generateInitialsAvatar(name) { const initials = getInitials(name); const bgColor = '#' + Math.floor(Math.abs(Math.sin(name.length) * 16777215) % 16777215).toString(16); return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(` ${initials} `)}`; } // Generate product associations HTML function generateProductAssociations(review) { if (!review.product_details || review.product_details.length === 0) { return ''; } // Limit products displayed const maxProducts = window.layoutConfig.products_max_display || 5; const productsToShow = review.product_details.slice(0, maxProducts); let productsHTML = ''; productsToShow.forEach(product => { const imagePosition = window.layoutConfig.product_image_position || 'left'; const showImages = window.layoutConfig.show_product_images !== false; let productContent = ''; const imageHTML = showImages ? `${product.title}` : ''; const titleHTML = `${product.title}`; if (imagePosition === 'top') { productContent = imageHTML + titleHTML; } else if (imagePosition === 'right') { productContent = titleHTML + imageHTML; } else { // left (default) productContent = imageHTML + titleHTML; } productsHTML += ` ${productContent} `; }); const writtenForLabel = window.layoutConfig.show_written_for_label !== false ? `${translations.written_for}` : ''; const productsLayout = window.layoutConfig.products_layout || 'horizontal'; return `
        ${writtenForLabel}
        ${productsHTML}
        `; } // Generate review photos HTML function generateReviewPhotos(review) { if (!review.photo_urls || review.photo_urls.length === 0) { return ''; } // Check if photo display is enabled in settings if (!window.layoutConfig.show_review_photos) { return ''; } // Use dynamic max photos per review setting const maxPhotos = window.layoutConfig.max_photos_per_review || 4; const photosToShow = review.photo_urls.slice(0, maxPhotos); const remainingCount = review.photo_urls.length - photosToShow.length; let photosHTML = ''; photosToShow.forEach((photoUrl, index) => { if (index === maxPhotos - 1 && remainingCount > 0) { // Show overlay with remaining count on last photo photosHTML += `
        Review photo ${index + 1}
        +${remainingCount}
        `; } else { photosHTML += `
        Review photo ${index + 1}
        `; } }); return `
        ${photosHTML}
        `; } // Photo viewer functionality window[`openPhotoViewer_${instanceId.replace(/-/g, '_')}`] = function(reviewId, photoIndex) { const review = reviewsData.reviews[reviewId]; if (!review || !review.photo_urls || review.photo_urls.length === 0) return; // Create photo viewer modal const modal = document.createElement('div'); modal.className = 'gr-photo-viewer-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); z-index: 10000; display: flex; align-items: center; justify-content: center; cursor: pointer; `; const container = document.createElement('div'); container.className = 'gr-photo-viewer-container'; container.style.cssText = ` position: relative; max-width: 90%; max-height: 90%; background: white; border-radius: 8px; overflow: hidden; cursor: default; `; const img = document.createElement('img'); img.src = review.photo_urls[photoIndex]; img.style.cssText = ` width: 100%; height: auto; max-height: 80vh; object-fit: contain; display: block; `; const closeBtn = document.createElement('button'); closeBtn.innerHTML = '×'; closeBtn.style.cssText = ` position: absolute; top: 10px; right: 15px; background: rgba(0, 0, 0, 0.5); color: white; border: none; font-size: 30px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; `; container.appendChild(img); container.appendChild(closeBtn); modal.appendChild(container); document.body.appendChild(modal); // Close functionality const closeModal = () => { document.body.removeChild(modal); }; modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); closeBtn.addEventListener('click', closeModal); container.addEventListener('click', (e) => { e.stopPropagation(); }); // Keyboard navigation const handleKeydown = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleKeydown); } }; document.addEventListener('keydown', handleKeydown); }; // Generate review card HTML with privacy and translations function generateReviewCard(reviewId, review) { const reviewText = review.text || ''; const characterLimit = window.layoutConfig.character_limit || 150; const shouldTruncate = !(window.layoutConfig.show_full_text ?? false) && reviewText.length > characterLimit; // Handle privacy settings for author display const authorName = privacy.use_initials_only ? getInitials(review.author_name) : review.author_name; const authorImage = privacy.disable_google_images ? generateInitialsAvatar(review.author_name) : review.profile_photo_url || generateInitialsAvatar(review.author_name); const authorUrl = privacy.disable_google_links ? '#' : review.author_url || '#'; const linkTarget = privacy.disable_google_links ? '' : 'target="_blank"'; // CRITICAL FIX: Check show_review_header during HTML generation const reviewHeaderHTML = window.layoutConfig.show_review_header ? `
        ${privacy.disable_google_links ? `${authorName}` : `${authorName}` }
        ${privacy.disable_google_links ? `
        ${authorName}
        ` : `${authorName}` } ${formatDate(review.time) ? `
        ${formatDate(review.time)}
        ` : ''}
        ${generateStarHTML(review.rating)}
        ` : ''; return `
      • ${reviewHeaderHTML}
        ${shouldTruncate ? `
        ${reviewText.substring(0, characterLimit)}...
        ` : `
        ${reviewText}
        ` } ${(false ||false) ? generateReviewPhotos(review) : ''}
        ${window.layoutConfig.show_product_associations ? generateProductAssociations(review) : ''}
      • `; } // Generate business header HTML - COMPACT LAYOUT LIKE SCREENSHOT // Get custom business name from configuration function getCustomBusinessName() { // Get custom business name from configuration, fallback to actual business name const customName = window.layoutConfig.custom_business_name || 'Top Rated'; return customName.trim() !== '' ? customName : (businessData.name || 'Top Rated'); } function generateBusinessHeader() { if (!window.layoutConfig.show_business_header) return ''; // Get configurable text template for translation support const headerReviewsTextTemplate = window.layoutConfig.header_reviews_text_template || 'Based on {count} Reviews'; // Build write review URL using place_id const writeReviewUrl = businessData.place_id ? `https://search.google.com/local/writereview?placeid=${businessData.place_id}` : businessData.url; return `
        ${getCustomBusinessName()}
        ${generateStarHTML(businessData.rating)}
        ${businessData.rating}
        ${headerReviewsTextTemplate.replace('{count}', `${businessData.user_ratings_total}`)}
        ${(!window.layoutConfig.show_write_review || privacy.disable_google_links) ? '' : `` }
        `; } // Generate widget title HTML function generateWidgetTitle() { if (!(window.layoutConfig.show_widget_title ?? true) || !window.layoutConfig.widget_title) return ''; const headingLevel = window.layoutConfig.widget_title_heading_level || 'h2'; return `${window.layoutConfig.widget_title}${headingLevel}>`; } // Generate Google footer with powered by text - FIXED function generateGoogleFooter() { if (!window.layoutConfig.show_footer) return ''; return ` `; } // Generate complete widget HTML function generateWidgetHTML() { const reviewsToShow = getState().filteredReviewIds.slice(0, getState().currentDisplayCount); // Handle custom message fallback if (reviewsToShow.length === 1 && reviewsToShow[0] === '__SHOW_MESSAGE__') { const customMessage = window.layoutConfig.fallback_message || 'No matching reviews found'; return ` ${generateBusinessHeader()} `; } let reviewsHTML = ''; // Get AI summary configuration const aiSummaryEnabled = window.layoutConfig.ai_summary_enabled ?? true; const aiSummaryPosition = window.layoutConfig.ai_summary_position || 'first'; const aiSummary = businessData.ai_summary; const reviewCount = businessData.user_ratings_total || reviewsToShow.length; // Debug AI Summary // Generate AI summary card if enabled (access control handled at template level) const aiSummaryCard = aiSummaryEnabled ? generateAISummaryCard(aiSummary, reviewCount) : ''; // Track if AI summary has been inserted to avoid duplicates let aiSummaryInserted = false; // Position-based HTML generation if (aiSummaryPosition === 'first' && aiSummaryCard) { reviewsHTML += aiSummaryCard; aiSummaryInserted = true; } reviewsToShow.forEach((reviewId, index) => { // Insert AI summary at specific positions BEFORE adding the review if (aiSummaryCard && !aiSummaryInserted) { if (aiSummaryPosition === 'second' && index === 1) { reviewsHTML += aiSummaryCard; aiSummaryInserted = true; } else if (aiSummaryPosition === 'third' && index === 2) { reviewsHTML += aiSummaryCard; aiSummaryInserted = true; } else if (aiSummaryPosition === 'middle' && index === Math.floor(reviewsToShow.length / 2)) { reviewsHTML += aiSummaryCard; aiSummaryInserted = true; } } const review = reviewsData.reviews[reviewId]; if (review) { reviewsHTML += generateReviewCard(reviewId, review); } else { } }); // Add AI summary at the end if position is 'last' and not already inserted if (aiSummaryPosition === 'last' && aiSummaryCard && !aiSummaryInserted) { reviewsHTML += aiSummaryCard; } return ` ${generateBusinessHeader()} `; } // Ensure viewport meta tag exists for responsive behavior function ensureViewportMeta() { let viewportMeta = document.querySelector('meta[name="viewport"]'); if (!viewportMeta) { viewportMeta = document.createElement('meta'); viewportMeta.name = 'viewport'; viewportMeta.content = 'width=device-width, initial-scale=1.0'; document.head.appendChild(viewportMeta); } } // Responsive carousel columns using CSS media query detection function applyResponsiveGrid() { const carouselElement = document.querySelector(`#${instanceId} .gr-carousel`); if (!carouselElement) return; // Ensure viewport meta tag exists first ensureViewportMeta(); let columns; let breakpoint; // Use CSS media queries to detect actual breakpoint if (window.matchMedia('(max-width: 480px)').matches) { columns = window.layoutConfig.columns_mobile || 1; breakpoint = 'mobile (≤480px)'; } else if (window.matchMedia('(max-width: 768px)').matches) { columns = window.layoutConfig.columns_tablet || 2; breakpoint = 'tablet (≤768px)'; } else { columns = window.layoutConfig.columns_desktop || 3; breakpoint = 'desktop (>768px)'; } carouselElement.style.carouselTemplateColumns = `repeat(${columns}, 1fr)`; } // Render widget function renderWidget() { // Multi-location: Update business data based on current load_reviews config businessData = getBusinessData(); // CRITICAL FIX: Don't reset state during load more operations if (!getState().isLoading) { // For carousel, use all available reviews up to reviews_to_show limit const currentDisplayCount = Math.max(getState().currentDisplayCount || 0, window.layoutConfig.reviews_to_show || 12); const filteredReviewIds = getFilteredReviews(); setState({ currentDisplayCount, filteredReviewIds }); } const container = document.getElementById(`gr-carousel-container-${instanceId}`); if (!container) return; // Check if widget should be hidden (when no reviews found and fallback is 'hide') if (getState().filteredReviewIds.length === 0) { const fallbackBehavior = window.layoutConfig.fallback_behavior || 'all'; if (fallbackBehavior === 'hide') { // Hide the main widget element, not just the container const mainWidget = document.getElementById(instanceId); if (mainWidget) { mainWidget.style.display = 'none'; } else { container.style.display = 'none'; } return; } } else { // Ensure widget is visible if there are reviews const mainWidget = document.getElementById(instanceId); if (mainWidget) { mainWidget.style.display = ''; } else { container.style.display = ''; } } // Set widget title in separate container const titleContainer = document.getElementById(`gr-widget-title-${instanceId}`); if (titleContainer) { titleContainer.innerHTML = generateWidgetTitle(); } // Set main content (business header + carousel) container.innerHTML = generateWidgetHTML(); // Set bullets in separate container const bulletsContainer = document.getElementById(`gr-bullets-${instanceId}`); if (bulletsContainer && (window.layoutConfig.show_bullets ?? true)) { bulletsContainer.innerHTML = `
        `; } else if (bulletsContainer) { bulletsContainer.innerHTML = ''; } // Set footer in separate container const footerContainer = document.getElementById(`gr-footer-${instanceId}`); if (footerContainer) { footerContainer.innerHTML = generateGoogleFooter(); } applyCSSVariables(); // Apply custom CSS if provided if (window.layoutConfig.custom_css) { applyCustomCSS(window.layoutConfig.custom_css); } // Initialize carousel after DOM is updated setTimeout(async () => { applyResponsiveGrid(); await initCarousel(); }, 50); } // Apply custom CSS with proper scoping function applyCustomCSS(customCSS) { let existingStyle = document.getElementById(`custom-css-${instanceId}`); if (existingStyle) { existingStyle.remove(); } const style = document.createElement('style'); style.id = `custom-css-${instanceId}`; // Scope CSS to widget instance const scopedCSS = customCSS.replace(/([^{}]+){/g, function(match, selector) { if (selector.includes(`#${instanceId}`)) { return match; } return `#${instanceId} ${selector}{`; }); style.textContent = scopedCSS; document.head.appendChild(style); } // Global functions for interactivity with translations window[`toggleReviewText_${instanceId.replace(/-/g, '_')}`] = function(reviewId) { // If full text mode is enabled, show more buttons shouldn't exist if (window.layoutConfig.show_full_text ?? false) { return; } // Find ALL cards with this review ID (including clones) const cards = document.querySelectorAll(`#${instanceId} [data-review-id="${reviewId}"]`); if (!cards.length) return; // Find the original (non-cloned) card to determine current state let originalCard = null; let currentState = 'collapsed'; // default state cards.forEach(card => { const slide = card.closest('.splide__slide'); if (!slide || !slide.classList.contains('splide__slide--clone')) { originalCard = card; const full = card.querySelector('.gr-text-full'); currentState = (full && full.style.display === 'block') ? 'expanded' : 'collapsed'; } }); // If no original card found, use the first available card to determine state if (!originalCard && cards.length > 0) { const full = cards[0].querySelector('.gr-text-full'); currentState = (full && full.style.display === 'block') ? 'expanded' : 'collapsed'; } // Toggle state for ALL cards (original + clones) to keep them in sync const newState = currentState === 'expanded' ? 'collapsed' : 'expanded'; cards.forEach(card => { const preview = card.querySelector('.gr-text-preview'); const full = card.querySelector('.gr-text-full'); const button = card.querySelector('.gr-read-more'); if (preview && full && button) { if (newState === 'expanded') { preview.style.display = 'none'; full.style.display = 'block'; button.textContent = translations.show_less; } else { preview.style.display = 'block'; full.style.display = 'none'; button.textContent = translations.show_more; } } }); }; // Expose functions globally for customizer access window.applyCSSVariables = applyCSSVariables; window.renderWidget = renderWidget; window.getFilteredReviews = getFilteredReviews; // Initialize widget function init() { // Initialize filtered results setState({ filteredReviewIds: getFilteredReviews() }); // Small delay to ensure DOM is ready setTimeout(() => { renderWidget(); }, 100); // Re-render on window resize for responsive design let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { applyCSSVariables(); applyResponsiveGrid(); }, 250); }); } // Start the widget if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Initialize carousel after widget is rendered async function initCarousel() { const carouselElement = document.querySelector(`#gr-reviews-carousel-${instanceId}`); if (!carouselElement) { return; } // Get instance-specific config const config = window.widgetConfigs[instanceId]; if (!config) { return; } // Check if marquee mode is enabled const isMarqueeMode = config.marquee_mode ?? false; if (isMarqueeMode) { await initializeSplideMarquee(); } else { await initializeSplideNormal(); } } // Splide.js marquee implementation with AutoScroll extension async function initializeSplideMarquee() { const carouselElement = document.querySelector(`#gr-reviews-carousel-${instanceId}`); if (!carouselElement) { return; } // Get instance-specific config const config = window.widgetConfigs[instanceId]; if (!config) { return; } try { // Add splide class carouselElement.classList.add('splide'); // Configuration for marquee mode const autoplaySpeed = config.autoplay_speed || 5000; const hasAutoplay = config.autoplay === true; const perView = config.slides_per_view_desktop || 3; const gap = config.gap_spacing || 20; // Check if marquee mode is enabled const isMarqueeMode = config.marquee_mode || false; // Use direct marquee speed for AutoScroll (only if marquee mode and autoplay) const scrollSpeed = hasAutoplay && isMarqueeMode ? (config.marquee_speed || 1) : 0; const splideConfig = { type: 'loop', direction: config.widget_direction || 'ltr', // Critical: RTL support perPage: perView, gap: gap, arrows: config.show_arrows !== false, pagination: false, // Use custom bullets drag: true, speed: 1000, easing: 'linear', breakpoints: { 800: { perPage: config.slides_per_view_tablet || 2 }, 480: { perPage: config.slides_per_view_mobile || 1 } } }; // Add AutoScroll configuration ONLY if marquee mode and autoplay are enabled if (hasAutoplay && isMarqueeMode) { splideConfig.autoScroll = { speed: scrollSpeed, pauseOnHover: config.hover_pause !== false, pauseOnFocus: true, autoStart: true, rewind: true }; } else if (hasAutoplay && !isMarqueeMode) { // Normal autoplay configuration splideConfig.autoplay = true; splideConfig.interval = autoplaySpeed; splideConfig.pauseOnHover = config.hover_pause ?? true; } // Initialize Splide const splide = new Splide(carouselElement, splideConfig); // Mount with AutoScroll extension ONLY if marquee mode is enabled if (hasAutoplay && isMarqueeMode && window.splide?.Extensions?.AutoScroll) { splide.mount({ AutoScroll: window.splide.Extensions.AutoScroll }); } else { splide.mount(); } // Store instance for this widget instance window[`splideInstance_${instanceId}`] = splide; // Setup custom bullets if needed if (window.layoutConfig.show_bullets ?? true) { setupCarouselBullets(); } } catch (error) { // Fallback to normal mode if marquee fails await initializeSplideNormal(); } } // Simple Splide.js normal carousel implementation async function initializeSplideNormal() { const carouselElement = document.querySelector(`#gr-reviews-carousel-${instanceId}`); if (!carouselElement) { return; } // Get instance-specific config const config = window.widgetConfigs[instanceId]; if (!config) { return; } try { // Simple, clean Splide setup carouselElement.classList.add('splide'); // Basic configuration - keep it simple! const autoplaySpeed = config.autoplay_speed || 5000; const hasAutoplay = config.autoplay === true; const splideConfig = { type: 'carousel', perPage: config.slides_per_view_desktop || 3, perMove: 1, // Move one slide at a time for autoplay gap: config.gap_spacing || 20, autoplay: hasAutoplay, interval: autoplaySpeed, pauseOnHover: config.hover_pause !== false, arrows: config.show_arrows !== false, pagination: false, // Use custom bullets rewind: true, breakpoints: { 800: { perPage: config.slides_per_view_tablet || 2 }, 480: { perPage: config.slides_per_view_mobile || 1 } } }; // Initialize Splide const splide = new Splide(carouselElement, splideConfig); splide.mount(); // Store instance for this widget instance window[`splideInstance_${instanceId}`] = splide; // Setup custom bullets if needed if (config.show_bullets ?? true) { setupCarouselBullets(); } } catch (error) { } } // Setup carousel bullets/pagination function setupCarouselBullets() { const bulletsContainer = document.querySelector(`#${instanceId} .splide__bullets`); const slidesCount = document.querySelectorAll(`#gr-carousel-slides-${instanceId} .splide__slide`).length; if (!bulletsContainer || slidesCount === 0) return; // Calculate number of bullets needed based on actual filtered reviews count const config = window.widgetConfigs[instanceId]; const perView = config.slides_per_view_desktop || 3; const reviewsToShow = config.reviews_to_show || 12; // Get actual filtered count const filteredCount = getState().filteredReviewIds ? getState().filteredReviewIds.length : slidesCount; const actualReviewsCount = Math.min(Math.min(slidesCount, filteredCount), reviewsToShow); const bulletsNeeded = Math.ceil(actualReviewsCount / perView); // Generate bullets let bulletsHTML = ''; for (let i = 0; i `; } bulletsContainer.innerHTML = bulletsHTML; // Add event listeners for bullet clicks const bullets = bulletsContainer.querySelectorAll('.splide__bullet'); bullets.forEach((bullet, index) => { bullet.addEventListener('click', () => { if (window[`splideInstance_${instanceId}`]) { window[`splideInstance_${instanceId}`].go(index * perView); } }); }); // Add event listener for carousel movement to update active bullet if (window[`splideInstance_${instanceId}`]) { window[`splideInstance_${instanceId}`].on('moved', () => { updateActiveBullet(); }); } } // Update active bullet based on current slide position function updateActiveBullet() { const bulletsContainer = document.querySelector(`#${instanceId} .splide__bullets`); if (!bulletsContainer || !window[`splideInstance_${instanceId}`]) return; const bullets = bulletsContainer.querySelectorAll('.splide__bullet'); const currentIndex = window[`splideInstance_${instanceId}`].index; const config = window.widgetConfigs[instanceId]; const perView = config.slides_per_view_desktop || 3; const activeBulletIndex = Math.floor(currentIndex / perView); // Remove active class from all bullets bullets.forEach(bullet => bullet.classList.remove('splide__bullet--active')); // Add active class to current bullet if (bullets[activeBulletIndex]) { bullets[activeBulletIndex].classList.add('splide__bullet--active'); } } })();
        Team at ALAM87 ready to help

        ALAM87: Website Modern dengan Sistem Akses Super Cepat

        1

        Kualitas teruji & terjamin

        Setiap perangkat melewati 40 titik pengecekan kualitas untuk memastikan fungsinya optimal, siap pakai, dan tahan lama.

        2

        Garansi 365 hari*

        Pesanan tidak sesuai deskripsi? Kamu punya waktu 365 hari untuk retur gratis. Prosesnya cepat dan mudah.

        3

        Unit 100% resmi Indonesia

        Semua unit sudah di-reset ke pengaturan pabrik dan bebas akun pemilik lama. Siap dipakai seperti baru.



        Lihat Lagi yang Kamu Suka

        ALAM87: Website Modern dengan Sistem Akses Super Cepat

        ALAM87 merupakan website modern yang dirancang untuk menghadirkan pengalaman digital yang cepat, praktis, dan nyaman bagi para penggunanya.