// La Pharmacia — Mobile e-commerce prototype (Hebrew, RTL) const { useState, useEffect, useRef } = React; // ── Feature flags ─────────────────────────────────────────────────── // Flip any of these to `true` to re-enable a feature; the rest of the app // already has the wiring intact, so nothing else needs to change. // - customerAccounts: register/login for shoppers, account tab, "create // account during checkout" toggle. Admin login on /admin is separate // and unaffected by this flag. // - coupons: promo-code input in cart + admin section // - rewards: loyalty-points display on the account screen const FEATURES = { customerAccounts: false, coupons: false, rewards: false, }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "emerald", "displayFont": "Heebo", "showGlow": true, "denseGrid": false }/*EDITMODE-END*/; const THEMES = { emerald: { bg: 'oklch(0.20 0.025 165)', bgSoft: 'oklch(0.24 0.030 165)', bgCard: 'oklch(0.28 0.035 165)', line: 'oklch(0.34 0.030 165)', ink: 'oklch(0.96 0.012 85)', inkDim: 'oklch(0.78 0.020 90)', inkMute: 'oklch(0.58 0.025 100)', accent: 'oklch(0.82 0.085 82)', accentDim: 'oklch(0.62 0.075 80)', glow: 'rgba(220, 240, 220, 0.35)', }, ink: { bg: 'oklch(0.16 0.005 250)', bgSoft: 'oklch(0.20 0.005 250)', bgCard: 'oklch(0.24 0.008 250)', line: 'oklch(0.30 0.008 250)', ink: 'oklch(0.96 0.005 85)', inkDim: 'oklch(0.78 0.010 85)', inkMute: 'oklch(0.58 0.012 90)', accent: 'oklch(0.85 0.10 80)', accentDim: 'oklch(0.65 0.09 80)', glow: 'rgba(255, 235, 200, 0.3)', }, bone: { bg: 'oklch(0.95 0.008 85)', bgSoft: 'oklch(0.92 0.010 85)', bgCard: 'oklch(0.98 0.005 85)', line: 'oklch(0.86 0.012 85)', ink: 'oklch(0.20 0.020 165)', inkDim: 'oklch(0.40 0.018 165)', inkMute: 'oklch(0.55 0.015 165)', accent: 'oklch(0.45 0.060 165)', accentDim: 'oklch(0.55 0.050 165)', glow: 'rgba(50, 80, 60, 0.15)', }, }; // Unsplash demo imagery const IMG = { bags: 'https://images.unsplash.com/photo-1548036328-c9fa89d128fa?w=800&q=80&auto=format&fit=crop', perfumes: 'https://images.unsplash.com/photo-1541643600914-78b084683601?w=800&q=80&auto=format&fit=crop', access: 'https://images.unsplash.com/photo-1535632787350-4e68ef0ac584?w=800&q=80&auto=format&fit=crop', beauty: 'https://images.unsplash.com/photo-1596462502278-27bfdc403348?w=800&q=80&auto=format&fit=crop', hero: 'https://images.unsplash.com/photo-1592945403244-b3fbafd7f539?w=1200&q=85&auto=format&fit=crop', bag1: 'https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=800&q=80&auto=format&fit=crop', bag2: 'https://images.unsplash.com/photo-1591561954557-26941169b49e?w=800&q=80&auto=format&fit=crop', bag3: 'https://images.unsplash.com/photo-1590874103328-eac38a683ce7?w=800&q=80&auto=format&fit=crop', bag4: 'https://images.unsplash.com/photo-1566150905458-1bf1fc113f0d?w=800&q=80&auto=format&fit=crop', bottle1: 'https://images.unsplash.com/photo-1523293182086-7651a899d37f?w=800&q=80&auto=format&fit=crop', bottle2: 'https://images.unsplash.com/photo-1587017539504-67cfbddac569?w=800&q=80&auto=format&fit=crop', bottle3: 'https://images.unsplash.com/photo-1594035910387-fea47794261f?w=800&q=80&auto=format&fit=crop', bottle4: 'https://images.unsplash.com/photo-1557170334-a9086d21c1d6?w=800&q=80&auto=format&fit=crop', ring1: 'https://images.unsplash.com/photo-1535632787350-4e68ef0ac584?w=800&q=80&auto=format&fit=crop', scarf1: 'https://images.unsplash.com/photo-1601762603339-fd61e28b698a?w=800&q=80&auto=format&fit=crop', watch1: 'https://images.unsplash.com/photo-1524805444758-089113d48a6d?w=800&q=80&auto=format&fit=crop', sun1: 'https://images.unsplash.com/photo-1572635196237-14b3f281503f?w=800&q=80&auto=format&fit=crop', lipstick: 'https://images.unsplash.com/photo-1586495777744-4413f21062fa?w=800&q=80&auto=format&fit=crop', shoes: 'https://images.unsplash.com/photo-1543163521-1bf539c55dd2?w=800&q=80&auto=format&fit=crop', jewelry: 'https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=800&q=80&auto=format&fit=crop', ig1: 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=600&q=80&auto=format&fit=crop', ig2: 'https://images.unsplash.com/photo-1469334031218-e382a71b716b?w=600&q=80&auto=format&fit=crop', ig3: 'https://images.unsplash.com/photo-1483985988355-763728e1935b?w=600&q=80&auto=format&fit=crop', }; // These globals are reassigned at boot from /api/* endpoints. // Components read them at render time; a `dataVersion` state in App forces a re-render // after the fetch completes. Hardcoded values are an offline fallback. let SETTINGS = { store_name: 'La Pharmacia', store_address: 'רוטשילד 42, תל אביב', store_hours: 'ראשון–חמישי 09:00–22:00 · שישי 09:00–15:00', waze_lat: '32.0633', waze_lng: '34.7702', social_instagram: 'https://instagram.com', social_facebook: 'https://facebook.com', social_whatsapp: 'https://wa.me/972500000000', social_tiktok: '', banner_active: false, banner_text: '', banner_link: '', banner_bg: '#d4a85a', banner_fg: '#14201a', }; let IG_POSTS = []; let HOME_SLIDES = []; // hero slider entries from /api/home-slides — admin managed let SHIPPING_METHODS = [ { id: 'express', label: 'אקספרס', sub: 'מחר עד 12:00', price: 0, icon: 'truck', badge: 'חינם' }, { id: 'standard', label: 'רגיל', sub: '2–4 ימי עסקים', price: 25, icon: 'truck' }, { id: 'pickup', label: 'איסוף עצמי', sub: 'תל אביב — רוטשילד 42', price: 0, icon: 'home' }, ]; let PAYMENT_METHODS = [ { id: 'cod', label: 'במזומן בעת קבלה', description: 'תשלום במזומן או בכרטיס לשליח / בחנות בזמן האיסוף', icon: 'cash' }, { id: 'credit', label: 'כרטיס אשראי', description: 'תועברי לעמוד תשלום מאובטח של Grow', icon: 'card' }, { id: 'apple', label: 'Apple Pay', description: 'אישור עם Touch ID / Face ID דרך Grow', icon: 'apple' }, { id: 'paypal', label: 'PayPal', description: 'התחברות לחשבון ה-PayPal שלך', icon: 'paypal' }, { id: 'bit', label: 'Bit', description: 'תשלום דרך אפליקציית Bit', icon: 'bit' }, ]; let CATEGORIES = [ { id: 'bags', label: 'תיקים', sub: '142 פריטים', img: IMG.bags }, { id: 'perfumes', label: 'בשמים', sub: '86 פריטים', img: IMG.perfumes }, { id: 'access', label: 'אקססוריז', sub: '210 פריטים', img: IMG.access }, { id: 'beauty', label: 'יופי', sub: '54 פריטים', img: IMG.beauty }, { id: 'shoes', label: 'נעליים', sub: '78 פריטים', img: IMG.shoes }, { id: 'jewelry', label: 'תכשיטים', sub: '64 פריטים', img: IMG.jewelry }, ]; let PRODUCTS = [ { id: 'p1', name: 'תיק טוט עור רך', brand: 'Maison Cervo', price: 285, was: null, cat: 'bags', img: IMG.bag1, tag: 'חדש' }, { id: 'p2', name: 'Amber Veil — 50ml', brand: 'Atelier Noir', price: 142, was: 168, cat: 'perfumes', img: IMG.bottle1, tag: '15%-' }, { id: 'p3', name: 'עגילי פנינה', brand: 'Lune & Co.', price: 64, was: null, cat: 'access', img: IMG.ring1, tag: null }, { id: 'p4', name: 'צעיף משי 90', brand: 'Lume', price: 120, was: null, cat: 'access', img: IMG.scarf1, tag: 'חדש' }, { id: 'p5', name: 'תיק קרוסבודי סהר', brand: 'Maison Cervo', price: 340, was: null, cat: 'bags', img: IMG.bag2, tag: null }, { id: 'p6', name: 'Rose de Mai — 100ml', brand: 'Atelier Noir', price: 198, was: null, cat: 'perfumes', img: IMG.bottle2, tag: 'לוהט' }, { id: 'p7', name: 'תיק דלי פשתן', brand: 'Casa Verde', price: 175, was: 210, cat: 'bags', img: IMG.bag3, tag: '20%-' }, { id: 'p8', name: 'Vetiver Smoke — 50ml', brand: 'Atelier Noir', price: 156, was: null, cat: 'perfumes', img: IMG.bottle3, tag: null }, { id: 'p9', name: 'משקפי שמש קלאסיים', brand: 'Lume', price: 89, was: null, cat: 'access', img: IMG.sun1, tag: null }, { id: 'p10', name: 'שעון יד מינימלי', brand: 'Lune & Co.', price: 220, was: null, cat: 'access', img: IMG.watch1, tag: 'חדש' }, ]; let STORIES = [ { id: 's1', title: 'בשמי האביב', sub: 'מבט מקרוב', img: IMG.bottle2 }, { id: 's2', title: 'תיקי הקיץ', sub: 'המלצות', img: IMG.bag2 }, { id: 's3', title: 'אקססוריז יומי', sub: 'סטיילינג', img: IMG.scarf1 }, { id: 's4', title: 'יופי בטבעיות', sub: 'טיפים', img: IMG.beauty }, { id: 's5', title: 'מתנות אהובות', sub: 'נבחרים', img: IMG.ring1 }, { id: 's6', title: 'מאחורי הקלעים', sub: 'הסטודיו', img: IMG.bottle1 }, ]; let BRANDS = [ { id: 'b1', name: 'Maison Cervo', count: 42, img: IMG.bag1 }, { id: 'b2', name: 'Atelier Noir', count: 38, img: IMG.bottle3 }, { id: 'b3', name: 'Lune & Co.', count: 27, img: IMG.ring1 }, { id: 'b4', name: 'Lume', count: 31, img: IMG.scarf1 }, { id: 'b5', name: 'Casa Verde', count: 19, img: IMG.bag3 }, { id: 'b6', name: 'Sable', count: 24, img: IMG.bottle4 }, ]; const fmt = (n) => '₪' + n.toFixed(0); // ── Hero slider — admin-managed via /api/home-slides. Auto-cycles every 6s when 2+ slides. // Renders both the mobile card (rounded, 380px tall) and the desktop full-bleed hero. function HomeSlider({ T, displayFont, showGlow, heroParallax = 0, heroOpacity = 1, layout = 'mobile', onCta }) { const slides = HOME_SLIDES.length ? HOME_SLIDES : []; const [idx, setIdx] = useState(0); useEffect(() => { if (slides.length < 2) return; const t = setInterval(() => setIdx(i => (i + 1) % slides.length), 6000); return () => clearInterval(t); }, [slides.length]); if (slides.length === 0) return null; const cur = slides[Math.min(idx, slides.length - 1)]; // Resolve a CTA link like 'cat:perfumes' or 'shop' into a navigation action function clickCta() { if (!cur.cta_link) return; if (cur.cta_link.startsWith('cat:')) onCta?.({ type: 'category', id: cur.cta_link.slice(4) }); else if (cur.cta_link === 'shop') onCta?.({ type: 'tab', id: 'shop' }); else if (cur.cta_link.startsWith('http')) window.open(cur.cta_link, '_blank', 'noopener'); } if (layout === 'mobile') { return (
{/* Image fills the slide statically — no parallax. The previous transform caused the image to translate above the slide as the page scrolled, leaving the bottom half "empty" with just the dark background showing through. */}
{cur.eyebrow && (
{cur.eyebrow}
)}
{cur.title && (
{cur.title}
)} {cur.subtitle && (
{cur.subtitle}
)} {cur.cta_label && ( )}
{/* Dots — only when there are multiple slides */} {slides.length > 1 && (
{slides.map((_, i) => (
)}
); } // Desktop: full-bleed hero with overlay text return (
{cur.eyebrow &&
{cur.eyebrow}
} {cur.title &&

{cur.title}

} {cur.subtitle &&

{cur.subtitle}

} {cur.cta_label && ( )}
{slides.length > 1 && (
{slides.map((_, i) => (
)}
); } // ── Banner — slim promo bar from /api/settings, shown on the home screens ── function Banner({ T }) { const [dismissed, setDismissed] = useState(() => { try { return localStorage.getItem('lp_banner_dismissed') === SETTINGS.banner_text; } catch { return false; } }); if (!SETTINGS.banner_active || !SETTINGS.banner_text || dismissed) return null; const dismiss = (e) => { e?.preventDefault(); e?.stopPropagation(); try { localStorage.setItem('lp_banner_dismissed', SETTINGS.banner_text); } catch {} setDismissed(true); }; const inner = ( <> {SETTINGS.banner_text} ); const baseStyle = { position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 38, padding: '10px 16px', background: SETTINGS.banner_bg || '#d4a85a', color: SETTINGS.banner_fg || '#14201a', fontSize: 12.5, fontWeight: 600, letterSpacing: 0.3, textAlign: 'center', textDecoration: 'none', direction: 'rtl', }; return SETTINGS.banner_link ? {inner} :
{inner}
; } // ── Instagram strip — fed by /api/instagram/posts, managed in admin ── function InstagramStrip({ T, displayFont, layout = 'mobile' }) { if (!IG_POSTS || IG_POSTS.length === 0) return null; const visible = IG_POSTS.slice(0, layout === 'mobile' ? 3 : 6); if (layout === 'mobile') { return (
אינסטגרם
עקבי אחרינו
{SETTINGS.social_instagram && ( לפיד המלא ← )}
{visible.map(p => ( {p.caption {p.caption && (
{p.caption}
)}
))}
); } // Desktop return (
@LAPHARMACIA

העולם שלנו באינסטגרם

{SETTINGS.social_instagram && (

עקבי לפיד המלא →

)}
{visible.map(p => ( e.currentTarget.style.transform = 'translateY(-2px)'} onPointerLeave={e => e.currentTarget.style.transform = 'translateY(0)'}> {p.caption ))}
); } // ── Image with subtle gradient overlay ── function Img({ src, style, overlay }) { return (
{overlay &&
}
); } // ── Icons ── const Icon = ({ name, size = 22, color = 'currentColor', strokeWidth = 1.6 }) => { const p = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'search': return ; case 'bag': return ; case 'heart': return ; case 'home': return ; case 'grid': return ; case 'user': return ; case 'chevron': return ; case 'chev-r': return ; case 'left': return ; case 'right': return ; case 'plus': return ; case 'minus': return ; case 'close': return ; case 'star': return ; case 'filter': return ; case 'sparkle': return ; case 'truck': return ; case 'shield': return ; case 'leaf': return ; case 'check': return ; case 'waze': return ; case 'insta': return ; case 'fb': return ; case 'wapp': return ; case 'pin': return ; default: return null; } }; // ── Tab bar — fixed to device frame, not scroll container ── function TabBar({ active, onNav, T, cartCount }) { const tabs = [ { id: 'home', icon: 'home', label: 'בית' }, { id: 'shop', icon: 'grid', label: 'חנות' }, { id: 'wishlist', icon: 'heart', label: 'מועדפים' }, { id: 'cart', icon: 'bag', label: 'סל' }, { id: 'track', icon: 'truck', label: 'מעקב' }, FEATURES.customerAccounts && { id: 'account', icon: 'user', label: 'חשבון' }, ].filter(Boolean); return (
{tabs.map(t => { const on = active === t.id; return ( ); })}
); } function IconBtn({ T, icon, onClick }) { return ( ); } function SectionHead({ T, displayFont, title, link }) { return (
{title}
{link} ←
); } // ── Home ── function HomeScreen({ T, displayFont, showGlow, onOpenProduct, onOpenCategory, onOpenSearch, onOpenTrackOrder, onOpenLegal, scrollRef }) { const [scrollY, setScrollY] = useState(0); useEffect(() => { const el = scrollRef.current; if (!el) return; const h = () => setScrollY(el.scrollTop); el.addEventListener('scroll', h, { passive: true }); return () => el.removeEventListener('scroll', h); }, [scrollRef]); // Parallax + glow fade were the source of the "slider goes empty" bug — disabled. // Kept the scrollY listener around in case other sections want it later. const heroOpacity = 1; const heroParallax = 0; return (
40 ? `color-mix(in oklab, ${T.bg}, transparent 10%)` : 'transparent', backdropFilter: scrollY > 40 ? 'blur(16px)' : 'none', WebkitBackdropFilter: scrollY > 40 ? 'blur(16px)' : 'none', transition: 'background 0.3s ease', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, }}> La Pharmacia
{/* Hero slider — admin-managed */} { if (action.type === 'category') onOpenCategory(action.id); else if (action.type === 'tab') onOpenCategory('__all'); // tab='shop' resolves to ShopScreen via __all }} /> {/* Categories — featured + show all */}
קטגוריות
קניה לפי תחום
{/* Featured cat — big card */} {/* Other cats — grid 3 */}
{CATEGORIES.slice(1).map((c, i) => ( ))} {/* Show all tile */}
{/* (stories moved to top) */}
{PRODUCTS.slice(0, 6).map(p => ( onOpenProduct(p)} displayFont={displayFont} /> ))}
{/* Brands */}
{BRANDS.map(b => )}
{/* Instagram feed — admin-curated */}
{[IMG.ig1, IMG.ig2, IMG.ig3].map((src, i) => ( e.preventDefault()} style={{ position: 'relative', aspectRatio: '1 / 1', borderRadius: 14, overflow: 'hidden', border: `1px solid ${T.line}`, display: 'block', textDecoration: 'none', }}>
{PRODUCTS.slice(2, 8).map(p => ( onOpenProduct(p)} displayFont={displayFont} /> ))}
{[ { icon: 'truck', l: 'משלוח חינם מ-300₪' }, { icon: 'shield', l: 'אותנטי 100%' }, { icon: 'leaf', l: 'החזרה תוך 30 יום' }, ].map(x => (
{x.l}
))}
{/* Instagram strip (admin-managed) */} {/* Brand footer + socials */}
La Pharmacia
{SETTINGS.store_address}
{[ { icon: 'insta', label: 'Instagram', href: SETTINGS.social_instagram }, { icon: 'fb', label: 'Facebook', href: SETTINGS.social_facebook }, { icon: 'wapp', label: 'WhatsApp', href: SETTINGS.social_whatsapp }, { icon: 'pin', label: 'Waze', href: `https://waze.com/ul?ll=${SETTINGS.waze_lat},${SETTINGS.waze_lng}&navigate=yes` }, ].filter(s => s.href && s.href !== '').map(s => ( { e.currentTarget.style.color = T.accent; e.currentTarget.style.borderColor = T.accent; }} onPointerLeave={e => { e.currentTarget.style.color = T.inkDim; e.currentTarget.style.borderColor = T.line; }}> ))}
© 2026 La Pharmacia · כל הזכויות שמורות
VISA · MASTERCARD · BIT · APPLE PAY
{onOpenTrackOrder && ( )} {onOpenLegal && ( )}
); } function CategoryCard({ T, c, i, onOpen, displayFont, showGlow }) { const [hover, setHover] = useState(false); return ( ); } // ── Story circle (Instagram-style round avatar) ── function StoryCircle({ T, s, displayFont }) { const [pressed, setPressed] = useState(false); return ( ); } // ── Story big card (kept for future use) ── function StoryCard({ T, s, displayFont, showGlow }) { const [pressed, setPressed] = useState(false); return ( ); } // ── Brand chip ── function BrandCard({ T, b, displayFont }) { const [pressed, setPressed] = useState(false); return ( ); } function ProductCardWide({ T, p, onOpen, displayFont }) { return ( ); } function ProductCard({ T, p, onOpen, displayFont, dense }) { const [pressed, setPressed] = useState(false); const oos = (p.stock ?? 999) <= 0; return ( ); } // ── Category ── function CategoryScreen({ T, displayFont, denseGrid, catId, onBack, onOpenProduct }) { const cat = CATEGORIES.find(c => c.id === catId) || CATEGORIES[0]; const items = PRODUCTS.filter(p => p.cat === catId).concat(PRODUCTS.filter(p => p.cat !== catId)); const [filter, setFilter] = useState('all'); return (
קטגוריה
{cat.label}
{cat.sub} · נבחר עבורך
{['הכל', 'חדש', 'מתחת ל-500₪', 'במבצע', 'Maison Cervo', 'Atelier Noir'].map(f => { const on = (f === 'הכל' && filter === 'all') || filter === f; return ( ); })}
{items.length} מוצרים מיון: מומלץ
{items.map(p => onOpenProduct(p)} displayFont={displayFont} dense={denseGrid} />)}
); } // ── Product ── function ProductScreen({ T, displayFont, showGlow, product, onBack, onAdd, inWishlist, onToggleWish }) { const [size, setSize] = useState(product.cat === 'perfumes' ? '50ml' : 'M'); const [added, setAdded] = useState(false); const oos = (product.stock ?? 999) <= 0; const handleAdd = () => { if (oos) return; onAdd(product, size); setAdded(true); setTimeout(() => setAdded(false), 1400); }; return (
{showGlow && (
)}
{[0,1,2,3].map(i => (
))}
{product.brand}
4.8 · 124 ביקורות
{product.name}
{fmt(product.price)}
{product.was &&
{fmt(product.was)}
}
מדיטציה של ענבר חם, וניל קטיפתי וארז אטלס. תווי פתיחה של ברגמוט נמסים לבסיס רך — מעוצב לרגעים שנמשכים.
{product.cat === 'perfumes' ? 'נפח' : 'מידה'}
{(product.cat === 'perfumes' ? ['30ml', '50ml', '100ml'] : ['S', 'M', 'L']).map(s => { const on = size === s; return ( ); })}
{[ { l: 'תווי פתיחה', v: 'ברגמוט · פלפל ורוד' }, { l: 'תווי לב', v: 'ורד · איריס' }, { l: 'תווי בסיס', v: 'ענבר · ארז · וניל' }, { l: 'עוצמה', v: 'רכה, נמשכת 6+ שעות' }, ].map(x => (
{x.l}
{x.v}
))}
משלוח אקספרס חינם
מגיע ביום ג', 9 במאי
); } // ── Cart ── function CartScreen({ T, displayFont, items, actions, onBack, onCheckout }) { const subtotal = items.reduce((s, x) => s + x.product.price * x.qty, 0); const [promoCode, setPromoCode] = useState(''); const [promo, setPromo] = useState(null); // { code, discount, description } const [promoError, setPromoError] = useState(''); const [promoBusy, setPromoBusy] = useState(false); const discount = promo?.discount || 0; const subtotalAfterDiscount = Math.max(0, subtotal - discount); const shipping = subtotalAfterDiscount > 300 ? 0 : 25; const total = subtotalAfterDiscount + shipping; const updateQty = (item, d) => actions.setQty(item, item.qty + d); const PROMO_ERRORS = { invalid_code: 'קוד לא תקין', inactive: 'הקוד אינו פעיל', expired: 'הקוד פג תוקף', exhausted: 'הקוד נוצל מקסימום פעמים', min_not_met: 'הזמנה מתחת למינימום', }; async function applyPromo() { if (!promoCode.trim()) return; setPromoError(''); setPromoBusy(true); try { const r = await window.API.post('/promo/apply', { code: promoCode.trim(), subtotal }); setPromo(r); setPromoError(''); } catch (err) { setPromo(null); setPromoError(PROMO_ERRORS[err.message] || 'אירעה שגיאה'); } finally { setPromoBusy(false); } } function clearPromo() { setPromo(null); setPromoCode(''); setPromoError(''); } return (
הסל שלך
{items.length === 0 ? (
הסל שלך ריק
הוסיפו כמה פריטים אהובים כדי להתחיל.
) : (
{items.map((it, idx) => (
{it.product.brand}
{it.product.name}
{it.size &&
מידה: {it.size}
}
{it.qty}
{fmt(it.product.price * it.qty)}
))} {FEATURES.coupons && (
{promo ? (
{promo.code}
{promo.description || (promo.kind === 'percent' ? `${promo.amount}% הנחה` : `${fmt(promo.amount)} הנחה`)}
) : ( <>
setPromoCode(e.target.value.toUpperCase())} onKeyDown={e => e.key === 'Enter' && applyPromo()} style={{ flex: 1, padding: '13px 16px', borderRadius: 14, background: T.bgCard, border: `1px solid ${promoError ? '#c0606080' : T.line}`, color: T.ink, fontSize: 12, outline: 'none', fontFamily: 'inherit', textAlign: 'right', direction: 'rtl', }} />
{promoError &&
{promoError}
} )}
)}
{FEATURES.coupons && discount > 0 && }
)} {items.length > 0 && (
)}
); } function Row({ T, l, v }) { return (
{l} {v}
); } // ── Checkout ── function CheckoutScreen({ T, displayFont, items, user, onBack, onPlace }) { const [step, setStep] = useState(0); // 0=address, 1=delivery, 2=payment, 3=review const [addr, setAddr] = useState({ name: user?.name || '', email: '', street: '', city: '', zip: '', phone: user?.phone || '' }); const [savedAddresses, setSavedAddresses] = useState([]); const [pickedAddrId, setPickedAddrId] = useState(null); const [saveAddress, setSaveAddress] = useState(false); // logged-in: save this new address // Load saved addresses + pre-fill from default useEffect(() => { if (!user) return; window.API.get('/addresses').then(r => { setSavedAddresses(r.addresses); const def = r.addresses.find(a => a.isDefault) || r.addresses[0]; if (def) { setAddr(p => ({ ...p, street: def.street, city: def.city, zip: def.zip || '', phone: def.phone || p.phone })); setPickedAddrId(def.id); } }).catch(() => {}); }, [user]); function pickSaved(a) { setPickedAddrId(a.id); setAddr(p => ({ ...p, street: a.street, city: a.city, zip: a.zip || '', phone: a.phone || p.phone })); } function clearPick() { setPickedAddrId(null); setAddr(p => ({ ...p, street: '', city: '', zip: '' })); } const [delivery, setDelivery] = useState(() => SHIPPING_METHODS[0]?.id || 'express'); const [pay, setPay] = useState(() => (PAYMENT_METHODS.find(m => m.id === 'cod') ? 'cod' : PAYMENT_METHODS[0]?.id) || 'cod'); const [wantAccount, setWantAccount] = useState(false); const [password, setPassword] = useState(''); const [attempted, setAttempted] = useState(false); const [submitting, setSubmitting] = useState(false); const [submitError, setSubmitError] = useState(''); // Email-acknowledgement modal: shown after user clicks "אשר הזמנה" on step 4. // The order won't actually submit until the user ticks the checkbox + confirms. const [showEmailGate, setShowEmailGate] = useState(false); const [emailAck, setEmailAck] = useState(false); const isGuest = !user; const subtotal = items.reduce((s, x) => s + x.product.price * x.qty, 0); const deliveryCost = SHIPPING_METHODS.find(m => m.id === delivery)?.price ?? 0; const total = subtotal + deliveryCost; const stepLabels = ['כתובת', 'משלוח', 'תשלום', 'אישור']; // Per-step validation. Errors are keyed by field name. const errors = (() => { const e = {}; if (step === 0) { if (!addr.name?.trim()) e.name = 'נא להזין שם מלא'; if (!addr.street?.trim()) e.street = 'נא להזין כתובת'; if (!addr.city?.trim()) e.city = 'נא להזין עיר'; if (!addr.phone || addr.phone.replace(/\D/g,'').length < 8) e.phone = 'מספר טלפון לא תקין'; if (isGuest) { if (!addr.email || !/^\S+@\S+\.\S+$/.test(addr.email)) e.email = 'נא להזין אימייל תקין'; if (wantAccount && (!password || password.length < 6)) e.password = 'סיסמה לפחות 6 תווים'; } } if (step === 1 && !SHIPPING_METHODS.find(m => m.id === delivery)) e.delivery = 'יש לבחור אופן משלוח'; if (step === 2 && !PAYMENT_METHODS.find(m => m.id === pay)) e.pay = 'יש לבחור אופן תשלום'; return e; })(); const stepValid = Object.keys(errors).length === 0; const next = () => { if (!stepValid) { setAttempted(true); return; } setAttempted(false); setStep(s => Math.min(3, s + 1)); }; const prev = () => step === 0 ? onBack() : setStep(s => s - 1); async function place() { setSubmitting(true); setSubmitError(''); try { await onPlace({ addr, delivery, pay, total, subtotal, deliveryCost, wantAccount: isGuest && wantAccount, password: isGuest && wantAccount ? password : null, saveAddress: !isGuest && saveAddress && !pickedAddrId, }); } catch (err) { setSubmitError(err?.message || 'שגיאה'); } finally { setSubmitting(false); } } return (
{/* Header with progress */}
תשלום
{step + 1}/4
{/* Stepper */}
{stepLabels.map((label, i) => (
{i < step ? : i + 1}
{label}
{i < 3 && (
)} ))}
{step === 0 && ( )} {step === 1 && } {step === 2 && } {step === 3 && } {submitError &&
{submitError}
}
{/* Sticky footer */}
סך הכל {fmt(total)}
{showEmailGate && ( setShowEmailGate(false)} onConfirm={async () => { setShowEmailGate(false); await place(); }} /> )}
); } // ── Pre-submit consent modal: tells the user a confirmation email is on the way and // reminds them to check the spam folder. Order doesn't submit until the checkbox is ticked. function EmailConsentModal({ T, displayFont, email, ack, setAck, submitting, onCancel, onConfirm }) { return (
{ if (e.target === e.currentTarget) onCancel(); }}>
e.stopPropagation()}> {/* Envelope icon */}
רגע לפני סיום
מיד לאחר אישור ההזמנה נשלח אישור למייל {email && (<> {email})} .
חשוב: ייתכן שהמייל ייכנס לתיקיית הספאם / קידומי מכירות — מומלץ להציץ גם שם.
{/* Save the order number — used for tracking */}
שמרי את מספר ההזמנה שיופיע מיד אחרי האישור (בפורמט LP-XXXX-XXXX). ניתן לעקוב אחר ההזמנה בכל זמן דרך הכפתור מעקב בתפריט התחתון.
); } function CheckoutField({ T, label, value, onChange, placeholder, type = 'text', required, error }) { const isRtl = type !== 'email' && type !== 'password' && type !== 'tel'; return ( ); } function CheckoutAddress({ T, displayFont, addr, setAddr, isGuest, wantAccount, setWantAccount, password, setPassword, errors = {}, savedAddresses = [], pickedAddrId, onPickSaved, onClearPick, saveAddress, setSaveAddress, }) { const set = (k) => (v) => setAddr({ ...addr, [k]: v }); return (
פרטי משלוח
{savedAddresses.length > 0 && (
כתובות שמורות
{savedAddresses.map(a => { const on = pickedAddrId === a.id; return ( ); })}
)} {isGuest && ( )}
{isGuest && FEATURES.customerAccounts && (
{wantAccount && (
)}
)} {!isGuest && !pickedAddrId && ( )}
); } function CheckoutDelivery({ T, displayFont, value, onChange }) { // Admin-editable shipping methods, fetched at boot from /api/shipping const opts = SHIPPING_METHODS; // If the previously selected option no longer exists (admin removed it), pick the first available useEffect(() => { if (opts.length && !opts.find(o => o.id === value)) onChange(opts[0].id); }, [opts, value]); return (
בחרו אופן משלוח
{opts.map(o => { const active = value === o.id; return ( ); })}
); } function CheckoutPayment({ T, displayFont, pay, setPay }) { const methods = PAYMENT_METHODS; // If admin disabled the previously-selected method, snap to the first one useEffect(() => { if (methods.length && !methods.find(m => m.id === pay)) setPay(methods[0].id); }, [methods, pay]); const active = methods.find(m => m.id === pay); if (!methods.length) return (
אין כרגע אפשרויות תשלום זמינות. נסי שוב מאוחר יותר.
); // Auto-grid: max 3 per row, more rows as methods grow const cols = Math.min(methods.length, 3); return (
אופן תשלום
{methods.map(m => { const on = pay === m.id; return ( ); })}
{active.label}
{active.description && (
{active.description}
)} {pay === 'cod' && (
ללא עמלה נוספת
)}
); } function PayIcon({ name, color = '#fff', size = 22 }) { if (name === 'apple') return ( ); if (name === 'paypal') return ( ); if (name === 'cash') return ( ); if (name === 'bit') return ( ); // card return ( ); } function CheckoutReview({ T, displayFont, items, addr, delivery, pay, subtotal, deliveryCost, total }) { const ship = SHIPPING_METHODS.find(m => m.id === delivery); const deliveryLabel = ship ? `${ship.label}${ship.sub ? ' · ' + ship.sub : ''}` : delivery; const payLabel = { card: 'כרטיס אשראי (דרך Grow)', cod: 'במזומן בעת קבלה', apple: 'Apple Pay', paypal: 'PayPal', bit: 'Bit', }[pay] || pay; return (
בדקו את ההזמנה
{/* Items */}
{items.length} פריטים
{items.map((it, i) => (
{it.product.brand}
{it.product.name}
מידה: {it.size} · כמות: {it.qty}
{fmt(it.product.price * it.qty)}
))}
{/* Summary cards */} {/* Totals */}
סך הכל {fmt(total)}
); } function ReviewLine({ T, icon, label, value }) { return (
{icon === 'card' ? : }
{label}
{value}
); } // ── Order success ── function OrderSuccessScreen({ T, displayFont, onDone }) { return (
תודה על הקנייה!
ההזמנה שלך התקבלה ואנחנו כבר מתחילים להכין את החבילה.
מספר הזמנה
#AA-{Math.floor(Math.random() * 90000 + 10000)}
); } // ── Search ── function SearchOverlay({ T, displayFont, onClose, onOpenProduct, onOpenCategory }) { const [q, setQ] = useState(''); const inputRef = useRef(null); useEffect(() => { setTimeout(() => inputRef.current?.focus(), 100); }, []); const results = q ? PRODUCTS.filter(p => p.name.toLowerCase().includes(q.toLowerCase()) || p.brand.toLowerCase().includes(q.toLowerCase()) ) : []; return (
setQ(e.target.value)} placeholder="חיפוש תיקים, בשמים, מותגים…" style={{ flex: 1, background: 'none', border: 'none', outline: 'none', color: T.ink, padding: '14px 12px', fontSize: 13, fontFamily: 'inherit', textAlign: 'right', direction: 'rtl', }} />
{!q && (
פופולרי
{['Amber Veil', 'קרוסבודי', 'צעיף משי', 'Rose de Mai', 'פנינה', 'תיק דלי', 'Atelier Noir'].map(t => (
{t}
))}
עיון
{CATEGORIES.map(c => ( ))}
)} {q && (
{results.length === 0 ? (
לא נמצאו תוצאות עבור "{q}"
) : (
{results.map(p => onOpenProduct(p)} displayFont={displayFont} />)}
)}
)}
); } // ── Wishlist ── function WishlistScreen({ T, displayFont, items, onOpenProduct, onRemove }) { return (
שמורים
מועדפים
{items.length} פריטים
{items.length === 0 ? (
אין כאן עדיין כלום
הקישי על הלב בכל מוצר
כדי לשמור אותו לכאן.
) : (
{items.map(p => (
onOpenProduct(p)} displayFont={displayFont} />
))}
)}
); } // ── Account ── // ── Legal pages (privacy / terms / returns) ── // Copy is a placeholder template. Have a lawyer review before going live. // Replace the [...] tokens with the actual entity name + contact info from settings, // or hard-code once you've finalized the wording. function LegalPageScreen({ T, displayFont, page, onBack }) { const PAGES = { privacy: { title: 'מדיניות פרטיות', eyebrow: 'PRIVACY', body: [ ['איזה מידע אנחנו אוספים', 'בעת ביצוע הזמנה אנחנו אוספים: שם מלא, אימייל, כתובת למשלוח, מספר טלפון, ופירוט פריטי ההזמנה. בעת תשלום באשראי הנתונים עוברים ישירות לסולק (CardCom) — אנחנו לא רואים את פרטי הכרטיס.'], ['במה משתמשים במידע', 'לעיבוד הזמנות, לאספקה, ליצירת קשר במידת הצורך, ולשליחת אישורי הזמנה ועדכוני סטטוס לאימייל שמסרת.'], ['שמירת המידע', 'המידע נשמר על שרת מאובטח. הזמנות נשמרות ל-7 שנים לצרכי דיווח מס. ניתן לבקש מחיקת פרטים אישיים שלא משויכים להזמנות פעילות.'], ['העברה לצדדים שלישיים', 'אנחנו לא מוכרים, משכירים או חולקים את המידע שלך עם צד שלישי, למעט: ספק הסליקה (CardCom), חברת המשלוחים, ושירות הדואר האלקטרוני (Resend). כולם פועלים לפי הסכמי שמירת פרטיות.'], ['הזכויות שלך', 'יש לך זכות לדעת איזה מידע נשמר עליך, לתקן מידע שגוי, ולבקש מחיקה. פנייה בנושא: privacy@lapharmacia.co.il'], ], reviewed: false, }, terms: { title: 'תקנון האתר', eyebrow: 'TERMS', body: [ ['הרכישה', 'אתר La Pharmacia מציע פריטי לייפסטייל ויופי, כולל תיקים, בשמים ואקססוריז. ההזמנה נכנסת לתוקף עם אישור התשלום ושליחת אישור הזמנה למייל.'], ['מחירים', 'כל המחירים באתר כוללים מע"מ ובשקלים חדשים. אנחנו שומרים את הזכות לעדכן מחירים — המחיר התקף הוא זה שמופיע באישור ההזמנה.'], ['משלוחים', 'משלוח אקספרס: מחר עד 12:00. משלוח רגיל: 2-4 ימי עסקים. איסוף עצמי: רוטשילד 42, תל אביב.'], ['ביטולים והחזרות', 'ראי "החזרות והחזרים".'], ['אחריות', 'כל הפריטים אותנטיים ומגיעים עם אריזה מקורית. בעיה איכותית — נטפל בה מיד.'], ['פתרון מחלוקות', 'הדין החל הוא הדין הישראלי. סמכות שיפוט בלעדית לבתי המשפט בתל אביב.'], ], reviewed: false, }, returns: { title: 'החזרות והחזרים', eyebrow: 'RETURNS', body: [ ['חלון ההחזר', '14 יום מקבלת המשלוח. הפריט חייב להיות באריזתו המקורית, ללא שימוש.'], ['פריטים שלא ניתן להחזיר', 'בשמים שנפתחו או שאריזת המגן הוסרה, פריטי היגיינה, ופריטים בהזמנה אישית.'], ['איך מחזירים', 'יצרי קשר ב-returns@lapharmacia.co.il עם מספר ההזמנה. נשלח אלייך תווית החזר.'], ['החזר כספי', 'מתבצע באותו אמצעי תשלום ששימש לרכישה, תוך 7 ימי עסקים מקבלת הפריט.'], ['פגם או טעות', 'אם קיבלת פריט פגום או שגוי — אנחנו מטפלים בהחזרה על חשבוננו ושולחים פריט חלופי או החזר מלא לבחירתך.'], ], reviewed: false, }, }; const p = PAGES[page] || PAGES.privacy; return (
{p.eyebrow}
{p.title}
{!p.reviewed && (
⚠ נוסח טיוטה. יש להחליף בנוסח עם עו"ד לפני פרסום בפועל.
)}
{p.body.map(([head, text], i) => (
{head}
{text}
))}
עודכן: {new Date().toLocaleDateString('he-IL')} · La Pharmacia · רוטשילד 42, תל אביב
); } // ── Public order tracking (footer link, works without login) ── function TrackOrderScreen({ T, displayFont, onBack }) { const [orderNumber, setOrderNumber] = useState(''); const [email, setEmail] = useState(''); const [order, setOrder] = useState(null); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const STATUS_LABELS = { pending: 'ממתין', paid: 'שולם', shipped: 'נשלח', delivered: 'נמסר', cancelled: 'בוטל', refunded: 'הוחזר' }; const SHIP_LABELS = { express: 'אקספרס', standard: 'רגיל', regular: 'רגיל', pickup: 'איסוף עצמי' }; async function track(e) { e.preventDefault(); setBusy(true); setError(''); setOrder(null); try { const r = await window.API.post('/orders/track', { orderNumber: orderNumber.trim(), email: email.trim() }); setOrder(r.order); } catch (err) { setError(err.message === 'not_found' ? 'הזמנה לא נמצאה. ודאי שמספר ההזמנה והאימייל נכונים.' : 'אירעה שגיאה'); } finally { setBusy(false); } } return (
{onBack && }
שירות לקוחות
מעקב הזמנה
הזיני את מספר ההזמנה שקיבלת באישור (LP-…) והאימייל ששימש להזמנה.
{error &&
{error}
} {order && (
{order.orderNumber}
{new Date(order.createdAt).toLocaleString('he-IL')}
{order.email && (
{order.email}
)}
{STATUS_LABELS[order.status] || order.status}
{order.items.map((it, i) => (
{it.img && }
{it.brand}
{it.name}
×{it.qty}
{fmt(it.price * it.qty)}
))}
{fmt(order.total)}} />
)}
); } // ── Forgot password (request reset link) ── function ForgotPasswordScreen({ T, displayFont, onBack }) { const [email, setEmail] = useState(''); const [busy, setBusy] = useState(false); const [done, setDone] = useState(false); async function submit(e) { e.preventDefault(); setBusy(true); try { await window.API.post('/auth/forgot', { email: email.trim() }); } catch { /* always show same success message — don't leak account existence */ } finally { setBusy(false); setDone(true); } } return (
איפוס סיסמה
{done ? (
בדקי את המייל
אם הכתובת רשומה אצלנו — שלחנו אליה קישור לאיפוס סיסמה. הקישור בתוקף לשעה.
) : (
הזיני את האימייל של החשבון. נשלח אלייך קישור לאיפוס.
)}
); } // ── Reset password (lands here from email link with ?reset=TOKEN) ── function ResetPasswordScreen({ T, displayFont, token, onDone }) { const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); async function submit(e) { e.preventDefault(); setError(''); if (password.length < 6) { setError('סיסמה לפחות 6 תווים'); return; } if (password !== confirm) { setError('הסיסמאות לא תואמות'); return; } setBusy(true); try { await window.API.post('/auth/reset', { token, password }); onDone(); } catch (err) { setError(err.message === 'expired_token' ? 'הקישור פג תוקף. בקשי איפוס חדש.' : err.message === 'invalid_token' ? 'קישור לא תקין או שכבר נוצל.' : 'אירעה שגיאה'); } finally { setBusy(false); } } return (
סיסמה חדשה
בחרי סיסמה חדשה לחשבון שלך.
{error &&
{error}
}
); } // ── Auth screen — shown in the account tab when there's no logged-in user ── function AuthScreen({ T, displayFont, onAuthed, onForgot }) { const [mode, setMode] = useState('login'); // 'login' | 'register' const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const ERR_MSG = { invalid_credentials: 'אימייל או סיסמה שגויים', invalid_email: 'כתובת מייל לא תקינה', weak_password: 'הסיסמה צריכה לפחות 6 תווים', name_required: 'נא להזין שם', email_taken: 'אימייל זה כבר רשום במערכת', }; async function submit(e) { e.preventDefault(); setError(''); setLoading(true); try { const path = mode === 'login' ? '/auth/login' : '/auth/register'; const body = mode === 'login' ? { email, password } : { email, password, name }; const { token, user } = await window.API.post(path, body); window.API.setToken(token); onAuthed(user); } catch (err) { setError(ERR_MSG[err.message] || 'משהו השתבש, נסי שוב'); } finally { setLoading(false); } } const inputStyle = { width: '100%', padding: '14px 16px', borderRadius: 14, background: T.bgCard, border: `1px solid ${T.line}`, color: T.ink, fontSize: 14, fontFamily: 'inherit', direction: 'rtl', outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.2s, background 0.2s', }; return (
{mode === 'login' ? 'ברוכה השבה' : 'הצטרפות'}
{mode === 'login' ? 'התחברי לחשבון שלך' : 'פתחי חשבון חדש'}
{mode === 'login' ? 'גישה לסל, להזמנות ולתוכנית הנקודות.' : 'נשמור לך את הסל, נעדכן על הזמנות ונצרף אותך למועדון.'}
{mode === 'register' && ( setName(e.target.value)} style={inputStyle} /> )} setEmail(e.target.value)} style={inputStyle} /> setPassword(e.target.value)} style={inputStyle} /> {error && (
{error}
)} {mode === 'login' && onForgot && ( )}
בהצטרפות את מאשרת את תנאי השימוש ומדיניות הפרטיות
); } function AccountScreen({ T, displayFont, user, onOpenOrders, onOpenProfile, onOpenAddresses, onLogout }) { const items = [ { l: 'הזמנות שלי', s: 'צפיה בהיסטוריית הזמנות', action: onOpenOrders }, { l: 'פרטים אישיים', s: user?.phone || 'הוספת מספר טלפון', action: onOpenProfile }, { l: 'כתובות', s: 'ניהול כתובות שמורות', action: onOpenAddresses }, { l: 'התראות', s: 'אימייל + פוש' }, { l: 'עזרה ותמיכה', s: '' }, { l: 'התנתקות', s: '', action: onLogout, danger: true }, ]; return (
חברה מאז {new Date(user?.created_at || Date.now()).getFullYear()}
{user?.name || 'אורחת'}
{user?.email || ''}
חברת אטלייה
1,240 נק׳
260 לדרגה הבאה
{items.map((x, i) => ( ))}
); } // ── Shop ── function ShopScreen({ T, displayFont, showGlow, onOpenCategory, onOpenSearch, onBack }) { return (
{onBack ? ( ) : null}
חנות
כל הקטגוריות
{CATEGORIES.map((c, i) => ( ))}
); } // ── Splash ── function Splash({ T, displayFont, onEnter }) { return (
La Pharmacia
בריאות ויופי,
במיטבם.
תיקים, בשמים ואקססוריז שנבחרו עבור היומיומי והנדיר.
); } // ── Toast ── function Toast({ T, message, show }) { return (
{message}
); } // ── Tiny inline brand-icon SVGs used in desktop chrome ── const Insta = () => ( ); const Fb = () => ( ); const Wa = () => ( ); // ── Desktop top nav (hidden under 768px via CSS) ── function DesktopTopNav({ active, cartCount, transparent, onTab, onOpenCategory, onSearch }) { return ( ); } // ── useMediaQuery — observes a CSS media query and re-renders on change ── function useMediaQuery(query) { const [matches, setMatches] = useState(() => typeof window !== 'undefined' && window.matchMedia(query).matches ); useEffect(() => { const mq = window.matchMedia(query); const onChange = () => setMatches(mq.matches); mq.addEventListener('change', onChange); return () => mq.removeEventListener('change', onChange); }, [query]); return matches; } // ── Desktop home page — full-bleed editorial layout (renders only on >=768px home) ── function DesktopHome({ onOpenCategory, onOpenProduct, onOpenSearch }) { return (
{/* HERO — admin-managed slider */} {HOME_SLIDES.length > 0 ? ( { if (action.type === 'category') onOpenCategory(action.id); else if (action.type === 'tab') onOpenCategory('__all'); }} /> ) : ( // Empty-state fallback so a fresh DB doesn't show a blank page
ATELIER · TEL AVIV

אוצרת בידיים,
נשאית בלב

בית של אובייקטים יפים — תיקים, בשמים ואקססוריז שנבחרו אחד אחד.

)} {/* TRUST STRIP */}
משלוח חינםבהזמנה מעל ₪400
החזר תוך 30 יוםללא שאלות
בית מרקחת מורשהבפיקוח משרד הבריאות
איסוף בחנותרוטשילד 42, תל אביב
{/* CATEGORIES */}
קולקציות

הקטגוריות שלנו

שש משפחות, אינסוף שילובים. כל פריט נבחר ביד מתוך מאות.

{CATEGORIES.map(c => ( ))}
{/* EDITORIAL BANNER — full-bleed alt section */}
סיפור

היופי שבשקט

אנחנו מאמינות שיופי אמיתי לא צועק. הוא נישא לאט, מתגלה ברגעים. הקטלוג שלנו הוא אסיף של פריטים שעוברים את מבחן הזמן — תיקי עור שמתעדנים עם השימוש, ניחוחות שמשתנים על העור, אובייקטים שאפשר להעביר בירושה.

{/* FEATURED PRODUCTS */}
חידושים השבוע

המוצרים החמים

חיבורים חדשים מאחרוני המותגים שאנחנו אוהבים

{PRODUCTS.slice(0, 8).map(p => ( ))}
{/* INSTAGRAM STRIP (admin-managed) */} {/* BRANDS — alt section */}
בעולם שלנו

מותגים מובילים

מסטודיואים קטנים בפריז ועד בתי בושם בפלורנס

{BRANDS.map(b => (
{b.name}
{b.count} פריטים
))}
{/* NEWSLETTER */}
מועדון La Pharmacia

הצטרפי לסיפור

השקות חדשות, אירועי מותג, ומבצעים בלעדיים — ישירות לתיבה שלך, פעם בשבוע.

); } // ── Desktop footer ── function DesktopFooter({ onTab, onOpenCategory, onOpenTrackOrder, onOpenLegal }) { return (
La Pharmacia

בית של אובייקטים יפים — תיקים, בשמים ואקססוריז שנבחרו ביד אחת אחת.
{SETTINGS.store_address}.

{SETTINGS.social_instagram && } {SETTINGS.social_facebook && } {SETTINGS.social_whatsapp && }

קניות

© 2026 La Pharmacia · כל הזכויות שמורות
VISA · MASTERCARD · BIT · APPLE PAY
); } // ── Saved addresses (account → כתובות) ── function AddressesScreen({ T, displayFont, onBack }) { const [rows, setRows] = useState(null); const [editing, setEditing] = useState(null); // null | {} for new | { ...row } for edit const [error, setError] = useState(''); const load = () => window.API.get('/addresses').then(r => setRows(r.addresses)).catch(() => setError('load_failed')); useEffect(() => { load(); }, []); async function save(form) { setError(''); try { if (form.id) await window.API.patch(`/addresses/${form.id}`, form); else await window.API.post('/addresses', form); setEditing(null); load(); } catch (err) { setError(err.data?.error || 'save_failed'); } } async function remove(id) { await window.API.del(`/addresses/${id}`); load(); } async function setDefault(a) { await window.API.patch(`/addresses/${a.id}`, { isDefault: true }); load(); } return (
הכתובות שלי
{!rows &&
טוען...
} {rows?.length === 0 && (
עדיין אין כתובות שמורות.
הוסיפי כתובת ראשונה כדי לחסוך זמן בהזמנות הבאות.
)} {rows?.map(a => (
{a.label || 'כתובת'} {a.isDefault && · ברירת מחדל}
{a.street}
{a.city}{a.zip ? ` · ${a.zip}` : ''}
{a.phone}
{!a.isDefault && ( )}
))}
{editing !== null && ( setEditing(null)} error={error} /> )}
); } function AddressForm({ T, displayFont, initial, onSave, onCancel, error }) { const [f, setF] = useState({ id: initial.id, label: initial.label || '', street: initial.street || '', city: initial.city || '', zip: initial.zip || '', phone: initial.phone || '', isDefault: initial.isDefault || false, }); const set = (k) => (v) => setF(p => ({ ...p, [k]: v })); const valid = f.street?.trim() && f.city?.trim(); return (
{initial.id ? 'עריכת כתובת' : 'כתובת חדשה'}
{error &&
שמירה נכשלה
}
); } // ── Orders list ── function OrdersScreen({ T, displayFont, onBack, onOpenOrder }) { const [orders, setOrders] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; window.API.get('/orders') .then(r => { if (!cancelled) setOrders(r.orders); }) .catch(e => { if (!cancelled) setError(e.message || 'load_failed'); }); return () => { cancelled = true; }; }, []); return (
ההזמנות שלי
{error && (
שגיאה בטעינה
)} {!orders && !error && (
טוען...
)} {orders && orders.length === 0 && (
עדיין אין הזמנות
ההזמנה הראשונה שלך תופיע כאן.
)} {orders && orders.map(o => ( ))}
); } // ── Single order detail ── function OrderDetailScreen({ T, displayFont, orderId, onBack }) { const [order, setOrder] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; window.API.get('/orders/' + orderId) .then(r => { if (!cancelled) setOrder(r.order); }) .catch(e => { if (!cancelled) setError(e.message || 'load_failed'); }); return () => { cancelled = true; }; }, [orderId]); if (error) return
שגיאה בטעינה
; if (!order) return
טוען...
; const addr = order.address || {}; const SHIP_LABEL = { express: 'אקספרס · מחר', regular: 'משלוח רגיל', pickup: 'איסוף עצמי' }; const PAY_LABEL = { credit: 'כרטיס אשראי', apple_pay: 'Apple Pay', paypal: 'PayPal', bit: 'Bit' }; return (
הזמנה
{order.order_number}
{order.status === 'paid' ? 'התשלום אושר' : order.status}
{new Date(order.created_at).toLocaleString('he-IL')}
{/* Items */}
פריטים
{(order.items || []).map(it => (
{it.brand}
{it.name}
{it.size && <>מידה: {it.size} · }כמות: {it.qty}
{fmt(it.price * it.qty)}
))}
{/* Address */}
כתובת משלוח
{addr.name && <>{addr.name}
} {addr.street}
{addr.city}{addr.zip ? ` · ${addr.zip}` : ''}
{addr.phone}
{/* Totals */}
סך הכל {fmt(order.total)}
); } // ── Profile edit ── function ProfileEditScreen({ T, displayFont, user, onBack, onSave }) { const [name, setName] = useState(user?.name || ''); const [phone, setPhone] = useState(user?.phone || ''); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const inputStyle = { width: '100%', padding: '14px 16px', borderRadius: 14, background: T.bgCard, border: `1px solid ${T.line}`, color: T.ink, fontSize: 14, fontFamily: 'inherit', direction: 'rtl', outline: 'none', boxSizing: 'border-box', }; async function submit(e) { e.preventDefault(); setError(''); setSaving(true); try { await onSave({ name, phone: phone || null }); } catch (err) { setError('שגיאה בשמירה'); } finally { setSaving(false); } } return (
פרטים אישיים
{error && (
{error}
)}
); } // ── Newsletter form (used by home + desktop home) ── function NewsletterForm({ T, compact = false }) { const [email, setEmail] = useState(''); const [state, setState] = useState('idle'); // idle | sending | done | error async function submit(e) { e.preventDefault(); if (!email.includes('@')) { setState('error'); return; } setState('sending'); try { await window.API.post('/newsletter', { email }); setState('done'); setEmail(''); } catch { setState('error'); } } if (compact) { return (
setEmail(e.target.value)} required placeholder={state === 'done' ? 'הצטרפת ✓' : 'כתובת המייל שלך'} disabled={state === 'sending' || state === 'done'} style={{ flex: 1, padding: '13px 16px', borderRadius: 14, background: T.bgCard, border: `1px solid ${T.line}`, color: T.ink, fontSize: 12, outline: 'none', fontFamily: 'inherit', textAlign: 'right', direction: 'rtl', }} />
); } return (
setEmail(e.target.value)} required placeholder={state === 'done' ? 'תודה שהצטרפת ✓' : 'כתובת המייל שלך'} disabled={state === 'sending' || state === 'done'} />
); } // ── App shell ── function App() { const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS); const T = THEMES[tweaks.theme] || THEMES.emerald; const [tab, setTab] = useState('home'); const [searchOpen, setSearchOpen] = useState(false); const [stack, setStack] = useState([]); // Cart + wishlist are persisted to localStorage so guests don't lose them on refresh. // When customer accounts are on AND the user is logged in, the boot effect overwrites // these from the server (server is the source of truth in that case). const [cart, setCart] = useState(() => { try { return JSON.parse(localStorage.getItem('lp_cart') || '[]'); } catch { return []; } }); const [wishlist, setWishlist] = useState(() => { try { return JSON.parse(localStorage.getItem('lp_wishlist') || '[]'); } catch { return []; } }); const [toast, setToast] = useState({ msg: '', show: false }); const [user, setUser] = useState(null); const [bootError, setBootError] = useState(null); const [dataVersion, setDataVersion] = useState(0); // bumped after API replaces CATEGORIES/PRODUCTS/BRANDS const scrollRef = useRef(null); const isDesktop = useMediaQuery('(min-width: 768px)'); useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [stack, tab]); useEffect(() => { window.scrollTo({ top: 0 }); }, [tab, stack.length]); // Persist cart + wishlist to localStorage on every change so a refresh keeps them. // Skipped when the user is logged in — server is the source of truth in that case. useEffect(() => { if (user) return; try { localStorage.setItem('lp_cart', JSON.stringify(cart)); } catch {} }, [cart, user]); useEffect(() => { if (user) return; try { localStorage.setItem('lp_wishlist', JSON.stringify(wishlist)); } catch {} }, [wishlist, user]); // Track page scroll for transparent-topnav-over-hero on desktop home const [pageScrolled, setPageScrolled] = useState(false); useEffect(() => { const onScroll = () => setPageScrolled(window.scrollY > 80); window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); // ── Boot: detect ?reset=TOKEN in the URL and push the reset screen ── useEffect(() => { const params = new URLSearchParams(window.location.search); const t = params.get('reset'); if (t) { setStack([{ type: 'reset-password', token: t }]); // Strip the param so refresh doesn't re-trigger const cleanUrl = window.location.pathname + window.location.hash; window.history.replaceState({}, '', cleanUrl); } }, []); // ── Boot: fetch catalog + settings + IG from API + restore session if token exists ── useEffect(() => { let cancelled = false; (async () => { try { const [c, b, p, s, ig, sh, pm, sl] = await Promise.all([ window.API.get('/categories'), window.API.get('/brands'), window.API.get('/products'), window.API.get('/settings').catch(() => ({ settings: SETTINGS })), window.API.get('/instagram/posts').catch(() => ({ posts: [] })), window.API.get('/shipping').catch(() => ({ methods: SHIPPING_METHODS })), window.API.get('/payments').catch(() => ({ methods: PAYMENT_METHODS })), window.API.get('/home-slides').catch(() => ({ slides: [] })), ]); if (cancelled) return; if (c.categories?.length) CATEGORIES = c.categories; if (b.brands?.length) BRANDS = b.brands; if (p.products?.length) PRODUCTS = p.products; if (s.settings) SETTINGS = { ...SETTINGS, ...s.settings }; if (ig.posts) IG_POSTS = ig.posts; if (sh.methods?.length) SHIPPING_METHODS = sh.methods; if (pm.methods?.length) PAYMENT_METHODS = pm.methods; if (sl.slides) HOME_SLIDES = sl.slides; setDataVersion(v => v + 1); } catch (err) { console.warn('[boot] catalog fetch failed, using offline fallback:', err.message); if (!cancelled) setBootError('catalog'); } // Only restore a user session when customer accounts are enabled. // The admin token is stored under the same localStorage key by /admin, so without // this gate, signing in to admin would carry over to the storefront and hide // the guest checkout flow (no email field, "save address" toggle, etc.). if (FEATURES.customerAccounts && window.API.token) { try { const me = await window.API.get('/auth/me'); if (!cancelled) setUser(me.user); // Hydrate cart + wishlist from the server const [cartRes, wishRes] = await Promise.all([ window.API.get('/cart').catch(() => ({ items: [] })), window.API.get('/wishlist').catch(() => ({ items: [] })), ]); if (!cancelled) { setCart(cartRes.items.map(i => ({ product: i.product, size: i.size, qty: i.qty, serverId: i.id }))); setWishlist(wishRes.items); } } catch (err) { // Token was rejected (likely expired) — drop it silently window.API.setToken(null); } } })(); return () => { cancelled = true; }; }, []); // ── Auth helpers ── const onAuthed = async (u) => { setUser(u); try { const [cartRes, wishRes] = await Promise.all([ window.API.get('/cart').catch(() => ({ items: [] })), window.API.get('/wishlist').catch(() => ({ items: [] })), ]); setCart(cartRes.items.map(i => ({ product: i.product, size: i.size, qty: i.qty, serverId: i.id }))); setWishlist(wishRes.items); } catch { /* empty cart on fresh signup is fine */ } }; const logout = () => { window.API.setToken(null); setUser(null); setCart([]); setWishlist([]); setTab('home'); }; const flashToast = (msg) => { setToast({ msg, show: true }); setTimeout(() => setToast(t => ({ ...t, show: false })), 1800); }; const updateProfile = async (patch) => { const { user: updated } = await window.API.patch('/auth/me', patch); setUser(updated); return updated; }; // ── Cart actions: add / setQty / remove / clear ─────────────────────────── // When logged in, every mutation hits /api/cart and we mirror the server response. // When logged out, mutations stay local. Cart shape: { product, size, qty, serverId? } const cartActions = { add: async (product, size, qty = 1) => { if (user) { try { const { items } = await window.API.post('/cart', { productId: product.id, size: size || null, qty }); setCart(items.map(i => ({ product: i.product, size: i.size, qty: i.qty, serverId: i.id }))); } catch (err) { console.error('[cart.add]', err); flashToast('שגיאה בהוספה לסל'); return; } } else { setCart(c => { const ix = c.findIndex(x => x.product.id === product.id && x.size === size); if (ix >= 0) { const next = [...c]; next[ix] = { ...next[ix], qty: next[ix].qty + qty }; return next; } return [...c, { product, size, qty }]; }); } flashToast('נוסף לסל'); // After a successful add, jump to the cart so the user sees what they just added. setStack([]); setTab('cart'); }, setQty: async (item, qty) => { if (user && item.serverId) { try { const { items } = await window.API.patch(`/cart/${item.serverId}`, { qty }); setCart(items.map(i => ({ product: i.product, size: i.size, qty: i.qty, serverId: i.id }))); return; } catch (err) { console.error('[cart.setQty]', err); flashToast('שגיאה בעדכון'); return; } } setCart(c => qty <= 0 ? c.filter(x => !(x.product.id === item.product.id && x.size === item.size)) : c.map(x => (x.product.id === item.product.id && x.size === item.size) ? { ...x, qty } : x) ); }, remove: (item) => cartActions.setQty(item, 0), clearLocal: () => setCart([]), }; // ── Wishlist actions: toggle / has ──────────────────────────────────────── const wishActions = { has: (productId) => wishlist.some(w => w.id === productId), toggle: async (product) => { if (user) { try { const has = wishActions.has(product.id); const { items } = await (has ? window.API.del(`/wishlist/${product.id}`) : window.API.post('/wishlist', { productId: product.id })); setWishlist(items); flashToast(has ? 'הוסר מהמועדפים' : 'נשמר במועדפים'); } catch (err) { console.error('[wishlist]', err); flashToast('שגיאה'); } } else { setWishlist(w => { const has = w.some(x => x.id === product.id); if (has) return w.filter(x => x.id !== product.id); return [...w, { id: product.id, name: product.name, brand: product.brand, price: product.price, was: product.was, img: product.img, tag: product.tag, cat: product.cat }]; }); flashToast(wishActions.has(product.id) ? 'הוסר מהמועדפים' : 'נשמר במועדפים'); } }, }; const top = stack[stack.length - 1]; const openCategory = (id) => setStack(s => [...s, { type: 'category', id }]); const openProduct = (p) => setStack(s => [...s, { type: 'product', product: p }]); const openCheckout = () => setStack(s => [...s, { type: 'checkout' }]); const openOrders = () => setStack(s => [...s, { type: 'orders' }]); const openOrderDetail = (orderId) => setStack(s => [...s, { type: 'order-detail', orderId }]); const openProfileEdit = () => setStack(s => [...s, { type: 'profile-edit' }]); const openAddresses = () => setStack(s => [...s, { type: 'addresses' }]); const openTrackOrder = () => setStack(s => [...s, { type: 'track-order' }]); const openForgotPwd = () => setStack(s => [...s, { type: 'forgot-password' }]); const openLegal = (page) => setStack(s => [...s, { type: 'legal', page }]); const openOrderSuccess = () => setStack([{ type: 'order-success' }]); const back = () => setStack(s => s.slice(0, -1)); const cartCount = cart.reduce((n, x) => n + x.qty, 0); let content; if (top?.type === 'product') { content = wishActions.toggle(top.product)} />; } else if (top?.type === 'orders') { content = ; } else if (top?.type === 'order-detail') { content = ; } else if (top?.type === 'profile-edit') { content = { await updateProfile(patch); back(); flashToast('הפרופיל עודכן'); }} />; } else if (top?.type === 'addresses') { content = ; } else if (top?.type === 'track-order') { content = ; } else if (top?.type === 'forgot-password') { content = ; } else if (top?.type === 'reset-password') { content = { back(); setTab('account'); flashToast('הסיסמה הוחלפה — התחברי שוב'); }} />; } else if (top?.type === 'legal') { content = ; } else if (top?.type === 'checkout') { content = { const paymentMap = { card: 'credit', apple: 'apple_pay', paypal: 'paypal', bit: 'bit', cod: 'cod', credit: 'credit' }; const address = { name: form.addr.name, email: form.addr.email, street: form.addr.street, city: form.addr.city, zip: form.addr.zip, phone: form.addr.phone, }; const body = { address, shippingMethod: form.delivery, paymentMethod: paymentMap[form.pay] || 'cod', }; if (user) { // Logged-in: server cart is the source of truth. Mirror local → server, then place. await window.API.del('/cart').catch(() => {}); for (const it of cart) { await window.API.post('/cart', { productId: it.product.id, size: it.size || null, qty: it.qty }); } } else { // Guest: items + (optional) account creation flow into the body. body.items = cart.map(it => ({ productId: it.product.id, size: it.size || null, qty: it.qty })); if (form.wantAccount && form.password) { body.createAccount = true; body.password = form.password; } } let result; try { result = await window.API.post('/orders', body); } catch (err) { if (err.message === 'out_of_stock') { const lines = (err.data?.items || []).map(i => `${i.name} (זמין: ${i.available}, ביקשת: ${i.requested})`).join('\n'); throw new Error('אזלו פריטים מהמלאי:\n' + lines); } if (err.message === 'email_taken') throw new Error('האימייל כבר רשום במערכת — נסי להתחבר במקום ליצור חשבון.'); if (err.message === 'guest_email_required') throw new Error('נדרש אימייל לסיום הזמנת אורח'); if (err.message === 'address_incomplete') throw new Error('נא למלא את כל פרטי המשלוח'); if (err.message === 'cart_empty') throw new Error('הסל ריק'); throw err; } console.log('[order placed]', result.order.orderNumber, '₪' + result.order.total); // If we created an account during checkout, sign the user in seamlessly. if (result.auth) { window.API.setToken(result.auth.token); setUser(result.auth.user); } // Redirect to Grow if the server returned a payment URL (card / wallets) // Save the address for next time if user opted in (logged in only) if (form.saveAddress && (user || result.auth)) { window.API.post('/addresses', { label: '', street: form.addr.street, city: form.addr.city, zip: form.addr.zip || null, phone: form.addr.phone, isDefault: false, }).catch(() => {}); } if (result.paymentUrl) { window.location.href = result.paymentUrl; return; } setCart([]); openOrderSuccess(); }} />; } else if (top?.type === 'order-success') { content = { setStack([]); setTab('home'); }} />; } else if (top?.type === 'category' && top.id === '__all') { content = setTab('search')} onBack={back} />; } else if (top?.type === 'category') { content = ; } else if (tab === 'shop') { content = setSearchOpen(true)} />; } else if (tab === 'wishlist') { content = wishActions.toggle(p)} />; } else if (tab === 'cart') { content = setTab('home')} onCheckout={openCheckout} />; } else if (tab === 'track') { // No onBack → back button hidden; user switches tabs to leave content = ; } else if (tab === 'account' && FEATURES.customerAccounts) { content = user ? : ; } else { content = setSearchOpen(true)} onOpenTrackOrder={openTrackOrder} onOpenLegal={openLegal} scrollRef={scrollRef} />; } // Show tab bar only on root tabs (not when drilled into stack) const showTabBar = stack.length === 0; // Helpers wired to desktop nav: jumping to a tab or category resets the screen stack const goTab = (t) => { setStack([]); setTab(t); }; const goCategory = (id) => { setTab('home'); setStack([{ type: 'category', id }]); }; // Desktop home shows the full-bleed editorial layout. All other screens fall back to the // centred mobile shell on desktop (intentional — it's a v1 scope decision). const isDesktopHome = isDesktop && tab === 'home' && stack.length === 0; const topnavTransparent = isDesktopHome && !pageScrolled; return ( <> {/* Desktop topnav lives only on the desktop home page; column views use their own in-screen header */} {isDesktopHome && ( setSearchOpen(true)} /> )} {isDesktopHome ? ( setSearchOpen(true)} /> ) : (
{content}
{/* Fixed tab bar — OUTSIDE scrollable region, hidden on desktop via class */} {showTabBar && }
)} {searchOpen && ( setSearchOpen(false)} onOpenProduct={(p) => { setSearchOpen(false); openProduct(p); }} onOpenCategory={(id) => { setSearchOpen(false); openCategory(id); }} /> )} ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();