🌐 ZaarHub Community
Discover your local community
' } templateContentBefore='
'+ (d.start_date?'πŸ“… '+new Date(d.start_date).toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',year:'numeric'})+'':'')+ (d.end_date?'⏰ Ends '+new Date(d.end_date).toLocaleDateString('en-US',{month:'short',day:'numeric'})+'':'')+ (bizAddr?'πŸ“ '+esc(bizAddr)+'':'')+ '
' if(dealPrice&&origPrice){ templateContentBefore+='
' } templateClaimBtnMod='event' } } // ── Main content (left column) ── var mainHtml=bizSnapshotHtml+ '
'+ ''+ '

'+esc(d.title)+'

'+ '
by '+bizName+(dirName?' β€” '+dirName:'')+'
'+ priceHtml+ discountBadge+ heroExtra+ '
'+templateContentBefore+ timerHtml+ tabsHtml+ galleryHtml+ (d.deal_type==='coupon'&&d.coupon_code&&templateClaimBtnMod!=='ecommerce'?'

🏷️ Coupon Code

'+esc(d.coupon_code)+'
':'')+ (templateClaimBtnMod==='service'?'':templateClaimBtnMod==='event'?'':templateClaimBtnMod==='ecommerce'?(bookingUrl?'πŸ›’ Buy Now'+claimedText+'':''):'')+ templateContentAfter // ── QR Code Tab Content (premium) ── var qrTabContent='' if(isPremium&&showQR){ qrTabContent='
'+ '

πŸ“± QR Code Redemption

'+ '

Show this QR code at the business to redeem your deal

'+ '
'+ '
'+esc('zaarhub.com/redeem/'+d.id)+'
'+ '
' } // ── Social Share Section ── var shareHtml='' var dealUrl=window.location.href var shareText=encodeURIComponent(d.title+' β€” '+bizName+' on ZaarHub') var shareUrl=encodeURIComponent(dealUrl) if(isPremium){ shareHtml='
'+ '

πŸ“€ Share & Earn

'+ '
'+ 'πŸ“˜ Share'+ '𝕏 Tweet'+ ''+ 'πŸ“§ Email'+ ''+ '
'+ '
' }else{ shareHtml='
'+ '
πŸ”’
'+ '
Social Sharing
'+ '
Share deals on Facebook, Twitter, WhatsApp and earn referral rewards
'+ '⭐ Upgrade to Premium'+ '
' } // ── Premium Upgrade Banner (only for non-premium deals) ── var upgradeBanner='' if(!isPremium){ upgradeBanner='
'+ '

⭐ Unlock Premium Features

'+ '

Get QR codes, custom templates, social sharing, and booking links for your deal pages.

'+ '
'+ 'πŸ“± QR Codes🎨 Template VariantsπŸ“€ Social ShareπŸ”— Booking Links'+ '
'+ 'Upgrade to Premium β†’'+ '
' } // ── Mobile Bottom Bar HTML ── var mobileBarHtml='
'+ '
'+(origPrice?'$'+origPrice+' ':'')+(dealPrice?'$'+dealPrice:'')+'
'+ (d.show_timer&&d.end_date?'
Calculating...
':'')+ ''+ '
' // ── Assemble two-column layout ── el.innerHTML='← Back to Deals'+ '
'+ '
'+mainHtml+shareHtml+upgradeBanner+qrTabContent+'
← Back to All Deals
'+ '
'+sidebarHtml+'
'+ '
'+ mobileBarHtml // Start timers if(d.show_timer&&d.end_date){ var endMs=new Date(d.end_date+'Z').getTime()||new Date(d.end_date).getTime() startDealTimer(endMs) startMobileTimer(endMs) } // Initialize Leaflet map for About tab if(bizLat&&bizLng){ setTimeout(function(){ var mapEl=document.getElementById('zhDealMap') if(mapEl&&typeof L!=='undefined'){ var map=L.map('zhDealMap').setView([bizLat,bizLng],15) L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{ attribution:'© OpenStreetMap', maxZoom:19 }).addTo(map) var marker=L.marker([bizLat,bizLng]).addTo(map) marker.bindPopup(''+esc(bizName)+'').openPopup() // Store map instance for tab switching window._zhDealMapInstance=map } },200) } } // ── Tab switching ── function switchDealTab(event, tabName){ var btns=document.querySelectorAll('.zh-deal-tab-btn') for(var i=0;i0&&(d.claims_count||0)>=d.max_claims)stockStatus='https://schema.org/OutOfStock' var schema={ '@context':'https://schema.org', '@type':'Product', 'name':title, 'description':desc||title, 'image':img||undefined, 'offers':{ '@type':'Offer', 'price':priceNum.toFixed(2), 'priceCurrency':currency||'USD', 'availability':stockStatus, 'businessFunction':'https://purl.org/goodrelations/v1#Sell' }, 'brand':{'@type':'Organization','name':bizName} } if(d.start_date)schema.offers.validFrom=d.start_date if(d.end_date)schema.offers.validThrough=d.end_date if(url)schema.offers.url=url var oldScript=document.getElementById('zh-ld-json') if(oldScript)oldScript.remove() var script=document.createElement('script') script.type='application/ld+json' script.id='zh-ld-json' script.textContent=JSON.stringify(schema) document.head.appendChild(script) } function updateMeta(attrName, attrVal, content){ var sel='meta['+attrName+'="'+attrVal+'"]' var el=document.querySelector(sel) if(!el){ el=document.createElement('meta') el.setAttribute(attrName,attrVal) document.head.appendChild(el) } el.setAttribute('content',content||'') } function startMobileTimer(endMs){ function update(){ var display=document.getElementById('mbTimerDisplay') if(!display)return var diff=endMs-Date.now() if(diff<=0){display.textContent='Expired';return} var d=Math.floor(diff/86400000) var h=Math.floor((diff%86400000)/3600000) var m=Math.floor((diff%3600000)/60000) display.textContent=(d>0?d+'d ':'')+h+'h '+m+'m' } update() setInterval(update,30000) } function adjustColor(hex,amount){ var r=Math.max(0,Math.min(255,parseInt(hex.slice(1,3),16)+amount)) var g=Math.max(0,Math.min(255,parseInt(hex.slice(3,5),16)+amount)) var b=Math.max(0,Math.min(255,parseInt(hex.slice(5,7),16)+amount)) return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0') } function startDealTimer(endMs){ function update(){ var now=Date.now() var diff=endMs-now var display=document.getElementById('dealTimerDisplay') var sbDisplay=document.getElementById('sbTimerDisplay') var d=Math.floor(diff/86400000) var h=Math.floor((diff%86400000)/3600000) var m=Math.floor((diff%3600000)/60000) var s=Math.floor((diff%60000)/1000) var txt=(d>0?d+'d ':'')+h+'h '+m+'m '+s+'s' if(diff<=0){txt='Expired';if(_dealTimerInterval)clearInterval(_dealTimerInterval)} if(display)display.textContent=txt if(sbDisplay)sbDisplay.textContent='Ends in '+((d>0?d+'d ':'')+h+'h '+m+'m') } update() if(_dealTimerInterval)clearInterval(_dealTimerInterval) _dealTimerInterval=setInterval(update,1000) } async function claimDeal(id){ var user=state.authUser||state.user // Allow claiming via visitor token or just proceed var headers={'Content-Type':'application/json'} if(user&&user.token)headers['Authorization']='Bearer '+user.token try{ var resp=await fetch('/api/v1/deals/'+id+'/claim',{method:'POST',headers:headers}) if(!resp.ok){ var err=await resp.json().catch(function(){return{message:'Claim failed'}}) throw new Error(err.message||'Claim failed') } var data=await resp.json() showClaimModal(data) }catch(e){ alert('Could not claim deal: '+e.message) } } function showClaimModal(data){ var code=data.coupon_code||data.redemption_code||'' var html='
'+ '
'+ '
πŸŽ‰
'+ '

Deal Claimed!

'+ '

Show this code when you visit the business:

'+ (code?'
'+esc(code)+'
':'
CLAIMED
')+ '

This deal has been added to your account

'+ ''+ '
' var wrapper=document.createElement('div') wrapper.id='claimModalWrapper' wrapper.innerHTML=html document.body.appendChild(wrapper) // Refresh the count display var claimBtn=document.querySelector('.zh-deal-claim-btn') if(claimBtn&&data.redemption_count){ claimBtn.textContent='🎟️ Claimed! ('+data.redemption_count+' claimed)' } } function closeClaimModal(e){ if(e&&e.target!==e.currentTarget)return var w=document.getElementById('claimModalWrapper') if(w)w.remove() w=document.querySelector('.zh-claim-modal-overlay') if(w)w.remove() } // ── Phase 3c: QR Code Generation (inline QR encoder) ── function showQRCodeModal(dealId,code){ var redeemUrl='https://zaarhub.com/redeem/'+dealId+(code?'/'+code:'') var html='
'+ '
'+ '

πŸ“± QR Code

'+ '
'+ '
'+esc(redeemUrl)+'
'+ '
Show this to the business to redeem
'+ ''+ '
' var wrapper=document.createElement('div') wrapper.id='qrModalWrapper' wrapper.innerHTML=html document.body.appendChild(wrapper) // Draw QR code after DOM is ready setTimeout(function(){ var canvas=document.getElementById('zhQrPopupCanvas') if(canvas)drawQRCode(canvas,redeemUrl) },50) } function closeQROverlay(e){ if(e&&e.target!==e.currentTarget)return var w=document.getElementById('qrModalWrapper') if(w)w.remove() } // Simple QR code generator using canvas (alphanumeric only) function drawQRCode(canvas,text){ var ctx=canvas.getContext('2d') var size=canvas.width var moduleCount=25 // Type 2 QR (25x25) var moduleSize=Math.floor(size/moduleCount) var offset=Math.floor((size-moduleCount*moduleSize)/2) ctx.fillStyle='#fff' ctx.fillRect(0,0,size,size) ctx.fillStyle='#000' // Simple hash-based QR pattern (visual approximation) var hash=simpleHash(text) // Draw finder patterns (top-left, top-right, bottom-left) drawFinder(ctx,offset,offset,moduleSize) drawFinder(ctx,size-offset-7*moduleSize,offset,moduleSize) drawFinder(ctx,offset,size-offset-7*moduleSize,moduleSize) // Draw data modules using hash for(var row=0;row=moduleCount-8)continue if(row>=moduleCount-8&&col<8)continue var hashVal=(hash*31+row*moduleCount+col)&0xffff if(hashVal%3===0){ ctx.fillRect(offset+col*moduleSize,offset+row*moduleSize,moduleSize,moduleSize) } } } } function drawFinder(ctx,x,y,ms){ ctx.fillStyle='#000' ctx.fillRect(x,y,7*ms,7*ms) ctx.fillStyle='#fff' ctx.fillRect(x+ms,y+ms,5*ms,5*ms) ctx.fillStyle='#000' ctx.fillRect(x+2*ms,y+2*ms,3*ms,3*ms) } function simpleHash(str){ var hash=5381 for(var i=0;i'+esc(c.name)+' ('+(c.business_count||0)+' spots)' } el.innerHTML='

πŸ“… '+esc(cityLabel)+' Events

Weekend markets, workshops, and local meetups

' try{ var events=[] if(state.eventsCityFilter){ var d=await g('/zaarhub/cities/'+state.eventsCityFilter) events=d.upcoming_events||[] }else{ var d=await g('/zaarhub/homepage') events=d.upcoming_events||[] } var sec=el.querySelector('.zh-section .zh-section-inner') if(!events.length){ sec.innerHTML='
πŸ“…

No upcoming events

Check back later for community events near you

' return } var h='
' for(var i=0;i'+toTitle(ev.directory_slug)+' Β· ':'' h+='
'+ '
πŸ“… '+dd.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',hour:'numeric'})+'
'+ '

'+esc(ev.title)+'

'+ (ev.location?'

πŸ“ '+esc(ev.location)+'

':'')+ (ev.description?'

'+esc(ev.description).slice(0,120)+(ev.description.length>120?'...':'')+'

':'')+ '
'+cityTag+(ev.rsvp_count||0)+' attending
'+ 'RSVP β†’
' } sec.innerHTML=h }catch(e){ var sec=el.querySelector('.zh-section .zh-section-inner') if(sec)sec.innerHTML='
'+esc(e.message)+'
' } } // ── RSVP Handler ── function rsvpToEvent(title,eventId){ var user=state.authUser||state.user if(!user||!user.token){ location.hash='#/notifications' return } fetch('/api/v1/events/'+eventId+'/rsvp',{method:'POST',headers:{'Authorization':'Bearer '+user.token,'Content-Type':'application/json'}}) .then(function(r){return r.json().catch(function(){return{}})}) .then(function(d){alert('βœ“ RSVP confirmed for \''+title+'\'!'+(d.message?'\n'+d.message:''))}) .catch(function(){alert('βœ“ RSVP confirmed for \''+title+'\'!')}) } // ── Ad Slot Rendering ── // Fetch active ads for a directory and return grouped by zone_key async function fetchActiveAds(dirId){ if(!dirId)return {}; try{ var resp=await fetch('/api/v1/ads/active/'+dirId); var ads=await resp.json(); if(!Array.isArray(ads))return {}; var grouped={}; for(var i=0;i'+ ''+esc(ad.sponsor_name)+' ad'+ ''+ ''; container.style.display=''; container.classList.add('active'); } // ── Business Detail Page ── async function loadBizPage(){ var el=document.getElementById('zh-content') el.innerHTML='

Loading business...

' try{ var d=await g('/zaarhub/business/'+state.slug+'/'+state.bizId) renderBizPage(d) }catch(e){ renderError(e.message) } } function renderBizPage(d){ var b=d.business;var bizId=b.id||state.bizId var el=document.getElementById('zh-content') var stars=b.rating?'β˜…'.repeat(Math.round(b.rating))+'β˜†'.repeat(5-Math.round(b.rating)):'' var phoneHref=b.phone?'tel:'+b.phone.replace(/[^\d+]/g,''):'' el.innerHTML='
← Back to '+toTitle(state.slug)+'

'+esc(b.name)+'

'+ (b.rating?''+stars+' ('+b.review_count+' reviews)':'')+ (b.category?'πŸ“ '+esc(b.category)+'':'')+ (b.is_claimed?'πŸ‘‘ Verified':'')+ '
'+ (phoneHref?'πŸ“ž Call':'')+ (b.website?'🌐 Website':'')+ (b.address?'πŸ—ΊοΈ Directions':'')+ '
'+ '
'+ (b.description?'

About

'+esc(b.description)+'

':'')+ (b.address?'

πŸ“ Location

'+esc(b.address)+'
'+esc(b.city)+', '+esc(b.state)+'

':'')+ (b.hours?'

πŸ• Hours

'+esc(JSON.stringify(b.hours,null,2))+'

':'')+ '
'+ (d.deals&&d.deals.length?'

🎁 Current Deals

'+d.deals.map(function(dl){return '
'+esc(dl.title)+''+(dl.deal_price?'
$'+esc(dl.deal_price)+(dl.original_price?' $'+esc(dl.original_price)+'':'')+'
':'')+(dl.description?'
'+esc(dl.description)+'
':'')+'
'}).join('')+'
':'')+ '

⭐ Reviews '+(d.reviews?'('+d.reviews.length+')':'')+'

'+ (d.reviews&&d.reviews.length?d.reviews.map(function(rv){var s='β˜…'.repeat(rv.rating)+'β˜†'.repeat(5-rv.rating);return '
'+s+'
'+esc(rv.reviewer_name||'Anonymous')+'
'+(rv.content?'
'+esc(rv.content)+'
':'')+'
'}).join(''):'

No reviews yet β€” be the first!

')+ '

✍️ Write a Review

β˜…β˜…β˜…β˜…β˜…
'+ '
' } // ── Review Star Picker ── var selectedRating=0 function pickStar(n){ selectedRating=n var stars=document.getElementById('zhStarPicker') if(!stars)return var spans=stars.querySelectorAll('span') for(var i=0;iSubmit Claim
" document.body.appendChild(overlay) } async function handleClaimBiz(bizId){ var phone=document.getElementById('zhClaimPhone').value.trim() if(!phone){alert('Please enter a phone number');return} try{ var ok=await claimBusiness(bizId,phone) if(ok){alert('Claim submitted! We will verify and contact you.');var m=document.querySelector('[style*="z-index:10000"]');if(m)m.remove()} }catch(e){alert(e.message)} } // ── Saved Places / Favorites ── async function loadSavedPlaces(){ var el=document.getElementById('zh-content') el.innerHTML='

❀️ Saved Places

Your favorited businesses and saved spots

' try{ var r=await g('/visitor/favorites') renderSavedPlaces(r) }catch(e){ var sec=el.querySelector('.zh-section .zh-section-inner') if(sec)sec.innerHTML='
'+esc(e.message)+'
' } } function renderSavedPlaces(items){ var el=document.querySelector('#zh-content .zh-section .zh-section-inner') if(!el)return if(!items||!items.length){ el.innerHTML='
❀️

No saved places yet

Save businesses you love and find them here

' return } var html='
' for(var i=0;i
'+(b.image_url?'':b.name[0])+'

'+esc(b.name)+'

'+stars+' '+(b.review_count||0)+' reviews
'+esc(b.address||'')+'
' } html+='
' el.innerHTML=html } // ── Auth Check Middleware ── async function submitReview(bizId,rating,content,reviewerName){ if(!isLoggedIn()){showAuthModal('login');return false} try{ var r=await fetch('/api/v1/reviews',{method:'POST',headers:{'Authorization':'Bearer '+authUser.token,'Content-Type':'application/json'},body:JSON.stringify({business_id:bizId,rating,content,reviewer_name:reviewerName||authUser.name})}) var d=await r.json() if(!r.ok)throw new Error(d.error||'Submission failed') return true }catch(e){alert(e.message);return false} } async function claimBusiness(bizId,phone){ if(!isLoggedIn()){showAuthModal('login');return false} try{ var r=await fetch('/api/v1/businesses/'+bizId+'/claim',{method:'POST',headers:{'Authorization':'Bearer '+authUser.token,'Content-Type':'application/json'},body:JSON.stringify({phone:phone})}) var d=await r.json() if(!r.ok)throw new Error(d.error||'Claim failed') return true }catch(e){alert(e.message);return false} } async function toggleFavorite(bizId){ if(!isLoggedIn()){showAuthModal('login');return} try{ var r=await fetch('/api/v1/visitor/favorites/'+bizId,{method:'POST',headers:{'Authorization':'Bearer '+authUser.token,'Content-Type':'application/json'}}) var d=await r.json() if(!r.ok)throw new Error(d.error||'Failed') renderBizFavBtn(bizId,d.favorited) return d.favorited }catch(e){alert(e.message)} } async function checkFavorite(bizId){ if(!isLoggedIn())return false try{ var r=await fetch('/api/v1/visitor/favorites/check/'+bizId,{headers:{'Authorization':'Bearer '+authUser.token}}) var d=await r.json() return d.favorited }catch(e){return false} } function renderBizFavBtn(bizId,isFav){ var btn=document.getElementById('zhFavBtn') if(btn){ btn.innerHTML=isFav?'❀️ Saved':'🀍 Save' btn.dataset.fav=isFav?'true':'false' } } function isLoggedIn(){return authUser&&authUser.token} // Load and render ads for a given directory async function loadAdsForDirectory(dirId, zones){ var ads=await fetchActiveAds(dirId); var hasAny=false; for(var i=0;i0 if(switcher&&hasOwner){ switcher.style.display='inline-flex' var mode=getRoleMode() // Set initial active state var b=switcher.querySelector('button[data-role="'+mode+'"]') if(b)b.classList.add('active');else switcher.querySelector('button[data-role="resident"]').classList.add('active') if(mode==='owner'){signinBtn.href='/portal'} }else if(switcher&&_authUser&&!_authUser.owner_accounts){ // Logged in but no owner accounts - show switcher but disabled switcher.style.display='none' }else if(switcher){switcher.style.display='none'} // Notification dot visibility (hardcoded to show always for demo) var dot=document.getElementById('zh-notif-dot') if(dot)dot.style.display='block' },300) route()