/* Me2You - Marketplace screens: Home, Browse, PDP, Cart, Checkout, Success, Orders, Dispute */ /* global React, Icon, StatusTag, Field, OrderStepper, Price, CondTag, PageTransition, EmptyState, btnPrimary, btnGhost, btnDanger, btnSmall, panelStyle, PRODUCTS, CATEGORIES, SAMPLE_ORDERS */ const { useState, useEffect, useRef, useMemo } = React; /* --- Hero --- */ function MarketplaceHero({ onBrowse, onSell }) { return (
Made in SA - POPIA-safe

Sell to your
neighbours, safely.

We hold your buyer's payment until they confirm receipt. No fake proof of payment, no cash meet-ups, no fuss.

); } /* --- Category Rail --- */ function CategoryRail({ active, onPick }) { return (
{CATEGORIES.map(c => ( ))}
); } /* --- Product Card --- */ function ProductCard({ product, onClick, onAddToCart }) { const [hovered, setHovered] = useState(false); const [liked, setLiked] = useState(false); return (
setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ background:'var(--paper)',borderRadius:'var(--radius-lg)', border:'1px solid var(--ink-200)', boxShadow: hovered ? 'var(--shadow-md)' : 'var(--shadow-sm)', padding:10,cursor:'pointer', transition:'all .2s cubic-bezier(.2,.7,.3,1)', transform: hovered ? 'translateY(-3px)' : 'none', display:'flex',flexDirection:'column',position:'relative' }}>
{product.emoji} {product.title.split(' ').slice(0,2).join(' ')}

{product.title}

{product.location} - {product.seller}
{product.rating}
); } /* --- Product Grid --- */ function ProductGrid({ products, onSelect, onAddToCart }) { return (
{products.map(p => onSelect(p)} onAddToCart={onAddToCart}/>)}
); } /* --- Search bar --- */ function SearchBar({ value, onChange, onClear }) { return (
onChange(e.target.value)} placeholder="Search listings, e.g. 'Hisense TV'" style={{width:'100%',padding:'12px 40px 12px 42px',border:'1.5px solid var(--ink-200)', borderRadius:'var(--radius-pill)',background:'var(--ink-50)', fontFamily:'var(--font-body)',fontSize:14,boxSizing:'border-box', transition:'border-color .15s'}}/> {value && ( )}
); } /* --- Lay-bye panel: reserve with deposit --- */ function LaybyePanel({ product, onReserve }) { const [agreed, setAgreed] = useState(false); const [reserved, setReserved] = useState(false); const [ackAt, setAckAt] = useState(null); // Split the price into a deposit plus 3 weekly instalments (4 payments total) const instalments = 4; const each = Math.floor((product.price / instalments) * 100) / 100; const last = Math.round((product.price - each * (instalments - 1)) * 100) / 100; const schedule = [ { label:'Deposit today', amount: each }, { label:'Instalment 2 - in 1 week', amount: each }, { label:'Instalment 3 - in 2 weeks', amount: each }, { label:'Instalment 4 - in 3 weeks', amount: last }, ]; const toggleAck = () => { setAgreed(prev => { const next = !prev; setAckAt(next ? new Date().toLocaleString('en-ZA', { dateStyle:'medium', timeStyle:'short' }) : null); return next; }); }; if (reserved) { return (
Item reserved on lay-bye
Deposit of R {each.toLocaleString('en-ZA')} taken. We hold the item for you. It ships once paid in full.
); } return (

Reserve on lay-bye

Pay a deposit today, then weekly instalments. The item ships once it is paid in full.

{schedule.map((s,i) => (
{s.label} R {s.amount.toLocaleString('en-ZA')}
))}
); } /* --- Auction / Bid panel: countdown, current bid, place bid --- */ function AuctionPanel({ product }) { const a = product.auction; const [remaining, setRemaining] = useState(a ? a.endsInMs : 0); const [currentBid, setCurrentBid] = useState(a ? a.currentBid : 0); const [bids, setBids] = useState(a ? a.bids : 0); const [won, setWon] = useState(false); const minNext = currentBid + (a ? a.minIncrement : 50); const [bidValue, setBidValue] = useState(minNext); const [error, setError] = useState(''); useEffect(() => { if (won || remaining <= 0) return; const t = setInterval(() => setRemaining(r => Math.max(0, r - 1000)), 1000); return () => clearInterval(t); }, [won, remaining]); const ended = remaining <= 0; const fmt = (ms) => { const total = Math.floor(ms / 1000); const h = String(Math.floor(total / 3600)).padStart(2,'0'); const m = String(Math.floor((total % 3600) / 60)).padStart(2,'0'); const s = String(total % 60).padStart(2,'0'); return `${h}:${m}:${s}`; }; const placeBid = () => { const v = Number(bidValue); if (!v || v < minNext) { setError(`Your bid must be at least R ${minNext.toLocaleString('en-ZA')}.`); return; } setError(''); setCurrentBid(v); setBids(b => b + 1); setBidValue(v + (a ? a.minIncrement : 50)); // Simulate the auction closing in your favour shortly after you take the lead setRemaining(4000); setTimeout(() => setWon(true), 4200); }; if (won) { return (
You won the auction
Winning bid R {currentBid.toLocaleString('en-ZA')}. This pays through escrow. Funds release on confirmed receipt.
); } return (

Live auction

{ended ? 'Ended' : fmt(remaining)}
Current bid R {currentBid.toLocaleString('en-ZA')}
{bids} bids - top bidder {a ? a.topBidder : ''}
{ended ? (
This auction has closed. The highest bidder pays through escrow.
) : ( <>
setBidValue(e.target.value)} style={{flex:1,padding:'13px 14px',border:'1.5px solid var(--ink-200)',borderRadius:'var(--radius-sm)', fontFamily:'var(--font-body)',fontSize:15,boxSizing:'border-box',background:'var(--paper)'}}/>
{error &&
{error}
} )}
Winning bid pays through escrow. Funds released on confirmed receipt.
); } /* --- PDP --- */ function ProductDetail({ product, onBack, onAddToCart, onSwap, onWatch, watched }) { if (!product) return null; const [activeImg, setActiveImg] = useState(0); const [showShare, setShowShare] = useState(false); const [payMode, setPayMode] = useState('buy'); const hasTabs = product.laybye || product.auction; // Simulate multiple images by using same emoji with different bg tints const images = [ product.bg, `linear-gradient(135deg, var(--ink-100), var(--brand-orange-50))`, `linear-gradient(135deg, var(--brand-orange-100), var(--ink-50))`, product.bg, ]; const reviews = [ { buyer:'Thandi K.', stars:5, comment:'Exactly as described. Fast delivery!', date:'2 days ago' }, { buyer:'James R.', stars:5, comment:'Great seller, very responsive on WhatsApp.', date:'1 week ago' }, { buyer:'Siya N.', stars:4, comment:'Good condition, minor scratch not in photos but ok.', date:'2 weeks ago' }, ]; const moreFromSeller = PRODUCTS.filter(p => p.seller === product.seller && p.id !== product.id).slice(0,3); return (
{product.sellerOnVacation && (
Shop on a short break {product.seller} is away until {product.vacationUntil}. You can save this listing and watch it for updates. Buy now is paused.
)}
{/* Image gallery */}
{product.emoji} {product.title.split(' ').slice(0,3).join(' ')}
{activeImg+1} / {images.length}
{images.map((bg,i) => (

{product.title}

{product.location} - {product.rating} ({product.reviews} reviews)
R {product.price.toLocaleString('en-ZA')}
Paid through Me2You - your money is safe until you confirm receipt.

Excellent condition, used for about a year. Original packaging included. Pickup or courier - your choice.

{/* Pay-mode tabs: Buy / Lay-bye / Bid (only for eligible listings) */} {hasTabs && (
{[ { key:'buy', label:'Buy now' }, product.laybye && { key:'laybye', label:'Lay-bye' }, product.auction && { key:'bid', label:'Bid' }, ].filter(Boolean).map(t => ( ))}
)} {hasTabs && payMode === 'laybye' && } {hasTabs && payMode === 'bid' && } {(!hasTabs || payMode === 'buy') && (
{showShare && (
{[ { label:'WhatsApp', icon:'message-circle', color:'#25D366' }, { label:'Copy link', icon:'tag', color:'var(--ink-700)' }, { label:'SMS', icon:'phone', color:'var(--info)' }, ].map(opt => ( ))}
)}
)} {/* Seller card */}
{product.seller.charAt(0)}
{product.seller}
{product.rating} - Joined Mar 2025
{/* Reviews */}

Reviews ({product.reviews})

{product.rating} average
{reviews.map((r,i) => (
{r.buyer} {[1,2,3,4,5].map(n => )}

{r.comment}

{r.date}
))}
{/* More from this seller */} {moreFromSeller.length > 0 && (

More from {product.seller}

{moreFromSeller.map(p => {}}/>)}
)}
); } /* --- Cart --- */ function CartView({ cart, onUpdateQty, onRemove, onCheckout, onBrowse }) { if (cart.length === 0) return ( ); const subtotal = cart.reduce((s,i) => s + i.product.price * i.qty, 0); const delivery = 60; return (

Your cart

{cart.map(i => (
{i.product.title}
From {i.product.seller}, {i.product.location}
{i.qty}
))}
TotalR {(subtotal + delivery).toLocaleString('en-ZA')}
); } function SummaryRow({ label, value }) { return (
{label}{value}
); } /* --- Checkout --- */ function CheckoutView({ cart, onSuccess }) { const [delivery, setDelivery] = useState('pickup'); const [tip, setTip] = useState(10); const [radius, setRadius] = useState(8); const subtotal = cart.reduce((s,i) => s + i.product.price * i.qty, 0); // Distance-based pricing for door-to-door driver const calcDoorPrice = (km) => { const base = 25; const perKm = 5; const surge = km > 20 ? Math.ceil((km - 20) / 5) * 10 : 0; return base + Math.round(km * perKm) + surge; }; const doorPrice = calcDoorPrice(radius); const pickupPrice = 30; const deliveryCost = delivery === 'door' ? doorPrice : delivery === 'pickup' ? pickupPrice : 0; const total = subtotal + deliveryCost + tip; return (

Checkout

Shipping, delivery and payment

Where should we send it?

How do you want it?

{[ { key:'pickup', icon:'map-pin', title:'Pickup point', desc:`Collect from your nearest PEP / Pargo / Paxi. R ${pickupPrice}.`, price: pickupPrice }, { key:'door', icon:'truck', title:'Driver to your door', desc:`Distance-based. Currently R ${doorPrice} for ${radius} km.`, price: doorPrice }, { key:'self', icon:'user-round', title:'Self-collect with OTP', desc:'Meet seller, inspect, release with 6-digit code. Free.', price: 0 }, ].map(opt => (
setDelivery(opt.key)} style={{ display:'flex',gap:12,padding:14,borderRadius:'var(--radius-md)',cursor:'pointer', border: '1.5px solid ' + (delivery === opt.key ? 'var(--brand-orange)' : 'var(--ink-200)'), background: delivery === opt.key ? 'var(--brand-orange-50)' : 'transparent',marginBottom:8, transition:'all .15s ease' }}>
{opt.title}
{opt.desc}
{opt.price === 0 ? 'Free' : `R ${opt.price}`}
))} {delivery === 'door' && (
Distance from seller {radius} km
setRadius(+e.target.value)} style={{width:'100%',accentColor:'var(--brand-orange)'}}/>
2 km - R {calcDoorPrice(2)} 20 km - R {calcDoorPrice(20)} 40 km - R {calcDoorPrice(40)}
R 25 base + R 5/km{radius > 20 ? + R {Math.ceil((radius - 20)/5)*10} long-distance surcharge : ''}. 100% of the delivery fee goes to your driver.
)}

Tip your driver (optional, 100% to driver)

{[0,5,10,20].map(t => ( ))}

Pay safely with PayFast

EFT, card, or mobile. We hold your money until you confirm receipt.

SubtotalR {subtotal.toLocaleString('en-ZA')}
Delivery ({delivery === 'door' ? `${radius} km` : delivery})R {deliveryCost}
{tip > 0 &&
Driver tipR {tip}
}
TotalR {total.toLocaleString('en-ZA')}
); } /* --- Order Success --- */ function OrderSuccess({ onContinue, onConfirmCollect }) { return (

You're paid up.

We've let the seller know. They have 3 days to dispatch your order.

Order reference
M2Y-2026-00248
{onConfirmCollect && ( )}
); } /* --- Orders List --- */ function OrdersList({ orders, onTrack }) { return (

Your orders

{orders.map(o => (
onTrack && onTrack(o)} style={{...panelStyle,display:'grid',gridTemplateColumns:'60px 1fr auto',gap:16,alignItems:'center',cursor:'pointer',transition:'box-shadow .15s'}} onMouseEnter={e => e.currentTarget.style.boxShadow='var(--shadow-md)'} onMouseLeave={e => e.currentTarget.style.boxShadow='none'}>
{o.product.title}
{o.id} - {o.date}
))}
); } /* --- Dispute Form --- */ function DisputeForm({ onBack }) { const [submitted, setSubmitted] = useState(false); const [reason, setReason] = useState(''); const [chatInput, setChatInput] = useState(''); const [messages, setMessages] = useState([ { from:'admin', name:'Marco (Admin)', text:"Hi, we've received your dispute. Can you share photos of the issue?", time:'just now' }, ]); const send = () => { if (!chatInput.trim()) return; setMessages(prev => [...prev, { from:'me', name:'You', text:chatInput, time:'now' }]); setChatInput(''); // Simulate admin response setTimeout(() => { setMessages(prev => [...prev, { from:'admin', name:'Marco (Admin)', text:"Thanks. We'll review and respond within 24 hours.", time:'now' }]); }, 1500); }; if (submitted) { return (

Dispute opened

Case DSP-2026-0142 - Funds frozen in escrow

What happens next

{[ { icon:'shield-check', text:'Escrow funds are frozen until resolved', done:true }, { icon:'message-circle', text:'Admin contacts both parties within 24 hours', done:false }, { icon:'check-circle', text:'Resolution: refund, release, or partial', done:false }, ].map((step,i) => (
{step.text}
))}
{/* Live admin chat */}
Me2You Support
Usually replies in 2 hours
{messages.map((m,i) => (
{m.name}
{m.text}
))}
setChatInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Message support..." style={{flex:1,padding:'10px 14px',border:'1.5px solid var(--ink-200)',borderRadius:'var(--radius-pill)', fontFamily:'var(--font-body)',fontSize:14}}/>
Need urgent help?
Call us on 0860 ME 2 YOU - WhatsApp +27 71 234 5678 - Mon-Fri 8am-6pm
); } return (

Something not right?

We've got you. Tell us what happened and our team will be in touch within 24 hours.

Order details

{[ {key:'not-received', label:'Never arrived', icon:'package'}, {key:'damaged', label:'Arrived damaged', icon:'triangle-alert'}, {key:'wrong-item', label:'Wrong item', icon:'x'}, {key:'fake', label:'Not as described', icon:'eye'}, ].map(r => ( ))}