onSelect(p)} style={{...panelStyle,padding:10,cursor:'pointer',marginBottom:0}}>
Saved & watching
{[
{ key:'saved', label:`Saved (${savedItems.length})` },
{ key:'watching', label:`Watching (${watchedItems.length})` },
].map(t => (
setTab(t.key)} style={{
padding:'8px 18px',borderRadius:999,border:'none',cursor:'pointer',minHeight:36,
fontFamily:'var(--font-body)',fontWeight:600,fontSize:13,
background: tab === t.key ? 'var(--paper)' : 'transparent',
color: tab === t.key ? 'var(--ink-900)' : 'var(--ink-500)',
boxShadow: tab === t.key ? 'var(--shadow-xs)' : 'none',transition:'all .15s'
}}>{t.label}
))}
{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 (
);
}
/* =================================================================
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 (
Back to messages
{thread?.avatar || 'S'}
{thread?.with || 'Sipho M.'}
Online
{messages.map((m,i) => (
))}
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 && (
)}
{stage === 1 && (
<>
Paying Me2You (Pty) Ltd
R {amount?.toLocaleString('en-ZA') || '1 910'}
>
)}
{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 (
{slides.map((_,i) => (
))}
{step > 0 && setStep(step-1)} style={{...btnGhost,flex:1,justifyContent:'center'}}>Back }
{step < slides.length - 1 ? (
setStep(step+1)} style={{...btnPrimary,flex:1,justifyContent:'center'}}>Next
) : (
Get started
)}
Skip tour
);
}
/* =================================================================
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')}
);
}
/* =================================================================
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 => (
{h}
))}
{AUDIT_ENTRIES.map((e,i) => (
{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.
Install
);
}
Object.assign(window, {
NotificationBell, SearchSuggestions, BrowseFilters,
WishlistPage, SellerStorefront, ChatList, ChatThread,
PayFastRedirect, OnboardingTour, EarningsChart, AdminAuditLog, PWAInstallBanner,
});