PageSourceSearch

https://varelaya.com/storefront.js

js varelaya.com collected 2026-10-03 18:38:49 UTC 29,902 bytes, 164 lines download raw bytes

1import {rotateHomeProducts,rotationHour} from './home-rotation.js';
2import {renderOtherCategories} from './other-categories.js';
3import {renderBannerCarousel} from './banner-carousel.js';
4import {matchesSearch} from './search.js';
5import {footerHtml} from './footer.js';
6import {helpPageHtml,isHelpPage} from './help-pages.js';
7import {renderVideos} from './videos.js';
8import {renderProductDetail} from './product-detail.js';
9import {planFor} from './plans.js';
10import {escapeHtml,money,whatsappUrl} from './shared.js';
11import {renderRubro,HOME_CATEGORIES} from './rubros.js';
12import {RUBROS,SUBRUBROS,subcategoriesFor} from './taxonomy.js';
13import {PRODUCT_SERVICES} from './product-services.js';
14import {readFilters,priceRangeError,effectivePrice,filterProducts} from './catalog-filters.js';
15const icons = {
16 eye:'<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
17 share:'<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m9 10 6-3M9 14l6 3"/>',
18 plus:'<path d="M12 4v16M4 12h16"/>',
19 search:'<circle cx="10.8" cy="10.8" r="6.8"/><path d="m16 16 4.5 4.5"/>',
20 arrow:'<path d="M4 12h16m-6-6 6 6-6 6"/>',
21 heart:'<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8L12 21l8.8-8.6a5.5 5.5 0 0 0 0-7.8Z"/>',
22 pin:'<path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/>',
23 store:'<path d="m3 9 2-6h14l2 6M4 12v9h16v-9M3 9v1a3 3 0 0 0 6 0 3 3 0 0 0 6 0 3 3 0 0 0 6 0V9H3Zm6 12v-7h6v7"/>',
24 home:'<path d="m3 10 9-7 9 7v11h-6v-7H9v7H3Z"/>',
25 grid:'<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
26 close:'<path d="m6 6 12 12M6 18 18 6"/>',
27 chevron:'<path d="m9 5 7 7-7 7"/>',
28 clock:'<circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/>',
29 check:'<path d="m5 12 4 4L19 6"/>',
30 bag:'<path d="M5 7h14l2 14H3L5 7Z"/><path d="M8 8V6a4 4 0 0 1 8 0v2"/>',
31 shirt:'<path d="m8 3-6 4 3 5 3-2v11h8V10l3 2 3-5-6-4a4 4 0 0 1-8 0Z"/>',
32 shoe:'<path d="M4 5h5l2 8 9 3 1 5H3V9l1-4Z"/><path d="M11 13h4m-3 3h5M3 17h6"/>',
33 spark:'<path d="m12 2 2.8 7.2L22 12l-7.2 2.8L12 22l-2.8-7.2L2 12l7.2-2.8L12 2Z"/>',
34 cup:'<path d="M3 8h13v7a6.5 6.5 0 0 1-13 0V8Zm13 1h2a3 3 0 1 1 0 6h-2M6 3v2m5-2v2M2 22h17"/>',
35 laptop:'<rect x="4" y="3" width="16" height="12" rx="1"/><path d="m4 15-3 6h22l-3-6M9 18h6"/>',
36 leaf:'<path d="M21 3C8 1 2 7 4 15c6 9 18 3 17-12Z"/><path d="M3 22 16 9"/>',
37 filter:'<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="3"/><circle cx="15" cy="17" r="3"/>',
38 whatsapp:'<path stroke-width="2" d="M20.5 11.8a8.7 8.7 0 0 1-12.9 7.6L3 21l1.5-4.6a8.7 8.7 0 1 1 16-4.6Z"/><path fill="currentColor" stroke="none" d="M8.2 6.7c-.3 0-.6.1-.8.4-.4.4-1 1.1-1 2.2 0 1.1.8 2.2 1 2.4.1.2 1.7 2.7 4.2 3.8 2.1.9 2.5.7 3 .7.5-.1 1.6-.7 1.8-1.3.2-.6.2-1.1.1-1.2-.1-.2-.3-.2-.6-.4l-1.8-.8c-.2-.1-.4-.1-.6.2l-.8 1c-.1.2-.3.2-.6.1a7.1 7.1 0 0 1-2.1-1.3 7.8 7.8 0 0 1-1.4-1.8c-.2-.3 0-.4.1-.6l.5-.6c.1-.2.2-.3.1-.6L8.7 7c-.1-.3-.3-.3-.5-.3Z"/>',
39 pause:'<path d="M8 5v14M16 5v14"/>',
40 play:'<path d="m8 4 12 8-12 8Z"/>',
41 user:'<circle cx="12" cy="7" r="4"/><path d="M4 22v-3a8 8 0 0 1 16 0v3"/>',
42 info:'<circle cx="12" cy="12" r="9"/><path d="M12 11v6m0-10v1"/>',
43 truck:'<path d="M2 4h12v13H2V4Zm12 5h4l4 5v3h-8"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="18" r="3"/>'
44};
45export const icon = (name,cls='') => `<svg class="icon ${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name]||icons.grid}</svg>`;
46const e = escapeHtml;
47const safeImage=url=>typeof url==='string' && (/^https?:\/\//i.test(url)||/^\/(uploads|images)\//.test(url)||/^\/assets\/(?:categories|banners)\/[a-z0-9-]+\.(webp|svg)$/.test(url))?url:'';
48export function photoSource(url,cls='',eager=false){
49 const src=safeImage(url);
50 const responsive=/^\/(uploads|assets\/categories)\/[^?]+\.webp$/.test(src);
51 const sizes=cls==='wide-banner-image'?'100vw':eager?'(max-width: 650px) 100vw, 720px':'(max-width: 650px) 50vw, (max-width: 1000px) 33vw, 320px';
52 return {src,srcset:responsive?[160,320,640,960].map(w=>src+'?w='+w+' '+w+'w').join(', ')+', '+src+' 1600w':'',sizes};
53}
54export const photo=(url,alt,cls='',eager=false)=>{
55 const {src,srcset,sizes}=photoSource(url,cls,eager);
56 if(!src)return '<div class="image-fallback '+cls+'" role="img" aria-label="'+e(alt)+'">'+icon('store')+'</div>';
57 return '<img class="'+e(cls)+'" src="'+e(src)+'" '+(srcset?'srcset="'+e(srcset)+'" sizes="'+sizes+'" ':'')+'alt="'+e(alt)+'" loading="'+(eager?'eager':'lazy')+'"'+(eager&&cls==='wide-banner-image'?' fetchpriority="high"':'')+' decoding="async">';
58};
59const initials=s=>(s||'V').trim().split(/\s+/).slice(0,2).map(v=>v[0]).join('');
60export const avatar=s=>safeImage(s.avatar)?photo(s.avatar,s.name):`<span class="avatar-initials">${e(initials(s.name))}</span>`;
61const labelMode=s=>[s.retail?'Minorista':'',s.wholesale?'Mayorista':''].filter(Boolean).join(' · ');
62const dateLabel=s=>{const d=new Date(s);return Number.isNaN(+d)?'Sin fecha':new Intl.DateTimeFormat('es-AR',{day:'numeric',month:'short'}).format(d);};
63const clean=s=>String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase();
64const featuredBadge=s=>planFor(s).featured?'<span class="plan-badge destacado">Destacado</span>':'';
65const demoLabel=()=>'';
66export const address=s=>s.locationType==='feria'?[s.fair,s.sector&&`Sector ${s.sector}`,s.stall&&`Puesto ${s.stall}`,s.address].filter(Boolean).join(' · '):s.locationType==='online'?'Venta online':s.address;
67const link=(url,text,cls='')=>`<a href="${e(url)}" class="${cls}">${text}</a>`;
68const price=p=>money(p.price);
69export function renderContact(s,p=null,st=null,cls='button button-whatsapp',label='Consultar por WhatsApp',iconOnly=false,origin=globalThis.location?.origin||'https://varelaya.com'){
70 const disabled=s.demo||p?.demo||st?.demo||!s.whatsapp;
71 return disabled?`<button class="${cls}" disabled title="Contacto por WhatsApp no disponible" aria-label="Contacto por WhatsApp no disponible">${icon('whatsapp')}${iconOnly?'':' Sin WhatsApp disponible'}</button>`:`<a class="${cls}" aria-label="${e(label)}" title="${e(label)}" href="${e(whatsappUrl(s,p,st,origin))}" target="_blank" rel="noopener noreferrer" data-vat-contact="${e(s.id)}" ${p?`data-product="${e(p.id)}"`:''} ${st?`data-story="${e(st.id)}"`:''}>${icon('whatsapp')}${iconOnly?'':label}</a>`;
72}
73
74// The same public templates render on Node and in the browser. No session data is embedded.
75export function renderStorefront({onlyFrame=false,catalog=null,user=null,favoriteIds=new Set(),pathname='/',search='',historyState={},shopPanelOpen=false,catalogFiltersOpen=false,origin='https://varelaya.com'}={}){
76 const location={pathname,search,origin},history={state:historyState};
77 const path=pathname.replace(/\/$/,'')||'/';
78 const isPanel=()=>/^\/(panel|ingresar|registrarse|restablecer-acceso)(\/|$)/.test(path);
79 const shopFor=id=>catalog?.shops.find(s=>s.id===id);
80 const contactButton=(s,p=null,st=null,cls='button button-whatsapp',label='Consultar por WhatsApp',iconOnly=false)=>renderContact(s,p,st,cls,label,iconOnly,origin);
81 const activeRubro=new URLSearchParams(location.search).get('rubro')||(path==='/'?'inicio':'');
82 const currentShop=path.startsWith('/comercio/')?shopFor(decodeURIComponent(path.slice(10))):null;
83
84 const headerHtml=`<header class="header reference-header"><div class="container header-inner"><a href="/" class="brand" aria-label="Varela Ya, inicio"><svg class="brand-logo" viewBox="75 10 460 185" role="img" aria-label="Varela Ya"><image href="/brand/logo-varela-ya.png" width="600" height="250"/></svg></a><form class="header-search search-form" data-vat-form="search" data-search-scope="global" role="search"><label class="sr-only" for="header-query">Buscar productos o comercios</label><input id="header-query" name="q" placeholder="Buscar productos o comercios" value="${e(new URLSearchParams(location.search).get('q')||'')}" autocomplete="off" role="combobox" aria-autocomplete="list" aria-haspopup="listbox" aria-expanded="false"><button type="submit" aria-label="Buscar">${icon('search')}</button></form><nav class="header-actions" aria-label="Navegación principal"><a href="/favoritos" aria-label="Favoritos">${icon('heart')}<span>Favoritos</span>${favoriteIds.size?`<small>${favoriteIds.size}</small>`:''}</a><a href="${user?'/panel':'/ingresar'}" aria-label="${user?'Mi panel':'Soy comerciante'}">${icon('user')}<span>${user?'Mi panel':'Soy comerciante'}</span></a></nav></div></header>${!isPanel()&&!isHelpPage(path)&&!currentShop&&!path.startsWith('/producto/')&&catalog?`<div class="header-story-row container">${storyStrip()}</div><nav class="reference-tabs container" aria-label="Rubros">${[{id:"inicio",label:"Inicio"},...RUBROS].map(r=>`<a href="/?rubro=${r.id}" ${activeRubro===r.id?'aria-current="page"':''}>${e(r.label)}</a>`).join('')}</nav>`:''}`;
85 const mobileNav=[['/','home','Inicio'],['/productos','search','Buscar'],['/feria/playon','store','Playón'],['/favoritos','heart','Favoritos']].map(([href,i,t])=>`<a href="${href}" ${path===href?'aria-current="page"':''}>${icon(i)}<span>${t}</span></a>`).join('');
86 const footer=footerHtml({demo:catalog?.demo});
87function demoNotice(){return ''; }
88function productCard(p,mode=''){
89 const s=shopFor(p.shopId);if(!s)return '';
90 const productUrl='/producto/'+encodeURIComponent(p.id)+(mode==='mayorista'?'?venta=mayorista':'');
91 return `<article class="product-card ${planFor(s).enhanced?'product-card-enhanced':''} ${planFor(s).featured?'product-card-featured':''}"><div class="product-image-wrap"><a href="${productUrl}" class="product-image">${photo(p.images?.[0],p.title)}${demoLabel(p)}</a><button class="favorite-button ${favoriteIds.has(p.id)?'is-favorite':''}" type="button" data-vat-action="favorite" data-id="${e(p.id)}" aria-label="${favoriteIds.has(p.id)?'Quitar de':'Agregar a'} favoritos: ${e(p.title)}" aria-pressed="${favoriteIds.has(p.id)}">${icon('heart')}</button></div><div class="product-card-content"><div class="price-line"><strong>${money(effectivePrice(p,mode,s))}</strong></div>${(mode==='mayorista'||!s.retail&&s.wholesale)?`<span class="product-price-label">Mayorista · mín. ${p.minimumQuantity} unidades</span>`:''}<a href="${productUrl}" class="product-title">${e(p.title)}</a><a href="/comercio/${encodeURIComponent(s.id)}" class="product-shop">${planFor(s).enhanced?`<span class="product-shop-avatar">${avatar(s)}</span>`:''}${e(s.name)}</a>${featuredBadge(s)}${s.wholesale&&s.retail&&p.wholesalePrice!=null&&mode!=='mayorista'?`<span class="card-wholesale">Por mayor ${money(p.wholesalePrice)}</span>`:''}</div></article>`;
92}
93function shopCard(s) {
94 return `<article class="shop-card"><a href="/comercio/${encodeURIComponent(s.id)}" class="shop-cover">${photo(s.cover||catalog.products.find(p=>p.shopId===s.id)?.images?.[0],s.name)}${demoLabel(s)}</a><div class="shop-card-info"><a class="shop-avatar" href="/comercio/${encodeURIComponent(s.id)}">${avatar(s)}</a>${featuredBadge(s)}<span class="shop-category">${e(s.category)}</span><h3><a href="/comercio/${encodeURIComponent(s.id)}">${e(s.name)}</a></h3><p>${icon('pin')}${e(s.area)}</p><div class="shop-card-bottom"><span>${e(labelMode(s)||'Comercio local')}</span><a href="/comercio/${encodeURIComponent(s.id)}" aria-label="Ver ${e(s.name)}">${icon('arrow')}</a></div></div></article>`;
95}
96function storyStrip(onlyShop){
97 const valid=catalog.stories.filter(st=>(!onlyShop||st.shopId===onlyShop)&&new Date(st.expiresAt)>new Date());
98 const shops=[...new Set(valid.map(st=>
98st.shopId))].map(shopFor).filter(Boolean);
99 if(!shops.length)return '';
100 return `<div class="story-strip" aria-label="Historias de los comercios">${shops.map(s=>`<button class="story-chip" data-vat-action="story" data-id="${e(s.id)}" aria-label="Ver historias de ${e(s.name)}"><span class="story-ring"><span>${photo(s.avatar||valid.find(st=>st.shopId===s.id).image,s.name)}</span></span><strong>${e(s.name)}</strong></button>`).join('')}</div>`;
101}
102function searchForm(q='',hero=false){return `<form class="search-form ${hero?'hero-search':''}" data-vat-form="search" role="search"><label class="sr-only" for="${hero?'hero-query':'catalog-query'}">Buscar productos o comercios</label>${icon('search')}<input id="${hero?'hero-query':'catalog-query'}" name="q" placeholder="${hero?'¿Qué estás buscando hoy?':'Buscá productos, comercios…'}" value="${e(q)}" autocomplete="off" role="combobox" aria-autocomplete="list" aria-haspopup="listbox" aria-expanded="false"><button class="button button-coral" type="submit" aria-label="Buscar en catálogo">${hero?'Buscar':icon('arrow')}</button></form>`;}
103const categoryIcons={'Indumentaria':'shirt','Calzado':'shoe','Hogar y bazar':'cup','Accesorios':'bag','Belleza':'spark','Tecnología':'laptop','Otros':'grid'};
104function categories(){return `<div class="category-list">${catalog.categories.map(c=>`<a class="category-item" href="/productos?categoria=${encodeURIComponent(c)}"><span>${icon(categoryIcons[c])}</span><strong>${e(c)}</strong></a>`).join('')}</div>`;}
105function home(){
106 const hour=Number.isSafeInteger(historyState?.homeRotationHour)?historyState.homeRotationHour:rotationHour();
107 const ps=rotateHomeProducts(catalog.products,catalog.shops,hour),ss=[...catalog.shops].sort((a,b)=>Number(planFor(b).featured)-Number(planFor(a).featured));
108  
109 const banner=catalog.banner?.active?catalog.banner:null;
110 const href=banner?.href&&/^\/(?!\/)/.test(banner.href)?banner.href:'/productos';
111 const featureCats=HOME_CATEGORIES;
112 return `${banner?renderBannerCarousel(banner,{photo,icon}):`<section class="catalog-intro container"><h1>Varela Ya</h1><p>Encontrá productos y comercios de Florencio Varela.</p></section>`}<div class="container reference-home"><section class="recommended-categories"><h2>Categorías recomendadas 🤩</h2><div class="category-photo-grid">${featureCats.map(({category,image,href,mobile})=>`<a href="${e(href)}" class="category-photo${mobile?'':' category-desktop-only'}"><div>${photo(image,category)}</div><span>${e(category)}</span></a>`).join('')}</div></section>${renderVideos(catalog)}<section class="home-products" data-home-rotation-hour="${hour}"><div class="section-heading"><h2>Productos destacados 🔥</h2><a href="/productos" class="text-link">Ver todos ${icon('arrow')}</a></div>${demoNotice()}${ps.length?`<div class="product-grid">${ps.map(productCard).join('')}</div>`:empty('Todavía no hay productos publicados','Sumá tu comercio y comenzá a mostrar tus productos.','Publicar mi comercio','/registrarse')}</section><section class="home-shops"><div class="section-heading"><h2>Comercios de Varela</h2><a href="/comercios" class="text-link">Ver todos ${icon('arrow')}</a></div><div class="shop-grid">${ss.slice(0,6).map(shopCard).join('')}</div></section><section class="simple-merchant"><h2>¿Tenés un comercio en Varela?</h2><p>Mostrá tus productos y recibí consultas directamente por WhatsApp.</p><a href="${user?'/panel':'/registrarse'}" class="button merchant-register-button">Publicar mi comercio gratis ${icon('arrow')}</a></section></div>`;
113}
114function empty(title,desc,cta='',href='/productos'){return `<div class="empty-state">${icon('search')}<h3>${e(title)}</h3><p>${e(desc)}</p>${cta?link(href,e(cta),'button button-dark'):''}</div>`;}
115function getFilters(){return readFilters(location.search);}
116function selectFilter(name,label,options,value){return `<label class="filter-select"><span>${label}</span><select name="${name}" data-vat-filter="${name}"><option value="">Todas${name==='venta'?' las modalidades':name==='zona'?' las zonas':' las categorías'}</option>${options.map(v=>{let [val,txt]=Array.isArray(v)?v:[v,v];return `<option value="${e(val)}" ${value===val?'selected':''}>${e(txt)}</option>`}).join('')}</select></label>`;}
117function productFilterControls(f){return `<form data-vat-form="shop-prices" class="shop-price-form"><p>${f.mode==='mayorista'?'Precio mayorista':f.mode==='minorista'?'Precio minorista':'Precio por unidad'} · ARS</p><div class="shop-price-fields"><label for="shop-price-from">
117Desde<input id="shop-price-from" name="desde" type="number" min="0" step="0.01" inputmode="decimal" value="${Number.isFinite(f.min)?f.min:''}" placeholder="0"></label><label for="shop-price-to">Hasta<input id="shop-price-to" name="hasta" type="number" min="0" step="0.01" inputmode="decimal" value="${Number.isFinite(f.max)?f.max:''}" placeholder="Sin límite"></label><button type="submit" aria-label="Aplicar filtro de precio">${icon('chevron')}</button></div><p class="price-filter-error" role="alert" ${priceRangeError(f)?'':'hidden'}>${e(priceRangeError(f))}</p></form><fieldset class="service-filters"><legend>Opciones de compra</legend>${PRODUCT_SERVICES.map(({key,param,label})=>`<label><input type="checkbox" data-vat-service="${param}" ${f[key]?'checked':''}><span>${label}</span></label>`).join('')}</fieldset>`;}
118function filters(f,playon=false,shops=false){return `<div class="catalog-tools">${shops?searchForm(f.q):''}<div class="filter-row">${!shops?`<label class="filter-select"><span>Rubro</span><select data-vat-filter="rubro"><option value="">Todos los rubros</option>${RUBROS.map(r=>`<option value="${r.id}" ${f.rubro===r.id?'selected':''}>${e(r.label)}</option>`).join('')}</select></label>${f.rubro&&SUBRUBROS[f.rubro]?`<label class="filter-select"><span>Subrubro</span><select data-vat-filter="subrubro"><option value="">Todos los subrubros</option>${SUBRUBROS[f.rubro].map(r=>`<option value="${r.id}" ${f.subrubro===r.id?'selected':''}>${e(r.label)}</option>`).join('')}</select></label>`:''}`:''}${!shops&&subcategoriesFor(f.rubro,f.subrubro).length?selectFilter('subcategoria','Categoría de Otros',subcategoriesFor(f.rubro,f.subrubro).map(s=>[s.id,s.label]),f.subcategoria):''}${selectFilter('categoria','Categoría',catalog.categories,f.category)}${playon?'':selectFilter('zona','Zona',catalog.areas,f.area)}${selectFilter('venta','Tipo de venta',[['minorista','Minorista'],['mayorista','Mayorista']],f.mode)}${Object.entries(f).some(([k,v])=>v!==null&&v!==false&&v!==''&&!['order','q'].includes(k))?'<button class="clear-filters" data-vat-action="clear-filters">Limpiar filtros</button>':''}</div>${!shops?`<div class="catalog-purchase-filters">${productFilterControls(f)}</div>`:''}</div>`;}
119function matchesShop(s,f,playon){return s&&(!f.area||s.area===f.area)&&(!f.mode||(f.mode==='mayorista'?s.wholesale:s.retail))&&(!playon||s.area==='Playón de Varela'||clean(s.fair).includes('playon'));}
120function filteredProducts(f,playon=false){return filterProducts(catalog.products,catalog.shops,f,{playon});}
121function catalogPage(playon=false,shops=false) {
122 const f=getFilters();
123 const products=filteredProducts(f,playon);
124 const stores=catalog.shops.filter(s=>matchesShop(s,f,playon)&&(!f.category||s.category===f.category)&&(!f.q||matchesSearch([s.name,s.description,s.category,address(s)].join(' '),f.q)));
125 const title=f.q?'Resultados para “'+f.q+'”':playon?'El Playón, a un toque.':shops?'Comercios de Varela':subcategoriesFor(f.rubro,f.subrubro).find(s=>s.id===f.subcategoria)?.label||SUBRUBROS[f.rubro]?.find(r=>r.id===f.subrubro)?.label||RUBROS.find(r=>r.id===f.rubro)?.label||'Todos los productos';
126 return `<section class="page-heading ${f.q?'query-heading-hidden':''} ${!shops?'search-page-heading':''} ${playon?'page-heading-playon':''}"><div class="container"><div class="breadcrumbs"><a href="/">Inicio</a>${icon('chevron')}<span>${playon?'El Playón':shops?'Comercios':'Productos'}</span></div><span class="eyebrow">${playon?'RECORRÉ LA FERIA DESDE DONDE ESTÉS':shops?'CONOCÉ A QUIENES HACEN VARELA':'EL CATÁLOGO DE TU CIUDAD'}</span><h1>${e(title)}</h1><p>${playon?'Buscá productos, conocé sus puestos y contactá a los vendedores antes de ir.':shops?'Detrás de cada producto, hay un comercio de tu barrio.':'Productos de comercios de Varela. Elegí, consultá y comprá directo.'}</p>${playon?'<span class="fair-info">Consultá días, horarios y ubicación con cada puesto.</span>':''}</div></section><div class="container catalog-content catalog-search-layout ${shops?'catalog-shop-results':''}">${shops?filters(f,playon,true):catalogToolbar(f,playon,products.length)}<div class="results-heading"><p><strong>${shops?stores.length:products.length}</strong> ${shops?(stores.length===1?'comercio':'comercios'):(products.length===1?'producto':'productos')}${f.q?` para “${e(f.q)}”`:''}</p><span>${playon?'Puestos y productos del Playón':'Florencio Varela'}</span></div>${shops?(stores.length?`<div class="shop-grid">${stores.map(shopCard).join('')}</div>`:empty('No encontramos comercios','Probá otra categoría, zona o palabra.','Ver todos','/comercios')):(products.length?`<div class="product-grid">${products.map(p=>productCard(p,f.mode)).join('')}</div>`:empty('Todavía no encontramos eso','Probá con otra palabra o limpiá los filtros para descubrir más productos.','Ver productos',playon?'/feria/playon':'/productos'))}${playon&&stores.length?`<section class="section"><div class="section-heading"><h2>Los puestos del Playón</h2></div><div class="shop-grid">${stores.map(shopCard).join('')}</div></section>`:''}</div>`;
127}
128function productPage(id) {
129 const p=catalog.products.find(p=>p.id===id),s=p&&shopFor(p.shopId);
130 if(!p||!s){return notFound('Este producto no está disponible','Puede haberse pausado o eliminado del catálogo.');}
131 return renderProductDetail({p,s,catalog,photo,avatar,icon,contactButton,productCard,favoriteIds,address,featuredBadge,
131search:location.search,historyState:history.state});
132}
133function shopPage(id){
134 const s=shopFor(id);if(!s){return notFound('Este comercio no está disponible','Puede estar preparando su catálogo o pendiente de revisión.');}
135 const all=catalog.products.filter(p=>p.shopId===id),f=getFilters(),ps=filterProducts(catalog.products,catalog.shops,f,{shopId:id});
136 const mode=f.mode==='mayorista'?'may':f.mode==='minorista'?'min':'';
137 const groups=RUBROS.filter(r=>all.some(p=>p.rubro===r.id));
138 const rangeError=priceRangeError(f);
139 return `<div class="container shop-detail-page shop-filter-page ${shopPanelOpen?'filters-expanded':''}"><div class="breadcrumbs"><a href="/comercios">Volver</a>${icon('chevron')}<span>Catálogo de ${e(s.name)}</span></div><section class="shop-summary ${planFor(s).enhanced?'shop-summary-enhanced':''}"><div class="shop-summary-avatar">${avatar(s)}</div><div class="shop-summary-info"><div class="shop-name-row"><h1>${e(s.name)}</h1></div><div class="shop-summary-location"><p>${e(address(s)||s.area)}</p></div><div class="shop-summary-meta">${featuredBadge(s)}<small>${e(s.area)} · ${e(labelMode(s))}</small></div></div>${contactButton(s,null,null,'button button-whatsapp shop-mobile-whatsapp','Consultar por WhatsApp a '+s.name,true)}<div class="shop-profile-contact">${contactButton(s)}</div></section><button class="mobile-shop-filters" data-vat-action="toggle-shop-filters" aria-expanded="${shopPanelOpen}" aria-controls="shop-filter-sidebar">${icon('filter')} ${shopPanelOpen?'Cerrar filtros':'Rubros y filtros'}</button><div class="shop-body"><aside id="shop-filter-sidebar" class="shop-filter-sidebar"><section class="shop-filter-box"><h2>Tipo de compra</h2><div class="purchase-mode" role="group" aria-label="Tipo de compra">${[['may','Mayorista'],['min','Minorista'],['','Todo']].map(([value,label])=>`<button type="button" data-vat-action="shop-mode" data-mode="${value}" aria-pressed="${mode===value}">${label}</button>`).join('')}</div></section><section class="shop-filter-box"><h2>Categorías</h2><label class="shop-taxonomy-option"><span>Todas las publicaciones</span><input type="radio" name="shop-taxonomy" data-vat-taxonomy="" data-subrubro="" ${!f.rubro?'checked':''}></label>${groups.map(r=>`<details ${f.rubro===r.id?'open':''}><summary>Publicaciones de ${e(r.label)}</summary><label class="shop-taxonomy-option"><span>Todo de ${e(r.label)}</span><input type="radio" name="shop-taxonomy" data-vat-taxonomy="${r.id}" data-subrubro="" ${f.rubro===r.id&&!f.subrubro?'checked':''}></label>${(SUBRUBROS[r.id]||[]).filter(sub=>all.some(p=>p.rubro===r.id&&p.subrubro===sub.id)).map(sub=>`<label class="shop-taxonomy-option"><span>${e(sub.label)}</span><input type="radio" name="shop-taxonomy" data-vat-taxonomy="${r.id}" data-subrubro="${sub.id}" ${f.rubro===r.id&&f.subrubro===sub.id?'checked':''}></label>`).join('')}</details>`).join('')}${subcategoriesFor(f.rubro,f.subrubro).length?selectFilter('subcategoria','Categoría de Otros',subcategoriesFor(f.rubro,f.subrubro).map(s=>[s.id,s.label]),f.subcategoria):''}</section><section class="shop-filter-box"><div class="shop-filter-title"><h2>Filtrar por:</h2><button class="clear-filters" data-vat-action="clear-filters">Limpiar filtros</button></div>${productFilterControls(f)}</section><details class="shop-filter-box shop-information"><summary>Información del comercio</summary><p>${e(s.description||'Consultá al comercio.')}</p><h3>Dónde encontrarnos</h3><p>${e(address(s)||'Consultá al comercio')}</p>${s.locationType!=='online'&&s.address&&s.address!=='Dirección a confirmar'&&!s.demo?`<a class="text-link" href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent([s.address,'Florencio Varela','Buenos Aires'].join(', '))}" target="_blank" rel="noopener noreferrer">Cómo llegar</a>`:''}<h3>Horarios</h3><p>${e(s.hours||'Consultá horarios al comercio')}</p><h3>Medios de pago</h3><p>${e(s.payments?.join(' · ')||'Consultá al comercio')}</p>${s.delivery?'<h3>Envíos disponibles</h3><p>Coordiná cobertura y costo con el comercio.</p>':''}</details></aside><section class="shop-catalog"><div class="shop-catalog-toolbar"><p><strong>${ps.length}</strong> ${ps.length===1?'producto':'productos'}${f.q?` para “${e(f.q)}”`:''}</p><label class="shop-sort">
139Ordenar por <select aria-label="Ordenar productos del comercio" data-vat-filter="orden">${[['novedades','Más recientes'],['precio-asc','Menor precio'],['precio-desc','Mayor precio']].map(([value,label])=>`<option value="${value}" ${f.order===value?'selected':''}>${label}</option>`).join('')}</select></label></div>${ps.length?`<div class="product-grid">${ps.map(p=>productCard(p,f.mode)).join('')}</div>`:empty(rangeError?'Revisá el rango de precios':'No hay productos con estos filtros','Podés cambiar el tipo de compra o limpiar los filtros.','Ver todo el catálogo','/comercio/'+encodeURIComponent(id))}${storyStrip(id)?`<div class="shop-stories"><h2>Novedades del comercio</h2>${storyStrip(id)}</div>`:''}</section></div></div>`;
140}
141
142function favoritesPage(){const ps=catalog.products.filter(p=>favoriteIds.has(p.id));return `<section class="page-heading"><div class="container"><div class="breadcrumbs"><a href="/">Inicio</a>${icon('chevron')}<span>Favoritos</span></div><span class="eyebrow">PARA VOLVER DESPUÉS</span><h1>Tus próximos favoritos.</h1><p>Lo que te gustó, guardado en este navegador. Sin crear una cuenta.</p></div></section><div class="container catalog-content"><div class="results-heading"><p><strong>${ps.length}</strong> productos guardados</p></div>${ps.length?`<div class="product-grid">${ps.map(productCard).join('')}</div>`:empty('Tu lista está esperando algo lindo','Tocá el corazón de un producto y lo vas a encontrar acá.','Explorar productos')}</div>`;}
143function notFound(title='Por acá todavía no hay nada.',description='La página que buscás no está disponible.'){return `<div class="container">${empty(title,description,'Volver al inicio','/')}</div>`;}
144function catalogToolbar(f,playon,count){
145 const active=Object.entries(f).filter(([key,value])=>!['q','order'].includes(key)&&value!==null&&value!==false&&value!=='').length;
146 return '<div class="catalog-toolbar"><button class="catalog-filter-toggle" data-vat-action="toggle-catalog-filters" aria-expanded="'+catalogFiltersOpen+'" aria-controls="catalog-filter-panel">'+icon('filter')+' Filtrar'+(active?' ('+active+')':'')+'</button><label class="catalog-sort">Ordenar <select aria-label="Ordenar resultados" data-vat-filter="orden">'+[['novedades','Más recientes'],['precio-asc','Menor precio'],['precio-desc','Mayor precio']].map(([value,label])=>'<option value="'+value+'" '+(f.order===value?'selected':'')+'>'+label+'</option>').join('')+'</select></label></div><div id="catalog-filter-panel" class="catalog-filter-panel '+(catalogFiltersOpen?'is-open':'')+'">'+filters(f,playon)+'<button class="button button-dark catalog-filter-done" data-vat-action="apply-catalog-filters">Ver '+count+' '+(count===1?'producto':'productos')+'</button></div>';
147}
148
149 let main='';
150 if(!onlyFrame&&isHelpPage(path))main=helpPageHtml(path);
151 else if(!onlyFrame&&catalog&&!isPanel()){
152  const rubro=new URLSearchParams(search).get('rubro');
153  if(path==='/')main=RUBROS.some(r=>r.id===rubro)?renderRubro({rubro,catalog,e,photo,icon,productCard})+(rubro==='mas'&&new URLSearchParams(search).get('subrubro')==='otros'?renderOtherCategories():''):home();
154  else if(path==='/videos')main='<div class="container videos-page"><a class="text-link" href="/">← Volver al inicio</a>'+renderVideos(catalog,true)+'</div>';
155  else if(path==='/productos')main=catalogPage();
156  else if(path==='/comercios')main=catalogPage(false,true);
157  else if(path==='/feria/playon')main=catalogPage(true);
158  else if(path==='/favoritos')main=favoritesPage();
159  else if(/^\/producto\/[^/]+$/.test(path))main=productPage(decodeURIComponent(path.slice(10)));
160  else if(/^\/comercio\/[^/]+$/.test(path))main=shopPage(decodeURIComponent(path.slice(10)));
161  else main=notFound();
162 }
163 return {header:headerHtml,mobileNav,footer,main,page:path.startsWith('/producto/')?'product':path.startsWith('/comercio/')?'shop':isPanel()?'panel':'catalog',catalogPage:['/productos','/feria/playon'].includes(path)};
164}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.