const words={ en:{title:'From the vitrines',demo:'A preview of the vitrines',all:'Explore all',pause:'Pause the ribbon',play:'Play the ribbon',fresh:'New',sample:'Demo',request:'Price on request',asking:'Seller’s asking price'}, fr:{title:'Au fil des vitrines',demo:'Un aperçu des vitrines',all:'Tout découvrir',pause:'Mettre le ruban en pause',play:'Animer le ruban',fresh:'Nouveau',sample:'Démo',request:'Prix sur demande',asking:'Prix demandé par la boutique'}, ar:{title:'من الڤيترينات',demo:'نظرة على الڤيترينات',all:'شوف الكل',pause:'وقف الحركة',play:'شغل الحركة',fresh:'جديد',sample:'تجريبي',request:'الثمن عند المحل',asking:'الثمن اللي حاط المحل'} }; const safeId=value=>typeof value==='string'&&/^[a-zA-Z0-9_-]{1,160}$/.test(value); const escapeHtml=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); export function ribbonListings(rows){ const cities=new Set(),ids=new Set();return (Array.isArray(rows)?rows:[]).filter(item=>{ if(item?.type!=='PROFESSIONAL'||item.status!=='PUBLISHED'||!safeId(item.id)||!safeId(item.owner_id)||!safeId(item.city_id)||cities.has(item.city_id)||ids.has(item.id)||!/^\/api\/(?:v1\/)?images\/[a-zA-Z0-9_-]+$/.test(item.images?.[0]||''))return false; cities.add(item.city_id);ids.add(item.id);return true; }).slice(0,12); } export function ribbonCard(item,language,city,duplicate=false){ const lang=words[language]?language:'en',copy=words[lang],badge=ribbonPrice(item,lang); const legacyDemo=item.is_demo&&/^\/api\/images\/demo_image_\d+_\d+$/.test(item.images[0]); const fresh=!item.is_demo&&item.is_new===true; const href='/jewelry/'+encodeURIComponent(item.id)+'?lang='+lang+'&from=homepage'; return ``; } export function ribbonPrice(item,language='en'){ const lang=words[language]?language:'en'; return Number.isSafeInteger(item.total_asking_cents)&&item.total_asking_cents>0?new Intl.NumberFormat(lang==='fr'?'fr-MA':lang==='ar'?'ar-MA':'en-GB',{maximumFractionDigits:2}).format(item.total_asking_cents/100)+' MAD':words[lang].request; } async function startRibbon(){ const root=document.getElementById('homeVitrine');if(!root)return; const viewport=root.querySelector('.vitrine-window'),track=root.querySelector('.vitrine-track'),seed=crypto.randomUUID().replaceAll('-',''); let items=[],cities=[],cycle=-1,nextBatch=null,busy=false,waiting=false,interval=0,observer=null; const seen=new Set(),lastByCity=new Map(),timers=new Map(),reduced=matchMedia('(prefers-reduced-motion:reduce)'); const locale=()=>{const value=document.documentElement.lang.split('-')[0];return words[value]?value:'en';}; const report=(kind,id)=>{if(typeof dhReportSend==='function')dhReportSend(kind,id,{source:'HOMEPAGE'});}; function stopObserver(){observer?.disconnect();for(const timer of timers.values())clearTimeout(timer);timers.clear();} function watch(){ stopObserver();if(typeof IntersectionObserver!=='function'||typeof dhMetricsAllowed!=='function'||!dhMetricsAllowed())return; observer=new IntersectionObserver(entries=>{for(const entry of entries){ const card=entry.target,id=card.dataset.vitrineListing;clearTimeout(timers.get(card)); if(!entry.isIntersecting||entry.intersectionRatio<.5||document.hidden||seen.has(id))continue; timers.set(card,setTimeout(()=>{if(!card.isConnected||document.hidden||seen.has(id))return;seen.add(id);report('IMPRESSION',id);},1000)); }},{threshold:[0,.5]});track.querySelectorAll('[data-vitrine-listing]').forEach(card=>observer.observe(card)); } function render(){ root.hidden=!items.length;if(!items.length){stopObserver();return;} const language=locale(),names={en:'Jewelry from cities across Morocco',fr:'Bijoux des villes du Maroc',ar:'حلي من مدن المغرب'}; root.setAttribute('aria-label',names[language]+(items.some(item=>item.is_demo)?' · '+words[language].sample:'')); root.dataset.cycle=String(cycle); const cityName=item=>{const city=cities.find(city=>city.id===item.city_id);return (language==='ar'?city?.name_ar:city?.name)||item.city_id;}; const group=duplicate=>items.map(item=>ribbonCard(item,language,cityName(item),duplicate)).join(''); const lapWidth=items.length*(matchMedia('(max-width:600px)').matches?138:160); const copies=Math.max(2,Math.ceil(viewport.clientWidth/lapWidth)+1); track.innerHTML=`
${group(false)}
`+Array.from({length:copies-1},()=>``).join(''); track.style.setProperty('--lap-distance',lapWidth+'px'); // Recreate the track's animation only when the next lap is ready. track.style.animation='none';void track.offsetWidth;track.style.animation=''; track.style.setProperty('--belt-duration',Math.max(20,items.length*7)+'s');watch(); } function apply(data){cycle=data.cycle;items=ribbonListings(data.listings);for(const item of items){lastByCity.delete(item.city_id);lastByCity.set(item.city_id,item.id);}while(lastByCity.size>40)lastByCity.delete(lastByCity.keys().next().value);nextBatch=null;waiting=false;render();} async function prefetch(){ if(busy||document.hidden)return;busy=true;const target=cycle<0?0:cycle+1;let applied=false; try{ const p=new URLSearchParams({seed,cycle:String(target)}),previous=[...lastByCity.values()];if(previous.length)p.set('previous',previous.join(',')); const response=await fetch('/api/v1/inventory/ribbon?'+p,{credentials:'same-origin',cache:'no-store'});if(!response.ok)throw Error('RIBBON_UNAVAILABLE'); const data=await response.json();if(data.cycle!==target||!Array.isArray(data.listings))throw Error('INVALID_RIBBON'); if(target===(cycle<0?0:cycle+1)){ nextBatch=data; if(cycle<0||waiting||!items.length&&data.listings.length){apply(data);applied=true;} } }catch{ /* Preserve the last successful lap; never invent inventory or dates. */ } finally{busy=false;if(applied&&items.length||target!==(cycle<0?0:cycle+1)&&!applied)prefetch();} } function advance(){ if(document.hidden)return; if(nextBatch){apply(nextBatch);prefetch();}else{waiting=true;prefetch();} } track.addEventListener('animationiteration',event=>{if(event.target===track&&event.animationName==='vitrine-drift')advance();}); root.addEventListener('click',event=>{const card=event.target.closest('[data-vitrine-listing]');if(card)report('LISTING_CLICK',card.dataset.vitrineListing);}); document.addEventListener('dheb:language',render); let measuredWidth=0;const resize=typeof ResizeObserver==='function'?new ResizeObserver(entries=>{const width=Math.round(entries[0].contentRect.width);if(width>0&&width!==measuredWidth){measuredWidth=width;render();}}):null;resize?.observe(viewport); const tick=()=>{if(document.hidden)return;if(reduced.matches&&!root.matches(':hover,:focus-within')&&items.length)advance();else prefetch();}; const startTimer=()=>{if(!interval)interval=setInterval(tick,60000);}; document.addEventListener('visibilitychange',()=>{root.classList.toggle('is-tab-hidden',document.hidden);if(document.hidden){for(const timer of timers.values())clearTimeout(timer);}else{watch();prefetch();}}); try{const response=await fetch('/api/v1/cities',{credentials:'same-origin'});if(response.ok)cities=(await response.json()).cities||[];}catch{} await prefetch();startTimer(); window.addEventListener('pagehide',()=>{clearInterval(interval);interval=0;stopObserver();resize?.disconnect();}); window.addEventListener('pageshow',event=>{if(event.persisted){startTimer();resize?.observe(viewport);watch();prefetch();}}); } if(typeof document!=='undefined')startRibbon();