/** * Client-side buyability — mirrors PHP product_availability_utils (4-month feed staleness). * Cards always link to the on-site product page; unavailable state is product-detail only. */ const STALE_FEED_MONTHS = 4; const MS_PER_MONTH = 1000 * 60 * 60 * 24 * 30.44; function parseFeedSeenDate(product) { const raw = String(product?.last_feed_seen || '').trim(); if (!raw) return null; const ts = Date.parse(raw); return Number.isFinite(ts) ? ts : null; } function hasPurchasableOffers(product) { const offers = product?.offers || []; if (offers.some(o => Number(o?.price) > 0 && (o?.aff_url || o?.url))) { return true; } return (product?.variants || []).some(v => (v?.offers || []).some(o => Number(o?.price) > 0 && (o?.aff_url || o?.url))); } function isStaleInFeed(product, months = STALE_FEED_MONTHS) { const seenTs = parseFeedSeenDate(product); if (seenTs === null) { return !hasPurchasableOffers(product); } return (Date.now() - seenTs) / MS_PER_MONTH >= months; } function isProductBuyable(product) { if (product?.unavailable) return false; if (isStaleInFeed(product)) return false; return hasPurchasableOffers(product); } function renderBuyBtnHtml(product, pageUrl) { const url = pageUrl || (typeof getProductPageUrl === 'function' ? getProductPageUrl(product) : '#'); if (!url || url === '#') { return 'Show Me'; } return `Show Me`; } function applyBuyBtnToCard($card, product, pageUrl) { const url = pageUrl || (typeof getProductPageUrl === 'function' ? getProductPageUrl(product) : '#'); const $btn = $card.find('.buy-btn, .buy-btn-unavailable'); if (!url || url === '#') { if ($btn.length) { $btn.replaceWith('Show Me'); } return; } if ($btn.is('span')) { $btn.replaceWith(`Show Me`); } else if ($btn.length) { $btn.attr('href', url).text('Show Me').removeClass('buy-btn-unavailable').removeAttr('aria-disabled target rel'); } else { $card.find('.actions').after(`Show Me`); } } 'use strict'; const PRODUCTS_PER_PAGE = 20; let allProducts = []; let originalProducts = []; let currentPage = 1; let totalPages = 1; let isLoading = false; let filtersDirty = false; let listingClientMode = false; const PRODUCT_REVIEWS_LOOKUP = {"mac-powder-kiss-velvet-blur-slim-stick-2g-890-wild-sumac":{"reviewUrl":"/articles/lips/2026-06-11-mac-powder-kiss-velvet-blur-slim-stick-2g-890-wild-sumac-review/","authorPhoto":"/articles/authors/carla.png","authorName":"Carla Quinn","authorKey":"carla","authorVideo":"/articles/authors/carla.mp4"}}; // DOM Elements const $filterBtnLabel = $('#floatingFilterBtn .filter-btn-label'); const $loading = $('#loadingState'); const $error = $('#errorState'); const $hero = $('#heroSection'); const $productsGrid = $('#productsGrid'); const $noResults = $('#noResults'); const $pagination = $('#pagination'); const $loadMoreBtn = $('#loadMoreBtn'); const $resultsCounter = $('#resultsCounter'); // LOCAL STORAGE — prefixed with "uklips_" to avoid conflict with old domain let compareList = JSON.parse(localStorage.getItem('uklips_compareList')) || []; let favorites = JSON.parse(localStorage.getItem('uklips_favorites')) || []; let watchedVideos = JSON.parse(localStorage.getItem('uklips_watchedlip-primerVideos')) || []; // Brand helpers for filter dropdown function escapeHtmlAttr(str) { return String(str) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function productSafeBrand(product) { const safe = typeof product?.safe_brand === 'string' ? product.safe_brand.trim() : ''; if (safe) return safe; const display = typeof product?.brand === 'string' ? product.brand.trim() : ''; if (!display) return ''; return display.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } function productBrandDisplay(product) { return typeof product?.brand === 'string' ? product.brand.trim() : ''; } // Match PHP listing_filter_ranked_products — rank > 0, not unavailable, brand + image. function isLiveProduct(p) { if (!p || (p.rank ?? 0) <= 0) return false; if (p.unavailable) return false; const brand = typeof p.brand === 'string' ? p.brand.trim() : ''; if (!brand) return false; const filename = typeof p.filename === 'string' ? p.filename.trim() : ''; const imageUrl = p.image_url != null ? String(p.image_url).trim() : ''; return filename !== '' || imageUrl !== ''; } // =============================================== // 1. UTILITY: SHORT NAME // =============================================== function getShortName(fullName) { return fullName.split(/[,-\.]/)[0].trim(); } function getProductThumb(p) { if (p.image_thumb_local) return p.image_thumb_local; if (p.images_success && p.filename && p.safe_brand) { return `/lip-primer/${p.safe_brand}/i/${p.filename}-card.webp`; } if (p.image_thumb_jpg_local) return p.image_thumb_jpg_local; if (p.image_url_local) { const local = String(p.image_url_local); if (local.includes('/i/') && local.endsWith('-1.jpg')) { return local.replace(/-1\.jpg$/, '-card.webp'); } return local; } if (p.image_url && String(p.image_url).trim()) return p.image_url.trim(); if (p.filename && p.safe_brand) { return `/lip-primer/${p.safe_brand}/i/${p.filename}-card.webp`; } return '/images/lip-stain.jpg'; } function getProductDetailImage(p, seq = 1) { if (p.image_url_webp && seq === 1) return p.image_url_webp; if (p.images_success && p.filename && p.safe_brand) { return `/lip-primer/${p.safe_brand}/i/${p.filename}-${seq}.webp`; } if (p.image_url_local && seq === 1) return p.image_url_local; if (p.filename && p.safe_brand) { return `/lip-primer/${p.safe_brand}/i/${p.filename}-${seq}.jpg`; } return getProductThumb(p); } function getProductSwatchUrl(p, shade) { if (shade?.image_url && String(shade.image_url).startsWith('/')) return shade.image_url; if (p.images_success && p.filename && p.safe_brand) { return `/lip-primer/${p.safe_brand}/i/${p.filename}-swatch.jpg`; } if (shade?.image_url) return shade.image_url; return getProductThumb(p); } function getProductImageUrls(p) { if (p.images_success && p.filename && p.safe_brand) { const count = Number(p.imagecnt) || 1; const urls = []; for (let i = 1; i <= count; i++) { urls.push(`/lip-primer/${p.safe_brand}/i/${p.filename}-${i}.webp`); } return urls; } if (Array.isArray(p.image_urls) && p.image_urls.length) return p.image_urls; if (p.image_url_webp) return [p.image_url_webp]; if (p.image_url_local) return [p.image_url_local]; if (p.image_url && String(p.image_url).trim()) return [p.image_url.trim()]; if (!p.filename) return ['/images/lip-stain.jpg']; const base = p.safe_brand ? `/lip-primer/${p.safe_brand}/i/${p.filename}` : `/lip-primer/_img/${p.filename}`; const count = Number(p.imagecnt) || 1; const urls = []; for (let i = 1; i <= count; i++) urls.push(`${base}-${i}.jpg`); return urls; } function getActiveVariant(product, index = 0) { if (product?.is_group && Array.isArray(product.variants) && product.variants.length) { return product.variants[index] || product.variants[0]; } if (Array.isArray(product?.variants) && product.variants[index]) { return product.variants[index]; } return product; } function getColourCarouselSlides(product) { if (Array.isArray(product?.shades) && product.shades.length > 1) { return product.shades.map((shade, i) => { const variant = getActiveVariant(product, shade.variant_index ?? i); return { image_url: (shade.image_url_local || getProductThumb(variant)), swatch_url: getProductSwatchUrl(variant, shade), name: shade.name || '', variant_index: shade.variant_index ?? i }; }); } return getProductImageUrls(product).map((url) => ({ image_url: url, swatch_url: url, name: product.shade || '', variant_index: 0 })); } function getDisplayName(product) { return product.display_name || product.name || ''; } function getProductImageAlt(product, variant, extra = '') { const brand = (variant?.brand || product.brand || '').trim(); const name = getDisplayName(variant || product); const base = brand ? `${brand} ${name}` : name; const suffix = (extra || '').trim(); if (suffix && !base.toLowerCase().includes(suffix.toLowerCase())) { return `${base} – ${suffix}`; } return base; } function getProductPageSlug(product) { if (product.page_slug) return product.page_slug; if (product.filename) return String(product.filename).toLowerCase(); return ''; } function getAffiliateGoUrl(product, variantIndex = 0, source = 'card') { const slug = getProductPageSlug(product); if (!slug) return '#'; const params = new URLSearchParams({ c: 'lip-primer', p: slug, src: source }); if (variantIndex > 0) params.set('s', String(variantIndex)); return `/go/?${params.toString()}`; } function getProductPageUrl(product) { const slug = getProductPageSlug(product); if (!slug) return '#'; return `/lip-primer/p/${slug}`; } function renderAuthorBadgeMedia(rev, size) { const photo = String(rev.authorPhoto || '').replace(/"/g, '"'); const video = String(rev.authorVideo || '').replace(/"/g, '"'); if (!video) { return ``; } return `` + `` + `` + ``; } function renderProductReviewBadge(product) { const slug = getProductPageSlug(product); if (!slug) return ''; const rev = PRODUCT_REVIEWS_LOOKUP[slug]; if (!rev || !rev.reviewUrl) return ''; const name = String(rev.authorName || 'UK Lips').replace(/"/g, '"'); const url = String(rev.reviewUrl).replace(/"/g, '"'); return `` + renderAuthorBadgeMedia(rev, 36) + `Review` + ``; } function getCategoryPageUrl() { return `/lip-primer/`; } function getBrandPageUrl(safeBrand) { if (!safeBrand) return ''; return `/lip-primer/b/${safeBrand}/`; } function renderCardMetaHtml(lip) { const catLink = `Lip Primer`; const brand = (lip.brand || '').trim(); const safeBrand = (lip.safe_brand || '').trim(); if (!brand) { return `
${catLink}
`; } const brandEsc = brand.replace(/${brandEsc}` : brandEsc; return `
${catLink}${brandLink}
`; } function renderShadesHtml(lip) { const shades = lip.shades || []; if (!shades.length) { const legacy = (lip.shades || []).filter(s => s.color); if (!legacy.length) { return '
'; } } const maxDisplay = 8; const visible = shades.slice(0, maxDisplay); const hasMore = shades.length > maxDisplay; const swatches = visible.map((s, i) => { const idx = s.variant_index ?? i; const variant = getActiveVariant(lip, idx); const label = (s.name || `Shade ${i + 1}`).replace(/"/g, '"'); const swatchUrl = getProductSwatchUrl(variant, s); const style = s.color ? `background:${s.color}` : `background-image:url("${swatchUrl}")`; return ``; }).join(''); const selectedName = shades[0]?.name || lip.shade || ''; return `
${swatches}${hasMore ? `+${shades.length - maxDisplay}` : ''}
${selectedName}
`; } function parseCompareDataFromCard($card) { const id = +$card.data('id'); const name = ($card.find('.product-name-link').text() || $card.find('.product-name').text() || '').trim(); const priceText = ($card.find('.price span').first().text() || '').replace(/[£,\s]/g, ''); const price = parseFloat(priceText) || 0; const thumb = $card.find('.product-thumb, .listing-pick-img img').first().attr('src') || '/images/lip-stain.jpg'; const pageUrl = $card.find('.product-name-link, .product-img-link, .listing-pick-img-link').first().attr('href') || '#'; const ratingText = ($card.find('.rating-value, .listing-pick-rating').first().text() || '').replace(/[^\d.]/g, ''); const rating = parseFloat(ratingText) || 0; const reviewsText = ($card.find('.review-count').first().text() || '').replace(/[^\d]/g, ''); const reviews = parseInt(reviewsText, 10) || 0; const brand = ($card.find('.product-card-brand').text() || '').trim(); return { id, name, brand, price, rating, reviews, shades: $card.find('.shade-swatch').length, rank: null, thumb, pageUrl }; } function findCompareProduct(id) { const fromList = allProducts.find(x => x.id === id); if (fromList) return getProductCompareData(fromList); const $card = $(`.product-card[data-id="${id}"], .listing-pick-card[data-id="${id}"]`).first(); return $card.length ? parseCompareDataFromCard($card) : null; } function getProductCompareData(product) { const variant = getActiveVariant(product, 0); return { id: product.id, name: getDisplayName(product), brand: (product.brand || variant.brand || '').trim(), price: Number(variant.price ?? variant.min_total_price ?? product.price ?? 0), rating: Number(product.rating ?? variant.rating ?? 0), reviews: Number(product.reviews ?? variant.reviews ?? 0), shades: (product.shades || []).length, rank: product.rank ?? null, thumb: getProductThumb(variant), pageUrl: getProductPageUrl(product) }; } function pruneCompareList() { const validIds = new Set(); $('.product-card[data-id], .listing-pick-card[data-id]').each(function() { validIds.add(+$(this).data('id')); }); allProducts.forEach(p => validIds.add(p.id)); const before = compareList.length; compareList = compareList.filter(id => validIds.has(id)); if (compareList.length !== before) saveLists(); } function syncCompareButtons() { $('.compare-btn').each(function() { const id = +$(this).data('id'); const isComp = compareList.includes(id); $(this) .toggleClass('added', isComp) .attr('aria-pressed', isComp ? 'true' : 'false') .attr('aria-label', isComp ? 'Remove from comparison' : 'Add to comparison') .attr('title', isComp ? 'Remove from comparison' : 'Compare'); }); } function showCompareToast(message) { let $toast = $('#compareToast'); if (!$toast.length) { $toast = $('
'); $('body').append($toast); } $toast.text(message).addClass('show'); clearTimeout(showCompareToast._timer); showCompareToast._timer = setTimeout(() => $toast.removeClass('show'), 2800); } function toggleCompare(id) { const idx = compareList.indexOf(id); if (idx > -1) { compareList.splice(idx, 1); } else { if (compareList.length >= 4) { showCompareToast('You can compare up to 4 products'); return false; } compareList.push(id); showCompareToast('Added to comparison'); } saveLists(); syncCompareButtons(); updateComparisonBar(); return true; } function applyVariantToCard($card, product, variantIndex) { if (window.CardCarousel) { CardCarousel.applyVariantToCard($card[0], product, variantIndex); return; } const variant = getActiveVariant(product, variantIndex); const shade = product.shades?.find(s => (s.variant_index ?? 0) === variantIndex) || product.shades?.[variantIndex]; $card.data('active-variant', variantIndex); $card.find('.product-thumb').attr('src', getProductThumb(variant)); $card.find('.product-thumb').attr('alt', getProductImageAlt(product, variant, shade?.name || variant.shade || '')); $card.find('.price').text(`£${Number(variant.price || 0).toFixed(2)}`); applyBuyBtnToCard($card, product, getProductPageUrl(product)); $card.find('.selected-shade-name').text(shade?.name || variant.shade || product.shade || ''); $card.find('.shade-swatch').removeClass('active'); $card.find(`.shade-swatch[data-variant-index="${variantIndex}"]`).addClass('active'); } function isSSRListing() { return document.querySelector('#mainContent[data-ssr="1"]') !== null; } function hasSSRGrid() { return $('#productsGrid .product-card').length > 0; } function attachProductCardHandlers() { $('.product-card .shade-swatch').off('click').on('click', function(e) { e.stopPropagation(); e.preventDefault(); const $swatch = $(this); const $card = $swatch.closest('.product-card'); const productId = $card.data('id'); let product = allProducts.find(p => p.id == productId); const variantIndex = Number($swatch.data('variant-index')) || 0; if (product) { applyVariantToCard($card, product, variantIndex); return; } const swatchStyle = $swatch.attr('style') || ''; const bgMatch = swatchStyle.match(/url\((['"]?)(.*?)\1\)/); if (bgMatch && bgMatch[2]) { $card.find('.product-thumb').attr('src', bgMatch[2]); } $card.data('active-variant', variantIndex); $card.find('.shade-swatch').removeClass('active'); $swatch.addClass('active'); }); $('.compare-btn').off('click').on('click', function(e) { e.preventDefault(); e.stopPropagation(); toggleCompare(+$(this).data('id')); }); $('.fav-btn').off('click').on('click', function() { const id = +$(this).data('id'); const idx = favorites.indexOf(id); if (idx > -1) favorites.splice(idx, 1); else favorites.push(id); $(this).toggleClass('active'); saveLists(); }); if (window.UK_LIPS_initAuthorBadgeVideos) { window.UK_LIPS_initAuthorBadgeVideos(document); } } // =============================================== // 2. INITIAL LOAD // =============================================== async function load_products() { if (!isSSRListing()) { $loading.hide(); $error.show().text('Failed to load products. Please try again.'); return; } const urlParams = new URLSearchParams(window.location.search); const page = Math.max(1, parseInt(urlParams.get('page')) || 1); initApp(page); } // =============================================== // 3. INIT APP // =============================================== function initApp(startPage = 1) { currentPage = startPage; pruneCompareList(); if (window.CardCarousel) { CardCarousel.configure({ products: allProducts, categorySlug: 'lip-primer' }); CardCarousel.init(); } const $sortSelect = $('#sortSelect'); const urlParams = new URLSearchParams(window.location.search); const urlSort = urlParams.get('sort'); const validSorts = ['rank','price-low','price-high','name','rating-high','reviews-high']; const defaultSort = validSorts.includes(urlSort) ? urlSort : 'rank'; if ($sortSelect.length) $sortSelect.val(defaultSort); if (!isSSRListing()) { populateBrandFilter(); renderBrandButtons(); } if (filter_by_brand && filter_by_brand.trim() !== '') { $('#brandFilter').val(filter_by_brand.trim()); } if (isSSRListing() && !listingClientMode) { const $main = $('#mainContent'); totalPages = parseInt($main.data('listing-total-pages') || String(totalPages), 10); currentPage = parseInt($main.data('listing-page') || String(startPage), 10); attachProductCardHandlers(); syncCompareButtons(); observeCards(); $loading.hide(); $hero.show(); renderVideoButtons(); updateComparisonBar(); if (!$('.listing-top-picks .listing-pick-card').length) renderTopProducts(); $('#pagination').hide(); $('#filterPanel').removeClass('active'); $('#filterBackdrop').removeClass('active').attr('aria-hidden', 'true'); $('#floatingFilterBtn').removeClass('active').attr('aria-expanded', 'false'); resetFilterButtonLabel(); updateFilterButtonLabel(); return; } applyFilters(); $loading.hide(); $hero.show(); displayPage(currentPage); renderTopProducts(); renderVideoButtons(); updateComparisonBar(); if (!listingClientMode) updateURLAndSEO(currentPage); if (!listingClientMode && !hasSSRGrid()) populateTop3InSchema(); $('#filterPanel').removeClass('active'); $('#filterBackdrop').removeClass('active').attr('aria-hidden', 'true'); $('#floatingFilterBtn').removeClass('active').attr('aria-expanded', 'false'); resetFilterButtonLabel(); updateFilterButtonLabel(); } // =============================================== // 3.5 BRAND FILTER + BUTTONS // =============================================== function populateBrandFilter() { const brandMap = new Map(); allProducts.forEach(product => { if (!isLiveProduct(product)) return; const safeBrand = productSafeBrand(product); if (!safeBrand) return; const displayName = productBrandDisplay(product) || safeBrand; if (!brandMap.has(safeBrand)) { brandMap.set(safeBrand, displayName); return; } const existing = brandMap.get(safeBrand); if (displayName.length > existing.length) brandMap.set(safeBrand, displayName); }); const $brandSelect = $('#brandFilter').empty(); $brandSelect.append(''); Array.from(brandMap.entries()) .sort((a, b) => a[1].localeCompare(b[1])) .forEach(([safeBrand, displayName]) => { $brandSelect.append( `` ); }); } function renderBrandButtons() { const $grid = $('#brandGrid').empty(); // Use a Set to keep unique safe_brand values const safeBrands = new Set(); allProducts.forEach(product => { if (product.safe_brand && typeof product.safe_brand === 'string' && product.safe_brand.trim() !== '') { safeBrands.add(product.safe_brand.trim()); } }); const sortedBrands = Array.from(safeBrands).sort((a, b) => a.localeCompare(b)); if (sortedBrands.length === 0) { $grid.append('

No brands found.

'); return; } sortedBrands.forEach(safeBrandSlug => { const href = `/lip-primer/b/` + safeBrandSlug; // Optional: still show the nice brand name (not the slug) on the button // Find a product with this safe_brand to get the display name const sampleProduct = allProducts.find(p => p.safe_brand && p.safe_brand.trim() === safeBrandSlug ); const displayName = sampleProduct ? (sampleProduct.brand || safeBrandSlug) : safeBrandSlug; const $btn = $(`${displayName}`); $grid.append($btn); }); } // =============================================== // 4. DISPLAY PAGE // =============================================== function displayPage(page = 1, append = false) { const start = (page - 1) * PRODUCTS_PER_PAGE; const end = Math.min(start + PRODUCTS_PER_PAGE, allProducts.length); const pageProducts = allProducts.slice(start, end); if (!append) { $productsGrid.empty(); currentPage = page; } renderLipsticks(pageProducts, append); const shown = Math.min(end, allProducts.length); $resultsCounter.text(`Showing ${start + 1}–${shown} of ${allProducts.length} lip primer`); if (end < allProducts.length) { $pagination.show(); $loadMoreBtn.prop('disabled', false).html('Load More '); } else { $pagination.hide(); } if (!append) updateURLAndSEO(page); isLoading = false; } // =============================================== // 5. UPDATE URL + SEO // =============================================== function updateURLAndSEO(page) { if (isSSRListing()) return; const sort = $('#sortSelect').val(); const currentPath = window.location.pathname; const basePath = currentPath.replace(/\/$/, ''); const params = new URLSearchParams(); if (page > 1) params.set('page', page); if (sort && sort !== 'rank') params.set('sort', sort); const queryString = params.toString(); const newUrl = queryString ? `${basePath}?${queryString}` : basePath || '/'; history.replaceState({ page, sort }, '', newUrl); $('link[rel="next"], link[rel="prev"]').remove(); if (page > 1) { const prevParams = new URLSearchParams(); const prevPage = page - 1; if (prevPage > 1) prevParams.set('page', prevPage); if (sort && sort !== 'rank') prevParams.set('sort', sort); const prevQuery = prevParams.toString(); const prevUrl = prevQuery ? `${basePath}?${prevQuery}` : basePath || '/'; $('head').append(``); } if (page < totalPages) { const nextParams = new URLSearchParams(); nextParams.set('page', page + 1); if (sort && sort !== 'rank') nextParams.set('sort', sort); const nextUrl = `${basePath}?${nextParams.toString()}`; $('head').append(``); } } // =============================================== // 6. LOAD MORE // =============================================== $(document).on('click', '#loadMoreBtn', function() { if (isLoading) return; isLoading = true; $loadMoreBtn.prop('disabled', true).html('Loading...'); const nextPage = currentPage + 1; setTimeout(() => { displayPage(nextPage, true); currentPage = nextPage; updateURLAndSEO(currentPage); $loadMoreBtn.prop('disabled', false).html('Load More '); isLoading = false; }, 300); }); // =============================================== // 7. RATING FUNCTION // =============================================== function renderRating(lip) { const rating = Number(lip.rating) || 0; const reviews = Number(lip.reviews) || 0; if (rating <= 0 && reviews <= 0) return ''; const title = lip.rating_source === 'amazon' ? 'Amazon customer rating' : lip.rating_source === 'feed' ? 'Merchant feed rating' : lip.rating_source === 'brand' ? 'Based on brand reputation score' : ''; const full = Math.floor(rating); const half = rating % 1 >= 0.5 ? 1 : 0; const empty = 5 - full - half; const ratingLabel = rating > 0 ? `${rating.toFixed(1)}` : ''; const reviewLabel = reviews > 0 ? `(${reviews.toLocaleString()})` : ''; return `
${''.repeat(full)} ${half ? '' : ''} ${''.repeat(empty)}
${ratingLabel} ${reviewLabel}
`; } function formatReviewCount(reviews) { const count = Number(reviews) || 0; if (count <= 0) return ''; return count >= 1000 ? `${(count / 1000).toFixed(1)}k reviews` : `${count.toLocaleString()} reviews`; } function renderTopPickCard(lip, rank) { const active = getActiveVariant(lip, 0); const displayName = getDisplayName(lip); const thumb = getProductThumb(active); const pageUrl = getProductPageUrl(lip); const price = Number(active.price || lip.price || 0).toFixed(2); const rating = Number(lip.rating) || 0; const brand = (lip.brand || '').trim(); const safeBrand = (lip.safe_brand || '').trim(); const brandEsc = brand.replace(/${brandEsc}` : brandEsc; const ratingHtml = rating > 0 ? ` ${rating.toFixed(1)}` : ''; const titleHtml = pageUrl !== '#' ? `${displayName.replace(/` : displayName.replace(/ 1 ? colourSlides.map((slide, i) => ` `).join('') : ''; const carouselHtml = carouselSlides ? `` : ''; return `
#${rank}
${imgAlt} ${carouselHtml}
${renderProductReviewBadge(lip)}

Lip Primer

${titleHtml}

${brand ? `

${brandHtml}

` : ''}
£${price} ${ratingHtml}
`; } // =============================================== // 8. TOP 3 PICKS // =============================================== function renderTopProducts() { const $section = $('.listing-top-picks'); const $container = $('#topProductsContainer'); const $count = $('#totalProductsCount'); if (allProducts.length < 4) { $section.hide(); return; } $section.show(); if (!$container.length) return; const top3 = allProducts.slice(0, 3); $container.empty(); if (top3.length === 0) { $container.append('

No products found.

'); if ($count.length) $count.text('0'); return; } top3.forEach((lip, index) => { $container.append(renderTopPickCard(lip, index + 1)); }); if ($count.length) $count.text(allProducts.length); if (window.CardCarousel) CardCarousel.observeCards(); } // =============================================== // 9. RENDER MAIN GRID // =============================================== function renderLipsticks(data, append = false) { if (!append && (!data || data.length === 0)) { $productsGrid.empty(); $noResults.show(); return; } if (!append) $noResults.hide(); data.forEach((lip, index) => { const isComp = compareList.includes(lip.id); const isFav = favorites.includes(lip.id); const activeVariant = getActiveVariant(lip, 0); const thumb = getProductThumb(activeVariant); const pageUrl = getProductPageUrl(lip); const colourSlides = getColourCarouselSlides(lip); const imgAlt = getProductImageAlt(lip, activeVariant).replace(/"/g, '"'); const imgLinkOpen = pageUrl !== '#' ? `` : ''; const carouselSlides = colourSlides.map((slide, i) => ` `).join(''); const card = `
${(currentPage - 1) * PRODUCTS_PER_PAGE + index + 1}
${imgLinkOpen}
${imgAlt}
${imgLinkClose} ${renderProductReviewBadge(lip)}
${renderCardMetaHtml(lip)}
£${Number(activeVariant.price || lip.price || 0).toFixed(2)}
${renderRating(lip)} ${renderShadesHtml(lip)}
${renderBuyBtnHtml(lip, pageUrl)}
`; $productsGrid.append(card); }); attachProductCardHandlers(); syncCompareButtons(); } // =============================================== // 10. VIDEO BUTTONS // =============================================== function renderVideoButtons() { const $container = $('#videoScrollContainer').empty(); const videos = allProducts.filter(p => p.videoURL); $('#videoPreviewSection').toggle(videos.length > 0); videos.forEach(p => { const isWatched = watchedVideos.includes(p.id); const thumb = getProductThumb(p); $container.append(`
${p.name}
`); }); $('.video-btn').off('click keypress').on('click keypress', function(e) { if (e.type === 'keypress' && e.key !== 'Enter') return; const id = +$(this).data('id'); const p = allProducts.find(x => x.id === id); if (p) openVideoModal(p); }); } // =============================================== // 11. COMPARISON TABLE // =============================================== function renderComparisonTable() { const $container = $('#compareTableContainer').empty(); if (compareList.length === 0) { $container.html('

No products selected. Add items using the compare button on product cards.

'); return; } const products = compareList .map(id => allProducts.find(x => x.id === id)) .filter(Boolean) .map(getProductCompareData); if (products.length === 0) { $container.html('

Selected products are no longer available on this page.

'); return; } const lowestPrice = Math.min(...products.map(p => p.price).filter(n => n > 0)); const specRows = [ { key: 'price', label: 'Price', render: p => `£${p.price.toFixed(2)}` }, { key: 'brand', label: 'Brand', render: p => p.brand || '—' }, { key: 'rating', label: 'Rating', render: p => p.rating > 0 ? ` ${p.rating.toFixed(1)}` : '—' }, { key: 'reviews', label: 'Reviews', render: p => p.reviews > 0 ? p.reviews.toLocaleString() : '—' }, { key: 'shades', label: 'Shades', render: p => p.shades > 0 ? p.shades : '—' }, { key: 'rank', label: 'Rank', render: p => p.rank ? `#${p.rank}` : '—' } ]; let html = '
'; products.forEach(p => { html += ``; }); html += ''; specRows.forEach(row => { html += ``; products.forEach(p => { html += ``; }); html += ''; }); html += '
Product

${p.name.replace(/ ${p.pageUrl !== '#' ? `View product` : ''}

${row.label}${row.render(p)}
'; $container.html(html); $container.find('.compare-remove-col').on('click', function() { toggleCompare(+$(this).data('id')); renderComparisonTable(); if (compareList.length === 0) { $('#comparisonModal').removeClass('active'); $('body').css('overflow', ''); } }); } function applyFilters() { if (isSSRListing()) { listingClientMode = true; if (window.ListingAjax) { ListingAjax.fetchAndRender(1).then(function() { filtersDirty = false; updateFilterButtonLabel(); }); } return; } allProducts = [...originalProducts]; let filtered = allProducts.filter(isLiveProduct); const search = $('#searchInput').val().toLowerCase(); if (search) { filtered = filtered.filter(l => l.name.toLowerCase().includes(search) || (l.brand && l.brand.toLowerCase().includes(search)) ); } const popular = $('#popularFilter').val(); if (popular) filtered = filtered.filter(l => l.reviews > 10000); const selectedBrand = $('#brandFilter').val(); if (selectedBrand) { filtered = filtered.filter(p => productSafeBrand(p) === selectedBrand); } const rating = $('#ratingFilter').val(); if (rating) filtered = filtered.filter(l => l.rating >= +rating); const min = parseFloat($('#minPrice').val()) || 0; const max = parseFloat($('#maxPrice').val()) || Infinity; filtered = filtered.filter(l => l.price >= min && l.price <= max); const sort = $('#sortSelect').val(); if (sort === 'price-low') filtered.sort((a,b) => a.price - b.price); else if (sort === 'price-high') filtered.sort((a,b) => b.price - a.price); else if (sort === 'name') filtered.sort((a,b) => a.name.localeCompare(b.name)); else if (sort === 'rating-high') filtered.sort((a,b) => b.rating - a.rating); else if (sort === 'reviews-high') filtered.sort((a,b) => b.reviews - a.reviews); else if (sort === 'rank') filtered.sort((a,b) => a.rank - b.rank); allProducts = filtered; totalPages = Math.ceil(allProducts.length / PRODUCTS_PER_PAGE); listingClientMode = true; $('.ssr-pagination').hide(); displayPage(1); renderTopProducts(); updateFilterButtonLabel(); populateTop3InSchema(); } function saveLists() { localStorage.setItem('uklips_compareList', JSON.stringify(compareList)); localStorage.setItem('uklips_favorites', JSON.stringify(favorites)); } function saveWatchedVideos() { localStorage.setItem('uklips_watchedlip-primerVideos', JSON.stringify(watchedVideos)); } function updateComparisonBar() { const $items = $('#comparisonItems').empty(); const count = compareList.length; $('#compareCount').text(count); const $bar = $('#comparisonBar'); if (count > 0) { $bar.addClass('show'); compareList.forEach(id => { const data = findCompareProduct(id); if (!data) return; const shortName = data.name.length > 28 ? `${data.name.slice(0, 28)}…` : data.name; $items.append(`
${shortName.replace(/
`); }); } else { $bar.removeClass('show'); } $('.remove-item').off('click').on('click', function(e) { e.stopPropagation(); toggleCompare(+$(this).data('id')); }); } function openVideoModal(p) { $('#modalVideo source').attr('src', p.videoURL); $('#modalVideo')[0].load(); $('#videoModal').addClass('active'); $('body').css('overflow', 'hidden'); if (!watchedVideos.includes(p.id)) { watchedVideos.push(p.id); saveWatchedVideos(); $(`.video-btn[data-id="${p.id}"]`).addClass('watched'); } $('#modalVideo')[0].play(); } function updateFilterButtonLabel() { const isMobile = window.innerWidth <= 767; const labelText = filtersDirty && isMobile ? 'Apply' : 'Filters'; $filterBtnLabel.text(labelText); } function openFilterPanel() { $('#filterPanel').addClass('active'); $('#filterBackdrop').addClass('active').attr('aria-hidden', 'false'); $('#floatingFilterBtn').addClass('active').attr('aria-expanded', 'true'); if (window.innerWidth <= 767) $('body').css('overflow', 'hidden'); } function closeFilterPanel(apply = false) { $('#filterPanel').removeClass('active'); $('#filterBackdrop').removeClass('active').attr('aria-hidden', 'true'); $('#floatingFilterBtn').removeClass('active').attr('aria-expanded', 'false'); $('body').css('overflow', ''); if (apply) { applyFilters(); resetFilterButtonLabel(); scrollToProductsGrid(); } } function scrollToProductsGrid() { const top = $productsGrid.offset().top - 80; $('html, body').animate({ scrollTop: top }, 600); } function applyFiltersAndClose() { closeFilterPanel(true); } function markFiltersDirty() { if (!filtersDirty) { filtersDirty = true; updateFilterButtonLabel(); } } function resetFilterButtonLabel() { filtersDirty = false; updateFilterButtonLabel(); } $(window).on('load resize', updateFilterButtonLabel); function observeCards() { if (window.CardCarousel) CardCarousel.observeCards(); } let originalDisplayPage = displayPage; displayPage = function (...args) { originalDisplayPage.apply(this, args); setTimeout(observeCards, 100); }; let currentModalProduct = null; let currentModalIndex = 0; function openImageModal(product) { currentModalProduct = product; currentModalIndex = 0; const variantIndex = Number(product._modalVariantIndex) || 0; const activeProduct = getActiveVariant(product, variantIndex); let imgUrls = []; let shadeNames = []; if (Array.isArray(product.shades) && product.shades.length > 1) { product.shades.forEach((shade, i) => { const v = getActiveVariant(product, shade.variant_index ?? i); imgUrls.push(shade.image_url_local || getProductThumb(v)); shadeNames.push(shade.name || ''); }); } else { imgUrls = getProductImageUrls(activeProduct); shadeNames = imgUrls.map(() => activeProduct.shade || product.shade || ''); } const totalImages = imgUrls.length; const $img = $('#modalImage'); const $prevBtn = $('.modal-prev'); const $nextBtn = $('.modal-next'); function loadIndex(idx) { currentModalIndex = idx; const url = imgUrls[idx]; $img.attr({ src: url, alt: `${getDisplayName(product)} – ${shadeNames[idx] || `image ${idx + 1}`}` }); $prevBtn.toggleClass('hidden', idx === 0); $nextBtn.toggleClass('hidden', idx === totalImages - 1); if (product.shades?.[idx]) { $('#modalTitle').text(getDisplayName(product)); if ($('#modalShadeName').length) { $('#modalShadeName').text(shadeNames[idx] || ''); } const v = getActiveVariant(product, product.shades[idx].variant_index ?? idx); $('.modal-price').text(Number(v.price || 0).toFixed(2)); $('.modal-buy-btn').attr('href', getAffiliateGoUrl(product, product.shades[idx].variant_index ?? idx, 'modal')); } else { $('#modalTitle').text(getDisplayName(product)); if ($('#modalShadeName').length) $('#modalShadeName').text(''); $('.modal-price').text(Number(activeProduct.price || 0).toFixed(2)); $('.modal-buy-btn').attr('href', getAffiliateGoUrl(activeProduct, 0, 'modal')); } $('#modalShades .modal-shade-swatch').removeClass('active').eq(idx).addClass('active'); } loadIndex(variantIndex < totalImages ? variantIndex : 0); const $shades = $('#modalShades').empty(); if (product.shades?.length) { product.shades.forEach((s, i) => { const variant = getActiveVariant(product, s.variant_index ?? i); const swatchUrl = getProductSwatchUrl(variant, s); const style = s.color ? `background:${s.color}` : `background-image:url("${swatchUrl}")`; const activeClass = i === (variantIndex < totalImages ? variantIndex : 0) ? 'active' : ''; $shades.append( `` ); }); } $('#modalShades .modal-shade-swatch').off('click').on('click', function(e) { e.stopPropagation(); const idx = Number($(this).data('index')); loadIndex(idx); $('#modalShades .modal-shade-swatch').removeClass('active'); $(this).addClass('active'); }); $prevBtn.off('click').on('click', e => { e.stopPropagation(); if (currentModalIndex > 0) loadIndex(currentModalIndex - 1); }); $nextBtn.off('click').on('click', e => { e.stopPropagation(); if (currentModalIndex < totalImages - 1) loadIndex(currentModalIndex + 1); }); $(document).off('keydown.modalNav').on('keydown.modalNav', e => { if (!$('#imageModal').hasClass('active')) return; if (e.key === 'ArrowLeft') $prevBtn.click(); if (e.key === 'ArrowRight') $nextBtn.click(); }); let touchStartX = 0; let touchEndX = 0; const minSwipe = 50; $('.modal-image-wrapper') .off('touchstart touchmove touchend') .on('touchstart', e => { touchStartX = e.originalEvent.touches[0].clientX; }) .on('touchmove', e => { const deltaX = Math.abs(e.originalEvent.touches[0].clientX - touchStartX); const deltaY = Math.abs(e.originalEvent.touches[0].clientY - e.originalEvent.touches[0].clientY); if (deltaX > 10 && deltaY < 30) e.preventDefault(); }) .on('touchend', e => { touchEndX = e.originalEvent.changedTouches[0].clientX; const diff = touchStartX - touchEndX; if (Math.abs(diff) >= minSwipe) { if (diff > 0 && currentModalIndex < totalImages - 1) $nextBtn.click(); else if (diff < 0 && currentModalIndex > 0) $prevBtn.click(); } }); $('#modalShades .modal-shade-swatch.clickable').off('click').on('click', function (e) { e.stopPropagation(); const url = $(this).data('url'); if (url && url !== '#') window.open(url, '_blank', 'noopener'); }); $('#imageModal').addClass('active'); $('body').css('overflow', 'hidden'); } function schemaImageUrl(p) { const cat = 'lip-primer'; if (p.images_success && p.filename && p.safe_brand) { return `https://uk-lips.co.uk/${cat}/${p.safe_brand}/i/${p.filename}-1.jpg`; } return 'https://uk-lips.co.uk/images/lip-stain.jpg'; } // =============================================== // POPULATE SCHEMA — first page of visible products // =============================================== function schemaOfferPrice(value) { const n = Number(value); if (!Number.isFinite(n) || n <= 0) return null; return n.toFixed(2); } function populateTop3InSchema() { if (document.querySelector('[data-ssr="1"]')) return; const pageSize = Math.min(PRODUCTS_PER_PAGE, allProducts.length); const schemaProducts = allProducts.filter(p => p.rank > 0).slice(0, pageSize); if (schemaProducts.length === 0) return; const itemListElement = schemaProducts.map((p, index) => { const shortName = getShortName(p.name); const cleanName = p.name.trim(); const imageUrl = schemaImageUrl(p); const affUrl = p.aff_url && p.aff_url.trim() !== '' ? p.aff_url.trim() : p.url || '#'; const description = p.description || `Long-lasting ${shortName.toLowerCase()} with high ratings and multiple shades`; const schemaPrice = schemaOfferPrice(p.price); const offer = { "@type": "Offer", "priceCurrency": "GBP", "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "url": affUrl, "shippingDetails": { "@type": "OfferShippingDetails", "shippingRate": { "@type": "MonetaryAmount", "value": "0.00", "currency": "GBP" }, "shippingDestination": { "@type": "DefinedRegion", "addressCountry": "GB" }, "deliveryTime": { "@type": "ShippingDeliveryTime", "handlingTime": { "@type": "QuantitativeValue", "minValue": 0, "maxValue": 2, "unitCode": "DAY" }, "transitTime": { "@type": "QuantitativeValue", "minValue": 2, "maxValue": 7, "unitCode": "DAY" } } }, "hasMerchantReturnPolicy": { "@type": "MerchantReturnPolicy", "applicableCountry": "GB", "returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow", "merchantReturnDays": 30, "returnMethod": "https://schema.org/ReturnByMail", "returnFees": "https://schema.org/FreeReturn" } }; if (schemaPrice) offer.price = schemaPrice; return { "@type": "ListItem", "position": index + 1, "item": { "@type": "Product", "name": cleanName, "image": imageUrl, "description": description, "offers": offer } }; }); // Find your existing CollectionPage schema script const $schemaScript = $('script[type="application/ld+json"]').filter(function () { return this.textContent.includes('"mainEntity"') && this.textContent.includes('"CollectionPage"'); }); if ($schemaScript.length === 0) return; try { const schema = JSON.parse($schemaScript.text()); schema.mainEntity.itemListElement = itemListElement; $schemaScript.text(JSON.stringify(schema, null, 2)); } catch (e) { console.warn("Failed to update schema:", e); } } // =============================================== // DOCUMENT READY – ALL EVENT LISTENERS // =============================================== $(function() { load_products(); if (window.UKLipsTheme) { window.UKLipsTheme.restoreTheme(); } else { $('#darkToggle').on('click', function() { const isDark = $('html').attr('data-theme') === 'dark'; const next = isDark ? 'light' : 'dark'; $('html').attr('data-theme', next); try { localStorage.setItem('theme', next); } catch (e) {} $(this).html(isDark ? '' : ''); }); try { const saved = localStorage.getItem('theme'); if (saved === 'dark' || saved === 'light') { $('html').attr('data-theme', saved); $('#darkToggle').html(saved === 'dark' ? '' : ''); } } catch (e) {} } $('#viewAllBtn').on('click', () => { $('html,body').animate({ scrollTop: $productsGrid.offset().top - 100 }, 600); }); $('#floatingFilterBtn').on('click', function() { const $panel = $('#filterPanel'); const isActive = $panel.hasClass('active'); if (isActive) { closeFilterPanel(false); return; } openFilterPanel(); }); $('#filterBackdrop').on('click', () => closeFilterPanel(false)); $('#closeFiltersBtn').on('click', () => closeFilterPanel(false)); $('#applyFiltersBtn, #applyFiltersMobile').on('click', applyFiltersAndClose); $('#searchInput, #popularFilter, #brandFilter, #ratingFilter, #minPrice, #maxPrice, #sortSelect') .on('input change', markFiltersDirty); $('#clearFilters, #clearFiltersMobile').on('click', () => { $('#searchInput, #minPrice, #maxPrice').val(''); $('#popularFilter, #ratingFilter, #brandFilter, #sortSelect').val(''); const $sortSelect = $('#sortSelect'); if ($sortSelect.length) $sortSelect.val('rank'); applyFilters(); resetFilterButtonLabel(); closeFilterPanel(false); scrollToProductsGrid(); }); $('#viewComparison').on('click', () => { if (compareList.length < 2) { showCompareToast('Add at least 2 products to compare'); return; } renderComparisonTable(); $('#comparisonModal').addClass('active'); $('body').css('overflow', 'hidden'); }); $('#clearComparison').on('click', () => { compareList = []; saveLists(); syncCompareButtons(); updateComparisonBar(); showCompareToast('Comparison cleared'); }); $(document).on('click', '.close-modal', function(e) { e.stopPropagation(); $('#imageModal, #comparisonModal, #videoModal').removeClass('active'); $('body').css('overflow', ''); $('#modalVideo')[0].pause(); $(document).off('keydown.modalNav'); $('.modal-image-wrapper').off('touchstart touchmove touchend'); }); $(document).on('keydown', e => { if (e.key === 'Escape') { if ($('#filterPanel').hasClass('active')) { closeFilterPanel(false); return; } $('#imageModal, #comparisonModal, #videoModal').removeClass('active'); $('body').css('overflow', ''); $('#modalVideo')[0].pause(); } }); $(document).on('click', '#imageModal, #comparisonModal, #videoModal', function(e) { if (e.target === this) { $(this).removeClass('active'); $('body').css('overflow', ''); $('#modalVideo')[0].pause(); } }); });