// Wander — Creator Studio (Dashboard + Tour Editor) const HERO_COLORS = ['#FFC93C','#FF8A4C','#FF6B5A','#2DD4A7','#4FB7E8','#9B7EDC','#E89B6C','#1A1812']; const ALL_FORMATS = ['audio','video','text','ar','quiz']; // ─── CREATOR DASHBOARD ──────────────────────────────────────────────────────── function CreatorDashboardScreen({ authUser, authToken, onBack, onCreateTour, onEditTour }) { const [tours, setTours] = React.useState([]); const [loading, setLoading] = React.useState(true); const wrapRef = React.useRef(null); const loadTours = () => { fetch('/api/my/tours', { headers:{ Authorization:`Bearer ${authToken}` } }) .then(r => r.json()) .then(d => { setTours(Array.isArray(d)?d:[]); setLoading(false); }) .catch(() => setLoading(false)); }; React.useEffect(() => { loadTours(); if (window.anime && wrapRef.current) { anime({ targets: wrapRef.current, opacity:[0,1], translateY:[20,0], duration:400, easing:'easeOutQuart' }); } }, []); const deleteTour = async id => { if (!window.confirm('Delete this tour? This cannot be undone.')) return; await fetch(`/api/my/tours/${id}`, { method:'DELETE', headers:{ Authorization:`Bearer ${authToken}` } }); setTours(t => t.filter(x => x.id !== id)); if (window.anime) { anime({ targets:`[data-tour-id="${id}"]`, opacity:[1,0], height:[80,0], marginBottom:[10,0], duration:280, easing:'easeInQuart' }); } }; const totalAudio = tours.filter(t => t.audio_url).length; const totalVideo = tours.filter(t => t.video_url).length; return (
{/* Header */}
Creator Studio
{authUser?.avatar_emoji || '🧭'} {authUser?.name || 'My Tours'}
{/* Stats */}
{[['Tours',tours.length,'🗺️'],['🎙️ Audio',totalAudio,null],['📹 Video',totalVideo,null]].map(([label,v]) => (
{v}
{label}
))}
{/* Tour list */}
{loading ? (
Loading your tours…
) : tours.length === 0 ? (
🗺️
No tours yet
Create your first tour and share your local stories with travelers from around the world.
) : (
{tours.map(t => (
{t.title}
{t.duration} · {t.stops} stops
{t.tier==='pro' ? PRO ${t.price} : FREE} {t.audio_url ? '🎙️ audio' : '⚠️ no audio'} {t.video_url && 📹 video}
))}
)}
); } // ─── TOUR EDITOR ────────────────────────────────────────────────────────────── function TourEditorScreen({ authToken, existingTour, onBack }) { const isNew = !existingTour; const [tab, setTab] = React.useState('details'); const [savedTour, setSavedTour] = React.useState(existingTour || null); const [saving, setSaving] = React.useState(false); const [saveErr, setSaveErr] = React.useState(''); const [saveOk, setSaveOk] = React.useState(''); const [form, setForm] = React.useState({ title: existingTour?.title || '', subtitle: existingTour?.subtitle || '', blurb: existingTour?.blurb || '', duration: existingTour?.duration || '30 min', distance: existingTour?.distance || '1.0 km', stops: existingTour?.stops || 3, tier: existingTour?.tier || 'free', price: existingTour?.price || 0, emoji: existingTour?.emoji || '📍', formats: existingTour?.formats || ['audio'], hero: existingTour?.hero || ['#FFC93C','#FF8A4C'], }); // Media upload state const [audioProgress, setAudioProgress] = React.useState(null); const [audioError, setAudioError] = React.useState(''); const [audioDone, setAudioDone] = React.useState(!!existingTour?.audio_url); const [videoProgress, setVideoProgress] = React.useState(null); const [videoError, setVideoError] = React.useState(''); const [videoDone, setVideoDone] = React.useState(!!existingTour?.video_url); const audioInputRef = React.useRef(); const videoInputRef = React.useRef(); const currentId = savedTour?.id; const setF = k => e => setForm(f => ({ ...f, [k]: e.target.value })); const setFN = k => e => setForm(f => ({ ...f, [k]: parseFloat(e.target.value)||0 })); const toggleFormat = fmt => setForm(f => ({ ...f, formats: f.formats.includes(fmt) ? f.formats.filter(x=>x!==fmt) : [...f.formats, fmt], })); const handleSave = async e => { e.preventDefault(); if (!form.title.trim()) { setSaveErr('Tour title is required'); return; } setSaving(true); setSaveErr(''); setSaveOk(''); try { const method = (isNew && !savedTour) ? 'POST' : 'PUT'; const url = (isNew && !savedTour) ? '/api/my/tours' : `/api/my/tours/${currentId}`; const r = await fetch(url, { method, headers:{ 'Content-Type':'application/json', Authorization:`Bearer ${authToken}` }, body: JSON.stringify({ ...form, stops: parseInt(form.stops)||0 }), }); const d = await r.json(); if (!r.ok) throw new Error(d.error); setSavedTour(d); setSaveOk(isNew && !savedTour ? 'Tour created! Now upload your audio & video below.' : 'Changes saved ✓'); if (isNew && !savedTour) { setTimeout(() => setTab('media'), 800); } } catch(e) { setSaveErr(e.message); } finally { setSaving(false); } }; const uploadFile = (type, file) => { if (!currentId) return; const setProgress = type==='audio' ? setAudioProgress : setVideoProgress; const setError = type==='audio' ? setAudioError : setVideoError; const setDone = type==='audio' ? setAudioDone : setVideoDone; setProgress(0); setError(''); const fd = new FormData(); fd.append('file', file); const xhr = new XMLHttpRequest(); xhr.open('POST', `/api/my/tours/${currentId}/${type}`); xhr.setRequestHeader('Authorization', `Bearer ${authToken}`); xhr.upload.onprogress = ev => { if (ev.lengthComputable) setProgress(Math.round(ev.loaded/ev.total*100)); }; xhr.onload = () => { if (xhr.status===200) { setDone(true); setProgress(null); } else { setError('Upload failed — please try again'); setProgress(null); } }; xhr.onerror = () => { setError('Network error — check your connection'); setProgress(null); }; xhr.send(fd); }; const deleteMedia = async type => { if (!currentId) return; await fetch(`/api/my/tours/${currentId}/${type}`, { method:'DELETE', headers:{ Authorization:`Bearer ${authToken}` } }); if (type==='audio') setAudioDone(false); else setVideoDone(false); }; // Styles const S = { label: { fontSize:11, fontWeight:700, color:'var(--w-mute)', textTransform:'uppercase', letterSpacing:0.5, display:'block', marginBottom:5 }, input: { width:'100%', padding:'12px 14px', borderRadius:13, border:'1.5px solid var(--w-line)', fontSize:14, fontFamily:'inherit', color:'var(--w-ink)', background:'#fff', boxSizing:'border-box', outline:'none' }, section: { marginBottom:18 }, chip: active => ({ padding:'8px 14px', borderRadius:999, border:'none', cursor:'pointer', fontFamily:'inherit', fontWeight:600, fontSize:12, background: active?'var(--w-ink)':'var(--w-paper)', color: active?'var(--w-yellow)':'var(--w-mute)', boxShadow: active?'none':'inset 0 0 0 1px var(--w-line)' }), }; return (
{/* Header */}
{isNew && !savedTour ? 'New Tour' : form.title || 'Edit Tour'}
{form.emoji && {form.emoji}}
{/* Tab bar */}
{[['details','📝 Details'],['media','🎙️ Media']].map(([id,label]) => ( ))}
{/* Scrollable content */}
{/* ── DETAILS TAB ── */} {tab==='details' && (