/* Me2You - Polish features: Notifications, Search suggestions, Filters, Wishlist, Storefront, Chat, PayFast, Onboarding, Analytics, Audit log, PWA banner */ /* global React, Icon, StatusTag, Price, panelStyle, btnPrimary, btnGhost, btnSmall, btnDanger, PRODUCTS */ const { useState, useEffect, useRef } = React; /* ================================================================= 1. NOTIFICATIONS BELL ================================================================= */ const NOTIFICATIONS = [ { id:0, type:'price-drop', icon:'tag', title:'Price dropped', body:'Price dropped from R 349 to R 299 on Running shoes, Nike sz 10.', time:'2 days ago', unread:true, color:'var(--brand-orange)' }, { id:1, type:'order', icon:'truck', title:'Your order is on the way', body:'Bongani K. is heading to you with the Hisense TV.', time:'5 min ago', unread:true, color:'var(--info)' }, { id:2, type:'payment', icon:'wallet', title:'You\'re paid up', body:'Payment of R 1 910 received for M2Y-2026-00248.', time:'1 hour ago', unread:true, color:'var(--success)' }, { id:3, type:'message', icon:'message-circle', title:'New message from Thandi K.', body:'"Hi, is the price negotiable?"', time:'2 hours ago', unread:true, color:'var(--brand-orange)' }, { id:4, type:'review', icon:'star', title:'New 5-star review', body:'Nomsa M. left you a glowing review.', time:'Yesterday', unread:false, color:'var(--warning)' }, { id:5, type:'dispute', icon:'triangle-alert', title:'Dispute resolved', body:'M2Y-2026-00232 was resolved in your favour.', time:'2 days ago', unread:false, color:'var(--danger)' }, ]; function NotificationBell() { const [open, setOpen] = useState(false); const unread = NOTIFICATIONS.filter(n => n.unread).length; return (
{open && ( <>
setOpen(false)} style={{position:'fixed',inset:0,zIndex:30}}/>

Notifications

{NOTIFICATIONS.map(n => (
e.currentTarget.style.background='var(--ink-50)'} onMouseLeave={e => e.currentTarget.style.background= n.unread ? 'var(--brand-orange-50)' : 'transparent'}>
{n.title} {n.unread && }

{n.body}

{n.time}
))}
)}
); } /* ================================================================= 2. LIVE SEARCH SUGGESTIONS ================================================================= */ function SearchSuggestions({ value, onChange, onSelect }) { const [focused, setFocused] = useState(false); const recent = ['Samsung A14', 'Power drill', 'School blazer']; const trending = ['iPhone', 'Microwave', 'Bicycle', 'Cast-iron pot']; const matches = value ? PRODUCTS.filter(p => p.title.toLowerCase().includes(value.toLowerCase()) ).slice(0,5) : []; return (
onChange(e.target.value)} onFocus={() => setFocused(true)} onBlur={() => setTimeout(() => setFocused(false), 200)} 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 && ( )} {focused && (
{matches.length > 0 && ( <>
Listings
{matches.map(p => ( ))} )} {!value && ( <>
Recent searches
{recent.map(r => ( ))}
Trending
{trending.map(t => ( ))}
)}
)}
); } /* ================================================================= 3. BROWSE FILTERS SIDEBAR ================================================================= */ function BrowseFilters({ filters, onChange }) { return ( ); } const inputStyle = { padding:'8px 12px',border:'1.5px solid var(--ink-200)',borderRadius:'var(--radius-sm)', fontFamily:'var(--font-body)',fontSize:13,background:'var(--paper)',width:'100%',boxSizing:'border-box' }; /* ================================================================= 4. WISHLIST / SAVED PAGE ================================================================= */ function WishlistPage({ saved, watched = [], onSelect, onUnsave, onToggleWatch }) { const [tab, setTab] = useState('saved'); const savedItems = saved.map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean); const watchedItems = watched.map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean); const items = tab === 'saved' ? savedItems : watchedItems; const card = (p) => { const isWatched = watched.includes(p.id); return (
onSelect(p)} style={{...panelStyle,padding:10,cursor:'pointer',marginBottom:0}}>

{p.title}

{tab === 'saved' && ( )}
); }; return (

Saved & watching

{[ { key:'saved', label:`Saved (${savedItems.length})` }, { key:'watching', label:`Watching (${watchedItems.length})` }, ].map(t => ( ))}
{tab === 'watching' && watchedItems.length > 0 && (
Items you watch send a free in-app alert if the seller drops the price.
)} {items.length === 0 ? (

{tab === 'saved' ? 'No saved items yet' : 'You are not watching anything yet'}

{tab === 'saved' ? 'Tap the heart on any listing to save it for later.' : 'Tap Watch on a listing to be told if the price drops.'}

) : (
{items.map(card)}
)}
); } /* ================================================================= 5. SELLER STOREFRONT ================================================================= */ function SellerStorefront({ sellerName, onBack, onSelect }) { const seller = sellerName || 'Sipho M.'; const sellerListings = PRODUCTS.filter(p => p.seller === seller); const allRatings = sellerListings.map(p => p.rating); const avgRating = (allRatings.reduce((a,b) => a+b, 0) / allRatings.length).toFixed(1); const [following, setFollowing] = useState(false); return (
{seller.charAt(0)}

{seller}

{avgRating} avg - {sellerListings.length} listings - {sellerListings[0]?.location} - Joined Mar 2025

Listings from {seller}

{sellerListings.map(p => (
onSelect && onSelect(p)} style={{...panelStyle,padding:10,cursor:'pointer',marginBottom:0}}>
{p.emoji}

{p.title}

))}
); } /* ================================================================= 6. MESSAGES / CHAT ================================================================= */ const CHAT_THREADS = [ { id:1, with:'Sipho M.', last:'I can drop it off tomorrow if that works', time:'5 min ago', unread:true, avatar:'S', bg:'var(--brand-orange-100)' }, { id:2, with:'Thandi K.', last:'Sure, R1800 works for me', time:'2 hours ago', unread:false, avatar:'T', bg:'var(--success-100)' }, { id:3, with:'Mama K.', last:'Is the cast iron pot still available?', time:'Yesterday', unread:false, avatar:'M', bg:'var(--info-100)' }, ]; function ChatList({ onOpenThread }) { return (

Messages

{CHAT_THREADS.map(t => (
onOpenThread(t)} style={{...panelStyle,display:'flex',gap:12,alignItems:'center',cursor:'pointer'}}>
{t.avatar}
{t.with} {t.time}

{t.last}

{t.unread &&
}
))}
); } function ChatThread({ thread, onBack }) { const [input, setInput] = useState(''); const [messages, setMessages] = useState([ { from:'them', text:'Hi! Is this still available?', time:'10:23' }, { from:'me', text:'Yes it is! In great condition.', time:'10:25' }, { from:'them', text:'Would you take R 1 800?', time:'10:26' }, { from:'me', text:'Hmm, I could do R 1 850.', time:'10:28' }, { from:'them', text:thread?.last || 'I can drop it off tomorrow if that works', time:'10:30' }, ]); const send = () => { if (!input.trim()) return; setMessages(prev => [...prev, { from:'me', text:input, time:'now' }]); setInput(''); }; return (
{thread?.avatar || 'S'}
{thread?.with || 'Sipho M.'}
Online
{messages.map((m,i) => (
{m.text}
{m.time}
))}
setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Type a message..." style={{flex:1,padding:'10px 14px',border:'1.5px solid var(--ink-200)',borderRadius:'var(--radius-pill)',fontFamily:'var(--font-body)',fontSize:14}}/>
); } /* ================================================================= 7. PAYFAST SIMULATION ================================================================= */ function PayFastRedirect({ amount, onComplete }) { const [stage, setStage] = useState(0); // 0=loading, 1=form, 2=processing useEffect(() => { const t = setTimeout(() => setStage(1), 1200); return () => clearTimeout(t); }, []); const pay = () => { setStage(2); setTimeout(() => onComplete(), 1800); }; return (
PayFast
SECURE PAYMENT GATEWAY - ZA
{stage === 0 && (

Connecting to PayFast...

)} {stage === 1 && ( <>
Paying Me2You (Pty) Ltd
R {amount?.toLocaleString('en-ZA') || '1 910'}
{['Card','EFT','Mobile'].map((m,i) => ( ))}

Your card details never touch Me2You's servers - they're encrypted end-to-end by PayFast.

)} {stage === 2 && (

Processing your payment...

Do not close this window.

)}
); } /* ================================================================= 8. ONBOARDING TOUR ================================================================= */ function OnboardingTour({ onComplete }) { const [step, setStep] = useState(0); const slides = [ { title:'Welcome to Me2You', body:"South Africa's safer C2C marketplace. Buy and sell with neighbours you can trust.", icon:'shield-check', color:'var(--brand-orange)' }, { title:'Your money is safe', body:"We hold the buyer's payment until they confirm receipt. No more fake proof of payment.", icon:'wallet', color:'var(--success)' }, { title:'Three ways to receive', body:"Courier to your door, collect from a pickup point, or meet in person with OTP self-collect.", icon:'truck', color:'var(--info)' }, { title:'Built for South Africa', body:"POPIA-compliant. PayFast secure. Works on any phone, even on 3G.", icon:'star', color:'var(--brand-plum)' }, ]; const s = slides[step]; return (

{s.title}

{s.body}

{slides.map((_,i) => (
))}
{step > 0 && } {step < slides.length - 1 ? ( ) : ( )}
); } /* ================================================================= 9. SELLER EARNINGS ANALYTICS CHART ================================================================= */ function EarningsChart() { const data = [ { month:'Nov', amount:420 }, { month:'Dec', amount:680 }, { month:'Jan', amount:540 }, { month:'Feb', amount:920 }, { month:'Mar', amount:1240 }, { month:'Apr', amount:1850 }, ]; const max = Math.max(...data.map(d => d.amount)); return (

Earnings (6 months)

R {data.reduce((s,d) => s + d.amount, 0).toLocaleString('en-ZA')}
{data.map((d,i) => (
R {d.amount}
{d.month}
))}
Avg / month
R 942
Growth
+49%
Best month
April
); } /* ================================================================= 10. ADMIN AUDIT LOG ================================================================= */ const AUDIT_ENTRIES = [ { time:'1 May 2026, 14:23', actor:'Marco P.', action:'suspended_user', target:'Bongani Tshabalala', detail:'Repeated low ratings' }, { time:'1 May 2026, 11:08', actor:'Marco P.', action:'approved_listing', target:'iPhone 11 64GB', detail:'Pending moderation queue' }, { time:'1 May 2026, 09:45', actor:'Marco P.', action:'resolved_dispute', target:'M2Y-2026-00232', detail:'Refunded buyer R155' }, { time:'30 Apr 2026, 16:30', actor:'System', action:'auto_release_escrow', target:'M2Y-2026-00241', detail:'48h post-delivery' }, { time:'30 Apr 2026, 14:12', actor:'Marco P.', action:'rejected_listing', target:'Fake Gucci bag', detail:'Counterfeit suspected' }, { time:'30 Apr 2026, 10:00', actor:'Marco P.', action:'batch_payout', target:'5 sellers', detail:'R 3 380 total' }, ]; function AdminAuditLog() { return (

Audit log

Every admin action and system event, immutable.

{['Time','Actor','Action','Target','Detail'].map(h => ( ))} {AUDIT_ENTRIES.map((e,i) => ( ))}
{h}
{e.time} {e.actor} {e.action} {e.target} {e.detail}
); } /* ================================================================= 11. PWA INSTALL BANNER ================================================================= */ function PWAInstallBanner({ onDismiss }) { return (
Add Me2You to your home screen
Works offline. Lighter than the browser.
); } Object.assign(window, { NotificationBell, SearchSuggestions, BrowseFilters, WishlistPage, SellerStorefront, ChatList, ChatThread, PayFastRedirect, OnboardingTour, EarningsChart, AdminAuditLog, PWAInstallBanner, });