// Wander — Browse, Search, Saved, Profile, Paywall (Stripe), Completion, Offline
// ─── BROWSE ───────────────────────────────────────────────────────────────────
function BrowseScreen({ onOpenTour, onTab, onSearch, dense }) {
const { TOURS } = window.WANDER_DATA;
const [cat, setCat] = React.useState('All');
const cats = ['All','Free','Food','History','Architecture','Nature','After dark'];
const featured = TOURS[1];
const list = cat==='Free' ? TOURS.filter(t=>t.tier==='free') : TOURS;
return (
Tours · Singapore
Where to next?
{cats.map(c => (
))}
All tours
{list.map(t => (
))}
);
}
// ─── SEARCH ───────────────────────────────────────────────────────────────────
function SearchScreen({ onClose, onOpenTour }) {
const { TOURS } = window.WANDER_DATA;
const [q, setQ] = React.useState('');
const recent = ['Hawker','Sunset spots','Free tours','Architecture','Family-friendly'];
const matches = q ? TOURS.filter(t => t.title.toLowerCase().includes(q.toLowerCase()) || t.subtitle.toLowerCase().includes(q.toLowerCase())) : [];
return (
{!q ? (
Recent
{recent.map(r => (
setQ(r)} style={{ display:'flex', alignItems:'center', gap:12, padding:'14px 0', borderBottom:'1px solid var(--w-line)', cursor:'pointer' }}>
{r}
))}
Trending in Singapore
{['Chinatown food','Sunset Marina','Hidden gardens','Heritage shophouses','Peranakan Joo Chiat'].map(t => (
))}
) : (
{matches.length} {matches.length===1?'result':'results'}
{matches.map(t => (
onOpenTour(t.id)} style={{ display:'flex', gap:12, padding:'12px 0', cursor:'pointer', borderBottom:'1px solid var(--w-line)', alignItems:'center' }}>
))}
)}
);
}
// ─── SAVED ────────────────────────────────────────────────────────────────────
function SavedScreen({ onTab, onOpenTour, onOpenOffline, dense }) {
const { TOURS } = window.WANDER_DATA;
const saved = TOURS.slice(0,4);
return (
{['Wishlist','Downloaded','Completed'].map((t,i) => (
))}
{saved.map(t => (
onOpenTour(t.id)} style={{ background:'var(--w-paper)', borderRadius:22, padding:12, display:'flex', gap:14, alignItems:'center', boxShadow:'var(--sh-card)', cursor:'pointer' }}>
{t.title}
{t.duration} · {t.stops} stops
{t.formats.slice(0,3).map(f=>formatIcon(f,13,'var(--w-mute)'))}
))}
);
}
// ─── PROFILE ──────────────────────────────────────────────────────────────────
function ProfileScreen({ onTab, dense }) {
const stats = [{label:'Tours',v:'12'},{label:'Cities',v:'4'},{label:'Steps',v:'38k'}];
const badges = ['🦁 Singapore explorer','🌅 Sunrise walker','🍜 Hawker hunter','📜 History buff'];
return (
🧭
Alex Lim
Wandering since April 2025
Wander Pro
All tours, every city
$4.99/month after trial
Badges
{badges.map(b => {b})}
Settings
{['Language · English','Voice · Mei Ling','Notifications','Help & Feedback'].map((s,i,a) => (
{s}
))}
);
}
// ─── PAYWALL (with Stripe) ────────────────────────────────────────────────────
function PaywallScreen({ tourId, onClose, onSuccess }) {
const { TOURS } = window.WANDER_DATA;
const tour = TOURS.find(t => t.id===tourId) || TOURS[1];
const [plan, setPlan] = React.useState('pro');
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const plans = [
{ id:'tour', label:'Just this tour', price:'$'+tour.price, sub:'one-time', highlight:false },
{ id:'pro', label:'Wander Pro', price:'$4.99', sub:'per month · 7-day free trial', highlight:true },
];
// Listen for payment success from popup window
React.useEffect(() => {
const handler = (e) => {
if (e.data?.type==='WANDER_PAYMENT_SUCCESS') onSuccess();
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, [onSuccess]);
// Check if we landed back from Stripe redirect with ?unlocked=1
React.useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get('unlocked')==='1') {
window.history.replaceState({}, '', '/');
onSuccess();
}
}, []);
const handlePay = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch('/api/stripe/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tourId, plan }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Payment failed');
// Open Stripe Checkout in a small popup (stays inside the prototype view)
const popup = window.open(data.url, 'stripe_checkout', 'width=480,height=700,left=200,top=100');
if (!popup) {
// Fallback: redirect current tab
window.location.href = data.url;
}
} catch (e) {
if (e.message.includes('not configured')) {
// Demo mode — simulate success
setTimeout(() => onSuccess(), 1200);
} else {
setError(e.message);
}
} finally {
setLoading(false);
}
};
return (
{tour.emoji}
Unlock pro tour
{tour.title}
{tour.blurb}
{plans.map(p => (
))}
{['Unlimited tours in 47 cities','Offline downloads & maps','AR overlays + bonus stops','Cancel anytime'].map(t => (
-
{t}
))}
{error &&
{error}
}
Charged after 7 days. Cancel anytime in Settings.
);
}
// ─── COMPLETION ───────────────────────────────────────────────────────────────
function CompletionScreen({ tourId, onDone }) {
const { TOURS, STOPS } = window.WANDER_DATA;
const tour = TOURS.find(t=>t.id===tourId) || TOURS[0];
const [stars, setStars] = React.useState(5);
return (
{Array.from({length:20}).map((_,i) => {
const colors=['#FFC93C','#FF6B5A','#2DD4A7','#9B7EDC','#4FB7E8'];
return
;
})}
🏆
Tour complete
You wandered like a local.
{tour.title} · all {STOPS.length} stops · 1.8 km on foot
{[{l:'Time',v:'47 min'},{l:'Steps',v:'2,840'},{l:'Discoveries',v:'7'}].map(s => (
))}
🦁
Badge unlocked
Marina Bay Master
Rate this tour
{[1,2,3,4,5].map(n => )}
);
}
// ─── OFFLINE ──────────────────────────────────────────────────────────────────
function OfflineScreen({ onClose }) {
const { TOURS } = window.WANDER_DATA;
const downloads = [
{ tour:TOURS[0], pct:1, size:'24 MB' },
{ tour:TOURS[3], pct:1, size:'18 MB' },
{ tour:TOURS[1], pct:0.62, size:'32 MB', downloading:true },
];
return (
{downloads.map(d => (
{d.tour.title}
{d.downloading
? <>
Downloading… {Math.round(d.pct*100)}% · {d.size}
>
:
Available offline · {d.size}
}
))}
📡
Tip — download tours over Wi-Fi before flying. Maps work without signal.
);
}
Object.assign(window, { BrowseScreen, SearchScreen, SavedScreen, ProfileScreen, PaywallScreen, CompletionScreen, OfflineScreen });