// Admin Panel — full CRUD for events, mixes, tickets, campaigns, etc. var { useState, useEffect, useRef } = React; const ADMIN_TABS = [ { id: 'overview', label: 'Overview' }, { id: 'events', label: 'Events' }, { id: 'mixes', label: 'Mixes' }, { id: 'guestlist', label: 'Guest List' }, { id: 'waitlist', label: 'Waitlist' }, { id: 'attendees', label: 'Attendees' }, { id: 'messages', label: 'Messages' }, { id: 'campaigns', label: 'Email Campaigns' }, { id: 'sponsors', label: 'Sponsors' }, ]; // Pick the nearest upcoming event from a date-DESC list function nextUpcomingId(events) { const today = new Date().toISOString().slice(0, 10); const upcoming = events.filter(e => e.date >= today); if (upcoming.length) return upcoming[upcoming.length - 1].id; // last = closest date return events[0] ? events[0].id : ''; } const FS = { fontFamily:"'Space Grotesk',sans-serif" }; const inputSt = { ...FS,width:'100%',padding:'10px 14px',background:'rgba(255,255,255,0.06)',border:'1px solid rgba(255,255,255,0.15)',borderRadius:0,color:'#fff',fontSize:14,outline:'none',boxSizing:'border-box',marginBottom:10 }; const labelSt = { ...FS,color:'rgba(255,255,255,0.5)',fontSize:11,letterSpacing:'0.1em',textTransform:'uppercase',display:'block',marginBottom:4 }; function AdminInput({ label, ...props }) { return React.createElement('div', { style:{marginBottom:14} }, React.createElement('label', { style:labelSt }, label), React.createElement('input', { ...props, style:inputSt }) ); } function AdminTextarea({ label, rows=4, ...props }) { return React.createElement('div', { style:{marginBottom:14} }, React.createElement('label', { style:labelSt }, label), React.createElement('textarea', { ...props, rows, style:{ ...inputSt, resize:'vertical', marginBottom:0 } }) ); } function AdminSelect({ label, options, ...props }) { return React.createElement('div', { style:{marginBottom:14} }, React.createElement('label', { style:labelSt }, label), React.createElement('select', { ...props, style:{ ...inputSt,cursor:'pointer' } }, options.map(o => React.createElement('option', { key:o.value,value:o.value,style:{background:'#040c18'} }, o.label)) ) ); } function Modal({ title, onClose, children }) { return React.createElement('div', { className:'modal-overlay', style:{position:'fixed',inset:0,zIndex:600,background:'rgba(0,0,0,0.85)',backdropFilter:'blur(12px)',display:'flex',alignItems:'center',justifyContent:'center',padding:20}, onClick:onClose }, React.createElement('div', { className:'modal-sheet', onClick:e=>e.stopPropagation(), style:{background:'#040c18',border:'1px solid rgba(50,114,168,0.3)',borderRadius:0,padding:32,width:'100%',maxWidth:600,maxHeight:'90vh',overflowY:'auto',boxShadow:'0 0 60px rgba(50,114,168,0.2)'} }, React.createElement('div',{className:'modal-handle'}), React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:24} }, React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:20,color:'#fff',textTransform:'uppercase',margin:0} }, title), React.createElement('button', { onClick:onClose,style:{background:'none',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:20,lineHeight:1} },'×') ), children ) ); } function StatCard({ label, value, color }) { return React.createElement('div', { style:{background:'rgba(4,12,28,0.82)',backdropFilter:'blur(10px)',padding:'20px 24px',border:'1px solid rgba(255,255,255,0.06)'} }, React.createElement('div', { style:{...FS,color,fontSize:36,fontWeight:800} }, value), React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:11,letterSpacing:'0.12em',textTransform:'uppercase',marginTop:4} }, label) ); } function TabBar({ tabs, active, onChange }) { return React.createElement('div', { style:{display:'flex',gap:8,marginBottom:32,flexWrap:'wrap',borderBottom:'1px solid rgba(255,255,255,0.08)',paddingBottom:0} }, tabs.map(t => React.createElement('button', { key:t.id, onClick:()=>onChange(t.id), style:{...FS,padding:'10px 20px',border:'none',borderBottom:active===t.id?'2px solid #3272a8':'2px solid transparent',cursor:'pointer',fontWeight:700,fontSize:13,letterSpacing:'0.06em',textTransform:'uppercase',background:'transparent',color:active===t.id?'#3272a8':'rgba(255,255,255,0.45)',transition:'color 0.15s',marginBottom:-1} }, t.label + (t.badge?' ('+t.badge+')':''))) ); } // ── Overview ───────────────────────────────────────────────────────────────── function TabOverview() { const [stats, setStats] = useState(null); useEffect(() => { window.RNTECH_API.getStats().then(setStats).catch(()=>{}); }, []); if (!stats) return React.createElement(Spinner); return React.createElement('div', null, React.createElement('div', { style:{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(160px,1fr))',gap:16,marginBottom:40} }, React.createElement(StatCard, { label:'Events', value:stats.events, color:'#3272a8' }), React.createElement(StatCard, { label:'Tickets Issued', value:stats.tickets, color:'#10b981' }), React.createElement(StatCard, { label:'Members', value:stats.users, color:'#06b6d4' }), React.createElement(StatCard, { label:'Unread Messages', value:stats.messages, color:'#f59e0b' }), React.createElement(StatCard, { label:'Active Mixes', value:stats.mixes, color:'#9f84b2' }), React.createElement(StatCard, { label:'Email Subscribers', value:stats.subscribers, color:'#10b981' }), ), React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.3)',fontSize:13} }, 'Use the tabs above to manage events, mixes, tickets, attendees, and email campaigns.') ); } // ── Events ──────────────────────────────────────────────────────────────────── function TabEvents() { const [events, setEvents] = useState([]); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); const [form, setForm] = useState({ title:'',date:'',displayDate:'',startTime:'',endTime:'',venue:'',location:'',description:'',lineup:'',giveaways:'',status:'upcoming',isFree:true,featured:false,registrationMode:'open' }); const [saving, setSaving] = useState(false); const [posterFile, setPosterFile] = useState(null); const [ticketTypes, setTicketTypes] = useState([]); const [selectedEvent, setSelectedEvent] = useState(null); const [ttForm, setTtForm] = useState({ name:'',price:0,capacity:100,description:'',active:true }); const load = () => window.RNTECH_API.getEvents().then(setEvents).catch(()=>{}); useEffect(()=>{ load(); },[]); const openNew = () => { setEditing(null); setForm({ title:'',date:'',displayDate:'',startTime:'',endTime:'',venue:'The White Swan',location:'St Albans, Hertfordshire',description:'',lineup:'',giveaways:'',status:'upcoming',isFree:true,featured:false,registrationMode:'open' }); setPosterFile(null); setShowForm(true); }; const openEdit = (ev) => { setEditing(ev); setForm({ title:ev.title, date:ev.date, displayDate:ev.displayDate, startTime:ev.startTime, endTime:ev.endTime||'', venue:ev.venue, location:ev.location, description:ev.description||'', lineup:(ev.lineup||[]).map(l=>l.name+(l.role?':'+l.role:'')).join('\n'), giveaways:(ev.giveaways||[]).join('\n'), status:ev.status, isFree:ev.isFree, featured:ev.featured, registrationMode:ev.registrationMode||'open', }); setPosterFile(null); setShowForm(true); }; const save = async (e) => { e.preventDefault(); setSaving(true); const lineup = form.lineup.split('\n').filter(Boolean).map(l=>{ const [name,...rest]=l.split(':'); return { name:name.trim(), role:rest.join(':').trim()||'Support' }; }); const giveaways = form.giveaways.split('\n').filter(Boolean).map(g=>g.trim()); const payload = { ...form, lineup, giveaways }; try { let ev; if (editing) ev = await window.RNTECH_API.updateEvent(editing.id, payload); else ev = await window.RNTECH_API.createEvent(payload); if (posterFile) await window.RNTECH_API.uploadPoster(ev.id, posterFile); setShowForm(false); load(); } catch(err) { alert(err.message); } finally { setSaving(false); } }; const del = async (id) => { if (!confirm('Delete this event? This will also delete all ticket types.')) return; await window.RNTECH_API.deleteEvent(id).catch(err=>alert(err.message)); load(); }; const openTicketTypes = async (ev) => { setSelectedEvent(ev); const data = await window.RNTECH_API.getEvent(ev.id).catch(()=>({ ticketTypes:[] })); setTicketTypes(data.ticketTypes||[]); setTtForm({ name:'',price:0,capacity:100,description:'',active:true }); }; const saveTicketType = async (e) => { e.preventDefault(); await window.RNTECH_API.createTicketType({ ...ttForm, eventId:selectedEvent.id }).catch(err=>alert(err.message)); const data = await window.RNTECH_API.getEvent(selectedEvent.id).catch(()=>({ ticketTypes:[] })); setTicketTypes(data.ticketTypes||[]); setTtForm({ name:'',price:0,capacity:100,description:'',active:true }); }; const delTicketType = async (id) => { if (!confirm('Delete this ticket type?')) return; await window.RNTECH_API.deleteTicketType(id).catch(err=>alert(err.message)); const data = await window.RNTECH_API.getEvent(selectedEvent.id).catch(()=>({ ticketTypes:[] })); setTicketTypes(data.ticketTypes||[]); }; return React.createElement('div', null, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14} }, events.length+' events'), React.createElement(GlowButton, { small:true, onClick:openNew }, '+ New Event') ), React.createElement('div', { style:{display:'flex',flexDirection:'column',gap:12} }, events.map(ev=>React.createElement('div', { key:ev.id, style:{background:'rgba(5,18,44,0.92)',border:'1px solid rgba(50,114,168,0.2)',padding:'18px 20px',display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:12} }, React.createElement('div', null, React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:16,color:'#fff',margin:'0 0 4px',textTransform:'uppercase'} }, ev.title), React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:13} }, ev.displayDate+' · '+ev.venue), React.createElement('div', { style:{marginTop:6,display:'flex',gap:6} }, React.createElement(Chip, { label:ev.status, color:ev.status==='upcoming'?'#3272a8':'#64748b' }), ev.isFree&&React.createElement(Chip, { label:'Free', color:'#10b981' }), ev.featured&&React.createElement(Chip, { label:'Featured', color:'#f59e0b' }) ) ), React.createElement('div', { style:{display:'flex',gap:8,flexWrap:'wrap'} }, React.createElement(GlowButton, { small:true,variant:'ghost',onClick:()=>openTicketTypes(ev) },'Ticket Types'), React.createElement(GlowButton, { small:true,variant:'ghost',onClick:()=>openEdit(ev) },'Edit'), React.createElement(GlowButton, { small:true,variant:'danger',onClick:()=>del(ev.id) },'Delete') ) )) ), showForm&&React.createElement(Modal, { title:editing?'Edit Event':'New Event', onClose:()=>setShowForm(false) }, React.createElement('form', { onSubmit:save }, React.createElement(AdminInput, { label:'Title',value:form.title,onChange:e=>setForm(f=>({...f,title:e.target.value})),required:true }), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12} }, React.createElement(AdminInput, { label:'Date (YYYY-MM-DD)',value:form.date,onChange:e=>setForm(f=>({...f,date:e.target.value})),placeholder:'2025-05-25',required:true }), React.createElement(AdminInput, { label:'Display Date',value:form.displayDate,onChange:e=>setForm(f=>({...f,displayDate:e.target.value})),placeholder:'Sun 25th May 2025',required:true }) ), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12} }, React.createElement(AdminInput, { label:'Start Time',value:form.startTime,onChange:e=>setForm(f=>({...f,startTime:e.target.value})),placeholder:'2:00 PM' }), React.createElement(AdminInput, { label:'End Time',value:form.endTime,onChange:e=>setForm(f=>({...f,endTime:e.target.value})),placeholder:'11:00 PM' }) ), React.createElement(AdminInput, { label:'Venue',value:form.venue,onChange:e=>setForm(f=>({...f,venue:e.target.value})) }), React.createElement(AdminInput, { label:'Location',value:form.location,onChange:e=>setForm(f=>({...f,location:e.target.value})) }), React.createElement(AdminTextarea, { label:'Description',value:form.description,onChange:e=>setForm(f=>({...f,description:e.target.value})),rows:3 }), React.createElement(AdminTextarea, { label:'Lineup (one per line: NAME:Role)',value:form.lineup,onChange:e=>setForm(f=>({...f,lineup:e.target.value})),rows:4,placeholder:'DJ MIDDO:Headliner\nBEN HENDERSON:Headliner\nDJ DAGZ:Support' }), React.createElement(AdminTextarea, { label:'Giveaways (one per line)',value:form.giveaways,onChange:e=>setForm(f=>({...f,giveaways:e.target.value})),rows:3,placeholder:'Free entry all night\nBrewtini sampling' }), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12,marginBottom:14} }, React.createElement(AdminSelect, { label:'Status',value:form.status,onChange:e=>setForm(f=>({...f,status:e.target.value})),options:[{value:'upcoming',label:'Upcoming'},{value:'past',label:'Past'}] }), React.createElement(AdminSelect, { label:'Registration Mode',value:form.registrationMode,onChange:e=>setForm(f=>({...f,registrationMode:e.target.value})),options:[{value:'open',label:'Open — anyone can get tickets'},{value:'interest',label:'Interest Only — waitlist, admin picks'},{value:'closed',label:'Closed — no registration'}] }) ), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12,marginBottom:14} }, React.createElement('div', null, React.createElement('label', { style:labelSt },'Free Entry'), React.createElement('div', { style:{display:'flex',gap:8,paddingTop:4} }, ['Yes','No'].map(v=>React.createElement('label', { key:v,style:{...FS,color:'rgba(255,255,255,0.7)',fontSize:14,cursor:'pointer',display:'flex',alignItems:'center',gap:4} }, React.createElement('input', { type:'radio',name:'isFree',value:v==='Yes',checked:(form.isFree===(v==='Yes')),onChange:()=>setForm(f=>({...f,isFree:v==='Yes'})),style:{accentColor:'#3272a8'} }),v )) ) ), React.createElement('div', null, React.createElement('label', { style:labelSt },'Featured'), React.createElement('div', { style:{display:'flex',gap:8,paddingTop:4} }, ['Yes','No'].map(v=>React.createElement('label', { key:v,style:{...FS,color:'rgba(255,255,255,0.7)',fontSize:14,cursor:'pointer',display:'flex',alignItems:'center',gap:4} }, React.createElement('input', { type:'radio',name:'featured',value:v==='Yes',checked:(form.featured===(v==='Yes')),onChange:()=>setForm(f=>({...f,featured:v==='Yes'})),style:{accentColor:'#3272a8'} }),v )) ) ) ), React.createElement('div', { style:{marginBottom:20} }, React.createElement('label', { style:labelSt },'Poster Image'), React.createElement('input', { type:'file',accept:'image/*',onChange:e=>setPosterFile(e.target.files[0]),style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:13} }) ), React.createElement(GlowButton, { type:'submit',style:{width:'100%'},disabled:saving }, saving?'Saving…':editing?'Update Event':'Create Event') ) ), // Ticket Types modal selectedEvent&&React.createElement(Modal, { title:'Ticket Types — '+selectedEvent.title, onClose:()=>setSelectedEvent(null) }, ticketTypes.length>0&&React.createElement('div', { style:{marginBottom:20} }, ticketTypes.map(tt=>React.createElement('div', { key:tt.id,style:{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'12px 0',borderBottom:'1px solid rgba(255,255,255,0.06)'} }, React.createElement('div', null, React.createElement('div', { style:{...FS,color:'#fff',fontWeight:700,fontSize:14} }, tt.name), React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:12} }, (tt.price===0?'FREE':'£'+(tt.price/100).toFixed(2))+' · '+tt.soldCount+'/'+tt.capacity+' sold') ), React.createElement(GlowButton, { small:true,variant:'danger',onClick:()=>delTicketType(tt.id) },'Remove') )) ), React.createElement('form', { onSubmit:saveTicketType }, React.createElement('h4', { style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:12,letterSpacing:'0.12em',textTransform:'uppercase',marginBottom:16,marginTop:ticketTypes.length>0?24:0} },'Add Ticket Type'), React.createElement(AdminInput, { label:'Name',value:ttForm.name,onChange:e=>setTtForm(f=>({...f,name:e.target.value})),placeholder:'General Entry',required:true }), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12} }, React.createElement(AdminInput, { label:'Price (pence, 0=free)',type:'number',value:ttForm.price,onChange:e=>setTtForm(f=>({...f,price:parseInt(e.target.value)||0})),min:0 }), React.createElement(AdminInput, { label:'Capacity',type:'number',value:ttForm.capacity,onChange:e=>setTtForm(f=>({...f,capacity:parseInt(e.target.value)||100})),min:1 }) ), React.createElement(AdminInput, { label:'Description',value:ttForm.description,onChange:e=>setTtForm(f=>({...f,description:e.target.value})) }), React.createElement(GlowButton, { type:'submit',style:{width:'100%'} },'Add Ticket Type') ) ) ); } // ── Mixes ───────────────────────────────────────────────────────────────────── function TabMixes() { const [mixes, setMixes] = useState([]); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); const [form, setForm] = useState({ title:'',residentName:'',month:'',duration:'',description:'',tracklist:'',active:true,videoUrl:'' }); const [saving, setSaving] = useState(false); const [videoFile, setVideoFile] = useState(null); const [coverFile, setCoverFile] = useState(null); const [uploading, setUploading] = useState(null); const load = () => window.RNTECH_API.adminGetMixes().then(setMixes).catch(()=>{}); useEffect(()=>{ load(); },[]); const openNew = () => { setEditing(null); setForm({ title:'',residentName:'',month:'',duration:'',description:'',tracklist:'',active:true,videoUrl:'' }); setVideoFile(null); setCoverFile(null); setShowForm(true); }; const openEdit = (mix) => { setEditing(mix); setForm({ title:mix.title,residentName:mix.residentName,month:mix.month,duration:mix.duration,description:mix.description||'',tracklist:(mix.tracklist||[]).join('\n'),active:mix.active,videoUrl:mix.videoSrc&&!mix.videoSrc.startsWith('/uploads/')?mix.videoSrc:'' }); setVideoFile(null); setCoverFile(null); setShowForm(true); }; const save = async (e) => { e.preventDefault(); setSaving(true); const payload = { ...form, tracklist:form.tracklist.split('\n').filter(Boolean).map(t=>t.trim()) }; try { let mix; if (editing) mix = await window.RNTECH_API.updateMix(editing.id, payload); else mix = await window.RNTECH_API.createMix(payload); if (videoFile) { setUploading('video'); await window.RNTECH_API.uploadVideo(mix.id, videoFile); } if (coverFile) { setUploading('cover'); await window.RNTECH_API.uploadCover(mix.id, coverFile); } setShowForm(false); setUploading(null); load(); } catch(err) { alert(err.message); } finally { setSaving(false); setUploading(null); } }; const del = async (id) => { if (!confirm('Delete this mix?')) return; await window.RNTECH_API.deleteMix(id).catch(err=>alert(err.message)); load(); }; return React.createElement('div', null, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14} }, mixes.length+' mixes'), React.createElement(GlowButton, { small:true, onClick:openNew },'+ New Mix') ), React.createElement('div', { style:{display:'flex',flexDirection:'column',gap:12} }, mixes.map(mix=>React.createElement('div', { key:mix.id, style:{background:'rgba(5,18,44,0.92)',border:'1px solid rgba(50,114,168,0.2)',padding:'18px 20px',display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:12} }, React.createElement('div', { style:{display:'flex',gap:14,alignItems:'center'} }, mix.coverImage&&React.createElement('img', { src:mix.coverImage,alt:'',style:{width:48,height:48,objectFit:'cover',borderRadius:4,flexShrink:0} }), React.createElement('div', null, React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:15,color:'#fff',margin:'0 0 2px',textTransform:'uppercase'} }, mix.title), React.createElement('div', { style:{...FS,color:'#3272a8',fontSize:12} }, mix.residentName+' · '+mix.month), React.createElement('div', { style:{display:'flex',gap:6,marginTop:6} }, React.createElement(Chip, { label:mix.active?'Active':'Hidden', color:mix.active?'#10b981':'#64748b' }), mix.hasVideo&&React.createElement(Chip, { label:'Has Video', color:'#06b6d4' }) ) ) ), React.createElement('div', { style:{display:'flex',gap:8} }, React.createElement(GlowButton, { small:true,variant:'ghost',onClick:()=>openEdit(mix) },'Edit'), React.createElement(GlowButton, { small:true,variant:'danger',onClick:()=>del(mix.id) },'Delete') ) )) ), showForm&&React.createElement(Modal, { title:editing?'Edit Mix':'New Mix', onClose:()=>setShowForm(false) }, React.createElement('form', { onSubmit:save }, React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12} }, React.createElement(AdminInput, { label:'Title',value:form.title,onChange:e=>setForm(f=>({...f,title:e.target.value})),required:true }), React.createElement(AdminInput, { label:'Resident / DJ Name',value:form.residentName,onChange:e=>setForm(f=>({...f,residentName:e.target.value})),required:true }) ), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:12} }, React.createElement(AdminInput, { label:'Month (e.g. May 2025)',value:form.month,onChange:e=>setForm(f=>({...f,month:e.target.value})) }), React.createElement(AdminInput, { label:'Duration (e.g. 60 MIN)',value:form.duration,onChange:e=>setForm(f=>({...f,duration:e.target.value})) }) ), React.createElement(AdminTextarea, { label:'Description',value:form.description,onChange:e=>setForm(f=>({...f,description:e.target.value})),rows:3 }), React.createElement(AdminTextarea, { label:'Tracklist (one per line)',value:form.tracklist,onChange:e=>setForm(f=>({...f,tracklist:e.target.value})),rows:5,placeholder:'01. Opening Vibes\n02. RnB Selections...' }), React.createElement(AdminInput, { label:'External Video URL (YouTube/Vimeo embed or direct MP4)',value:form.videoUrl,onChange:e=>setForm(f=>({...f,videoUrl:e.target.value})),placeholder:'https://...' }), React.createElement('div', { style:{marginBottom:12} }, React.createElement('label', { style:labelSt },'Upload Video File (MP4) — overrides URL above'), React.createElement('input', { type:'file',accept:'video/*',onChange:e=>setVideoFile(e.target.files[0]),style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:13} }) ), React.createElement('div', { style:{marginBottom:20} }, React.createElement('label', { style:labelSt },'Cover Image'), React.createElement('input', { type:'file',accept:'image/*',onChange:e=>setCoverFile(e.target.files[0]),style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:13} }) ), React.createElement('div', { style:{display:'flex',gap:12,alignItems:'center',marginBottom:20} }, React.createElement('label', { style:{...FS,color:'rgba(255,255,255,0.7)',fontSize:14,cursor:'pointer',display:'flex',alignItems:'center',gap:8} }, React.createElement('input', { type:'checkbox',checked:form.active,onChange:e=>setForm(f=>({...f,active:e.target.checked})),style:{accentColor:'#3272a8',width:16,height:16} }), 'Active (visible on site)' ) ), uploading&&React.createElement('div', { style:{...FS,color:'#3272a8',fontSize:13,marginBottom:12} },'Uploading '+uploading+', please wait…'), React.createElement(GlowButton, { type:'submit',style:{width:'100%'},disabled:saving }, saving?'Saving…':editing?'Update Mix':'Create Mix') ) ) ); } // ── QR Scanner Modal ────────────────────────────────────────────────────────── function QRScanModal({ onClose, onScan }) { const html5QrRef = React.useRef(null); const [error, setError] = React.useState(''); const [scanning, setScanning] = React.useState(false); React.useEffect(() => { if (typeof Html5Qrcode === 'undefined') { setError('QR scanner library not loaded.'); return; } const scanner = new Html5Qrcode('qr-scanner-region'); html5QrRef.current = scanner; scanner.start( { facingMode: 'environment' }, { fps: 10, qrbox: { width: 240, height: 240 } }, (decoded) => { scanner.stop().catch(()=>{}); onScan(decoded); }, () => {} ).then(() => setScanning(true)).catch(err => setError('Camera denied or unavailable: ' + (err || ''))); return () => { scanner.stop().catch(()=>{}); }; }, []); const overlayStyle = { position:'fixed',inset:0,zIndex:600,background:'rgba(0,0,0,0.95)',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:24 }; return React.createElement('div', { style:overlayStyle }, React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:20,color:'#fff',margin:'0 0 20px',textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Scan Ticket QR'), React.createElement('div', { id:'qr-scanner-region', style:{width:280,height:280,background:'#000',border:'2px solid rgba(50,114,168,0.6)',borderRadius:4} }), error && React.createElement('p', { style:{...FS,color:'#ef4444',fontSize:13,marginTop:16,textAlign:'center',maxWidth:300} }, error), !scanning && !error && React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:13,marginTop:16} }, 'Starting camera…'), scanning && !error && React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:13,marginTop:16} }, 'Point at QR code on ticket'), React.createElement('div', { style:{marginTop:20} }, React.createElement(GlowButton, { onClick:()=>{ if(html5QrRef.current) html5QrRef.current.stop().catch(()=>{}); onClose(); }, variant:'ghost' }, 'Cancel') ) ); } // ── Guest List ──────────────────────────────────────────────────────────────── function TabGuestList() { const [events, setEvents] = useState([]); const [selectedId, setSelectedId] = useState(''); const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(false); const [search, setSearch] = useState(''); const [showAdd, setShowAdd] = useState(false); const [showScanner, setShowScanner] = useState(false); const [scanResult, setScanResult] = useState(null); const [guestName, setGuestName] = useState(''); const [guestEmail, setGuestEmail] = useState(''); const [guestTypeId, setGuestTypeId] = useState(''); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState(''); useEffect(() => { window.RNTECH_API.getEvents().then(ev=>{ setEvents(ev); if(ev.length) setSelectedId(nextUpcomingId(ev)); }).catch(()=>{}); },[]); useEffect(() => { if (!selectedId) return; setLoading(true); window.RNTECH_API.adminTickets(selectedId).then(t=>{ setTickets(t); setLoading(false); }).catch(()=>setLoading(false)); },[selectedId]); const reload = () => window.RNTECH_API.adminTickets(selectedId).then(setTickets).catch(()=>{}); const updateStatus = async (ticketId, status) => { await window.RNTECH_API.updateTicketStatus(ticketId, status).catch(err=>alert(err.message)); reload(); }; const addGuest = async () => { if (!guestName.trim() || !guestEmail.trim() || !guestTypeId) { setAddError('Fill all fields'); return; } setAdding(true); setAddError(''); try { await window.RNTECH_API.adminIssueTicket({ holderName:guestName.trim(), holderEmail:guestEmail.trim(), ticketTypeId:guestTypeId }); setGuestName(''); setGuestEmail(''); setGuestTypeId(''); setShowAdd(false); reload(); } catch(err) { setAddError(err.message || 'Failed to add guest'); } finally { setAdding(false); } }; const handleScan = async (qrValue) => { setShowScanner(false); try { const result = await window.RNTECH_API.checkinByQR(qrValue); setScanResult(result); reload(); } catch(err) { setScanResult({ error: err.message || 'Ticket not found' }); } }; const selectedEvent = events.find(e=>e.id===selectedId); const ticketTypes = selectedEvent ? (selectedEvent.ticketTypes||[]).filter(t=>t.active) : []; const filtered = tickets.filter(t => !search || t.holderName.toLowerCase().includes(search.toLowerCase()) || t.holderEmail.toLowerCase().includes(search.toLowerCase())); const btnStyle = { ...FS,background:'rgba(100,116,139,0.2)',border:'1px solid rgba(100,116,139,0.3)',color:'#94a3b8',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em' }; const btnGreenStyle = { ...FS,background:'rgba(16,185,129,0.1)',border:'1px solid rgba(16,185,129,0.3)',color:'#10b981',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em' }; const btnRedStyle = { ...FS,background:'rgba(239,68,68,0.1)',border:'1px solid rgba(239,68,68,0.3)',color:'#ef4444',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em' }; const removeGuest = async (t) => { if (!window.confirm(`Remove ${t.holderName} from the guest list and delete their ticket?`)) return; try { await window.RNTECH_API.deleteTicket(t.id); reload(); } catch(err) { alert(err.message); } }; return React.createElement('div', null, // Top bar React.createElement('div', { style:{display:'flex',gap:10,alignItems:'center',marginBottom:24,flexWrap:'wrap'} }, React.createElement('select', { value:selectedId, onChange:e=>setSelectedId(e.target.value), style:{...inputSt,width:'auto',marginBottom:0,minWidth:200} }, events.map(ev=>React.createElement('option', { key:ev.id,value:ev.id,style:{background:'#040c18'} }, ev.title)) ), React.createElement('input', { placeholder:'Search name or email…',value:search,onChange:e=>setSearch(e.target.value),style:{...inputSt,width:200,marginBottom:0} }), React.createElement(GlowButton, { onClick:()=>{setAddError('');setShowAdd(true);},small:true }, '+ Add Guest'), React.createElement(GlowButton, { onClick:()=>{setScanResult(null);setShowScanner(true);},small:true,variant:'ghost' }, '⬜ Scan QR'), React.createElement('a', { href:window.RNTECH_API.exportTickets(selectedId)+'&token='+window.RNTECH_AUTH.getToken(), download:'guest-list.csv', style:{...FS,color:'#3272a8',fontSize:13,fontWeight:700,letterSpacing:'0.06em',textTransform:'uppercase',textDecoration:'none',border:'1px solid rgba(50,114,168,0.4)',padding:'8px 16px'} }, '⬇ Export CSV'), React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:13} }, tickets.length+' attendees') ), // Table loading && React.createElement(Spinner), !loading && filtered.length===0 && React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.3)'} }, search ? 'No results for "'+search+'".' : 'No registrations yet.'), !loading && filtered.length>0 && React.createElement('div', { style:{background:'rgba(255,255,255,0.02)',border:'1px solid rgba(255,255,255,0.08)',overflow:'hidden'} }, React.createElement('table', { style:{width:'100%',borderCollapse:'collapse'} }, React.createElement('thead', null, React.createElement('tr', { style:{borderBottom:'1px solid rgba(255,255,255,0.08)'} }, ['Name','Email','Ticket Type','Ref','Status','Action'].map(h=> React.createElement('th', { key:h, style:{...FS,padding:'12px 14px',textAlign:'left',color:'rgba(255,255,255,0.4)',fontSize:11,letterSpacing:'0.1em',textTransform:'uppercase',fontWeight:600} }, h) ) ) ), React.createElement('tbody', null, filtered.map((t,i)=>React.createElement('tr', { key:t.id, style:{borderBottom:'1px solid rgba(255,255,255,0.04)',background:i%2?'rgba(255,255,255,0.01)':'transparent'} }, React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.9)'} }, t.holderName), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.5)'} }, t.holderEmail), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.5)'} }, t.ticketTypeName), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12,color:'rgba(255,255,255,0.35)',fontFamily:'monospace'} }, t.id), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:t.status==='valid'?'#10b981':'#64748b'} }, t.status), React.createElement('td', { style:{padding:'10px 14px',display:'flex',gap:6,flexWrap:'wrap'} }, t.status==='valid' ? React.createElement('button', { onClick:()=>updateStatus(t.id,'used'), style:btnStyle }, 'Mark Used') : React.createElement('button', { onClick:()=>updateStatus(t.id,'valid'), style:btnGreenStyle }, 'Mark Valid'), React.createElement('button', { onClick:()=>removeGuest(t), style:btnRedStyle }, 'Remove') ) )) ) ) ), // Add Guest Modal showAdd && React.createElement('div', { className:'modal-overlay', style:{position:'fixed',inset:0,zIndex:500,background:'rgba(0,0,0,0.85)',backdropFilter:'blur(12px)',display:'flex',alignItems:'center',justifyContent:'center',padding:20}, onClick:()=>setShowAdd(false) }, React.createElement('div', { className:'modal-sheet', onClick:e=>e.stopPropagation(), style:{background:'#040c18',border:'1px solid rgba(50,114,168,0.3)',padding:36,width:'100%',maxWidth:420} }, React.createElement('div',{className:'modal-handle'}), React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:20,color:'#fff',margin:'0 0 20px',textTransform:'uppercase'} }, 'Add Guest'), React.createElement('input', { placeholder:'Full Name',value:guestName,onChange:e=>setGuestName(e.target.value),style:inputSt }), React.createElement('input', { type:'email',placeholder:'Email',value:guestEmail,onChange:e=>setGuestEmail(e.target.value),style:inputSt }), React.createElement('select', { value:guestTypeId,onChange:e=>setGuestTypeId(e.target.value),style:inputSt }, React.createElement('option', { value:'' }, 'Select ticket type…'), ticketTypes.map(tt=>React.createElement('option', { key:tt.id,value:tt.id,style:{background:'#040c18'} }, tt.name+(tt.price?' — £'+(tt.price/100).toFixed(2):' — FREE'))) ), addError && React.createElement('p', { style:{...FS,color:'#ef4444',fontSize:13,marginBottom:12} }, addError), React.createElement('div', { style:{display:'flex',gap:12,marginTop:8} }, React.createElement(GlowButton, { onClick:addGuest,disabled:adding }, adding?'Adding…':'Add to Guest List'), React.createElement(GlowButton, { onClick:()=>setShowAdd(false),variant:'ghost' }, 'Cancel') ) ) ), // QR Scanner showScanner && React.createElement(QRScanModal, { onClose:()=>setShowScanner(false), onScan:handleScan }), // Scan Result Modal scanResult && React.createElement('div', { className:'modal-overlay', style:{position:'fixed',inset:0,zIndex:500,background:'rgba(0,0,0,0.85)',backdropFilter:'blur(12px)',display:'flex',alignItems:'center',justifyContent:'center',padding:20}, onClick:()=>setScanResult(null) }, React.createElement('div', { className:'modal-sheet', onClick:e=>e.stopPropagation(), style:{background:'#040c18',border:'1px solid '+(scanResult.error?'rgba(239,68,68,0.4)':scanResult.alreadyUsed?'rgba(100,116,139,0.4)':'rgba(16,185,129,0.4)'),padding:36,width:'100%',maxWidth:380,textAlign:'center'} }, React.createElement('div',{className:'modal-handle'}), React.createElement('div', { style:{fontSize:52,marginBottom:16} }, scanResult.error?'✗':scanResult.alreadyUsed?'⚠':'✓'), React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:22,color:'#fff',margin:'0 0 12px',textTransform:'uppercase'} }, scanResult.error ? 'Not Found' : scanResult.alreadyUsed ? 'Already Used' : 'Checked In!' ), !scanResult.error && React.createElement('div', null, React.createElement('p', { style:{...FS,fontSize:18,color:'#fff',margin:'0 0 4px',fontWeight:600} }, scanResult.holderName), React.createElement('p', { style:{...FS,fontSize:13,color:'rgba(255,255,255,0.5)',margin:'0 0 4px'} }, scanResult.ticketTypeName), React.createElement('p', { style:{...FS,fontSize:12,color:'rgba(255,255,255,0.3)',fontFamily:'monospace'} }, scanResult.id) ), scanResult.error && React.createElement('p', { style:{...FS,color:'#ef4444',fontSize:14,marginBottom:16} }, scanResult.error), React.createElement('div', { style:{marginTop:24} }, React.createElement(GlowButton, { onClick:()=>setScanResult(null) }, 'Close') ) ) ) ); } // ── Attendees ───────────────────────────────────────────────────────────────── function TabAttendees() { const [attendees, setAttendees] = useState([]); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(null); const [form, setForm] = useState({ firstName:'', lastName:'', email:'', instagram:'', role:'user' }); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(''); const [search, setSearch] = useState(''); const reload = () => window.RNTECH_API.getAttendees().then(a=>{ setAttendees(a); setLoading(false); }).catch(()=>setLoading(false)); useEffect(() => { reload(); }, []); const openEdit = (a) => { setEditing(a); setForm({ firstName: a.firstName, lastName: a.lastName, email: a.email, instagram: a.instagram, role: a.role }); setSaveError(''); }; const saveEdit = async () => { setSaving(true); setSaveError(''); try { await window.RNTECH_API.updateUser(editing.id, form); setEditing(null); reload(); } catch(err) { setSaveError(err.message || 'Failed to save'); } finally { setSaving(false); } }; const deleteMember = async (a) => { if (!window.confirm(`Delete ${a.name}'s account? This cannot be undone.`)) return; try { await window.RNTECH_API.deleteUser(a.id); reload(); } catch(err) { alert(err.message); } }; const unsubscribe = (a) => window.RNTECH_API.unsubscribeUser(a.id) .then(() => setAttendees(prev => prev.map(x => x.id===a.id ? {...x, subscribed:false} : x))); if (loading) return React.createElement(Spinner); const filtered = attendees.filter(a => !search || a.name.toLowerCase().includes(search.toLowerCase()) || a.email.toLowerCase().includes(search.toLowerCase())); return React.createElement('div', null, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20,flexWrap:'wrap',gap:10} }, React.createElement('div', { style:{display:'flex',gap:10,alignItems:'center'} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14} }, attendees.length+' members'), React.createElement('input', { placeholder:'Search name or email…', value:search, onChange:e=>setSearch(e.target.value), style:{...inputSt,width:200,marginBottom:0} }) ), React.createElement('a', { href:window.RNTECH_API.exportAttendees()+'&token='+window.RNTECH_AUTH.getToken(), download:'attendees.csv', style:{...FS,color:'#3272a8',fontSize:13,fontWeight:700,letterSpacing:'0.06em',textTransform:'uppercase',textDecoration:'none',border:'1px solid rgba(50,114,168,0.4)',padding:'8px 16px'} }, '⬇ Export CSV') ), attendees.length===0&&React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.3)'} },'No members yet.'), filtered.length>0&&React.createElement('div', { style:{background:'rgba(255,255,255,0.02)',border:'1px solid rgba(255,255,255,0.08)',overflow:'hidden'} }, React.createElement('table', { style:{width:'100%',borderCollapse:'collapse'} }, React.createElement('thead', null, React.createElement('tr', { style:{borderBottom:'1px solid rgba(255,255,255,0.08)'} }, ['Name','Email','Instagram','Tickets','Joined','Subscribed','Actions'].map(h=> React.createElement('th', { key:h, style:{...FS,padding:'12px 14px',textAlign:'left',color:'rgba(255,255,255,0.4)',fontSize:11,letterSpacing:'0.1em',textTransform:'uppercase',fontWeight:600} }, h) ) ) ), React.createElement('tbody', null, filtered.map((a,i)=>React.createElement('tr', { key:a.id, style:{borderBottom:'1px solid rgba(255,255,255,0.04)',background:i%2?'rgba(255,255,255,0.01)':'transparent'} }, React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.9)'} }, a.name), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.5)'} }, a.email), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12} }, a.instagram ? React.createElement('a', { href:'https://www.instagram.com/'+a.instagram, target:'_blank', rel:'noopener noreferrer', style:{color:'#6ba3d4',textDecoration:'none'} }, a.instagram) : React.createElement('span', { style:{color:'rgba(255,255,255,0.2)'} }, '—') ), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.5)',textAlign:'center'} }, a.ticketCount), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12,color:'rgba(255,255,255,0.4)'} }, a.joinedAt.slice(0,10)), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:a.subscribed?'#10b981':'#64748b'} }, a.subscribed?'Yes':'No'), React.createElement('td', { style:{padding:'10px 14px',display:'flex',gap:6,flexWrap:'wrap'} }, React.createElement('button', { onClick:()=>openEdit(a), style:{...FS,background:'rgba(50,114,168,0.15)',border:'1px solid rgba(50,114,168,0.35)',color:'#6ba3d4',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Edit'), a.subscribed&&React.createElement('button', { onClick:()=>unsubscribe(a), style:{...FS,background:'rgba(100,116,139,0.15)',border:'1px solid rgba(100,116,139,0.3)',color:'#94a3b8',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Unsub'), React.createElement('button', { onClick:()=>deleteMember(a), style:{...FS,background:'rgba(239,68,68,0.1)',border:'1px solid rgba(239,68,68,0.3)',color:'#ef4444',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Delete') ) )) ) ) ), // Edit modal editing&&React.createElement(Modal, { title:'Edit Member', onClose:()=>setEditing(null) }, React.createElement('div', { style:{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:10} }, React.createElement('div', null, React.createElement('label', { style:labelSt }, 'First Name'), React.createElement('input', { value:form.firstName, onChange:e=>setForm(f=>({...f,firstName:e.target.value})), style:inputSt }) ), React.createElement('div', null, React.createElement('label', { style:labelSt }, 'Last Name'), React.createElement('input', { value:form.lastName, onChange:e=>setForm(f=>({...f,lastName:e.target.value})), style:inputSt }) ) ), React.createElement('label', { style:labelSt }, 'Email'), React.createElement('input', { type:'email', value:form.email, onChange:e=>setForm(f=>({...f,email:e.target.value})), style:inputSt }), React.createElement('label', { style:labelSt }, 'Instagram handle (no @)'), React.createElement('input', { value:form.instagram, onChange:e=>setForm(f=>({...f,instagram:e.target.value})), style:inputSt }), React.createElement('label', { style:labelSt }, 'Role'), React.createElement('select', { value:form.role, onChange:e=>setForm(f=>({...f,role:e.target.value})), style:{...inputSt,cursor:'pointer'} }, React.createElement('option', { value:'user', style:{background:'#040c18'} }, 'User'), React.createElement('option', { value:'admin', style:{background:'#040c18'} }, 'Admin') ), saveError&&React.createElement('p', { style:{...FS,color:'#ef4444',fontSize:13,marginBottom:12} }, saveError), React.createElement('div', { style:{display:'flex',gap:12,marginTop:8} }, React.createElement(GlowButton, { onClick:saveEdit, disabled:saving }, saving?'Saving…':'Save Changes'), React.createElement(GlowButton, { onClick:()=>setEditing(null), variant:'ghost' }, 'Cancel') ) ) ); } // ── Messages ────────────────────────────────────────────────────────────────── function TabMessages() { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const load = () => window.RNTECH_API.getMessages().then(m=>{ setMessages(m); setLoading(false); }).catch(()=>setLoading(false)); useEffect(()=>{ load(); },[]); if (loading) return React.createElement(Spinner); return React.createElement('div', null, React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14,marginBottom:20} }, messages.filter(m=>!m.read).length+' unread'), messages.length===0&&React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.3)'} },'No messages yet.'), React.createElement('div', { style:{display:'flex',flexDirection:'column',gap:16} }, messages.map(msg=>React.createElement('div', { key:msg.id, style:{background:msg.read?'rgba(5,18,44,0.5)':'rgba(5,18,44,0.92)',border:`1px solid ${msg.read?'rgba(50,114,168,0.1)':'rgba(50,114,168,0.25)'}`,backdropFilter:'blur(14px)',padding:'18px 20px'} }, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',marginBottom:8,flexWrap:'wrap',gap:8} }, React.createElement('div', null, React.createElement('span', { style:{...FS,color:'#fff',fontWeight:700,fontSize:14} }, msg.name), React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:12,marginLeft:10} }, msg.email) ), React.createElement('div', { style:{display:'flex',gap:8,alignItems:'center'} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.3)',fontSize:12} }, msg.sentAt.slice(0,10)), !msg.read&&React.createElement('button', { onClick:()=>window.RNTECH_API.markRead(msg.id).then(load), style:{...FS,background:'rgba(50,114,168,0.1)',border:'1px solid rgba(50,114,168,0.3)',color:'#3272a8',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} },'Mark Read'), React.createElement('button', { onClick:()=>window.RNTECH_API.deleteMessage(msg.id).then(load), style:{...FS,background:'rgba(239,68,68,0.1)',border:'1px solid rgba(239,68,68,0.3)',color:'#ef4444',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} },'Delete') ) ), React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.7)',fontSize:14,margin:0,lineHeight:1.6} }, msg.message) )) ) ); } // ── Email Campaigns ─────────────────────────────────────────────────────────── function TabCampaigns() { const [campaigns, setCampaigns] = useState([]); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); const [editing, setEditing] = useState(null); const [form, setForm] = useState({ name:'',subject:'',bodyHtml:'',delayDays:0,active:true }); const [saving, setSaving] = useState(false); const [blasting, setBlasting] = useState(null); const load = () => window.RNTECH_API.getCampaigns().then(c=>{ setCampaigns(c); setLoading(false); }).catch(()=>setLoading(false)); useEffect(()=>{ load(); },[]); const openNew = () => { setEditing(null); setForm({ name:'',subject:'',bodyHtml:'',delayDays:0,active:true }); setShowForm(true); }; const openEdit = (c) => { setEditing(c); setForm({ name:c.name,subject:c.subject,bodyHtml:c.bodyHtml,delayDays:c.delayDays,active:c.active }); setShowForm(true); }; const save = async (e) => { e.preventDefault(); setSaving(true); try { if (editing) await window.RNTECH_API.updateCampaign(editing.id, form); else await window.RNTECH_API.createCampaign(form); setShowForm(false); load(); } catch(err) { alert(err.message); } finally { setSaving(false); } }; const blast = async (id) => { if (!confirm('Send this campaign to ALL active subscribers now?')) return; setBlasting(id); try { const res = await window.RNTECH_API.blastCampaign(id); alert('Sent to '+res.sent+' subscribers.'); load(); } catch(err) { alert(err.message); } finally { setBlasting(null); } }; if (loading) return React.createElement(Spinner); return React.createElement('div', null, React.createElement('div', { style:{background:'rgba(50,114,168,0.08)',border:'1px solid rgba(50,114,168,0.2)',padding:'16px 20px',marginBottom:24} }, React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:13,margin:0,lineHeight:1.7} }, React.createElement('strong', { style:{color:'#3272a8'} },'Drip Campaigns:'),' Create email sequences sent automatically to new members based on delay days after signup. "Send Now" blasts to all existing subscribers immediately.' ) ), React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14} }, campaigns.length+' campaigns'), React.createElement(GlowButton, { small:true,onClick:openNew },'+ New Campaign') ), React.createElement('div', { style:{display:'flex',flexDirection:'column',gap:12} }, campaigns.map(c=>React.createElement('div', { key:c.id, style:{background:'rgba(5,18,44,0.92)',border:'1px solid rgba(50,114,168,0.2)',padding:'18px 20px'} }, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'flex-start',flexWrap:'wrap',gap:12} }, React.createElement('div', null, React.createElement('h3', { style:{...FS,fontWeight:800,fontSize:15,color:'#fff',margin:'0 0 4px'} }, c.name), React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:13} }, 'Subject: '+c.subject), React.createElement('div', { style:{display:'flex',gap:8,marginTop:8} }, React.createElement(Chip, { label:c.active?'Active':'Paused', color:c.active?'#10b981':'#64748b' }), React.createElement(Chip, { label:'Day '+c.delayDays, color:'#3272a8' }), React.createElement(Chip, { label:c.sentCount+' sent', color:'#9f84b2' }), React.createElement(Chip, { label:c.pendingCount+' pending', color:'#f59e0b' }) ) ), React.createElement('div', { style:{display:'flex',gap:8,flexWrap:'wrap'} }, React.createElement(GlowButton, { small:true,variant:'pink',onClick:()=>blast(c.id),disabled:blasting===c.id }, blasting===c.id?'Sending…':'Send Now'), React.createElement(GlowButton, { small:true,variant:'ghost',onClick:()=>openEdit(c) },'Edit'), React.createElement(GlowButton, { small:true,variant:'danger',onClick:()=>window.RNTECH_API.deleteCampaign(c.id).then(load) },'Delete') ) ) )) ), showForm&&React.createElement(Modal, { title:editing?'Edit Campaign':'New Campaign', onClose:()=>setShowForm(false) }, React.createElement('form', { onSubmit:save }, React.createElement(AdminInput, { label:'Campaign Name (internal)',value:form.name,onChange:e=>setForm(f=>({...f,name:e.target.value})),required:true }), React.createElement(AdminInput, { label:'Email Subject',value:form.subject,onChange:e=>setForm(f=>({...f,subject:e.target.value})),required:true }), React.createElement(AdminInput, { label:'Send After N Days (0 = immediately on signup)',type:'number',min:0,value:form.delayDays,onChange:e=>setForm(f=>({...f,delayDays:parseInt(e.target.value)||0})) }), React.createElement(AdminTextarea, { label:'Email Body (HTML or plain text)',value:form.bodyHtml,onChange:e=>setForm(f=>({...f,bodyHtml:e.target.value})),rows:10,placeholder:'
Thanks for joining...
' }), React.createElement('div', { style:{display:'flex',gap:12,alignItems:'center',marginBottom:20} }, React.createElement('label', { style:{...FS,color:'rgba(255,255,255,0.7)',fontSize:14,cursor:'pointer',display:'flex',alignItems:'center',gap:8} }, React.createElement('input', { type:'checkbox',checked:form.active,onChange:e=>setForm(f=>({...f,active:e.target.checked})),style:{accentColor:'#3272a8',width:16,height:16} }), 'Active (send to new signups)' ) ), React.createElement(GlowButton, { type:'submit',style:{width:'100%'},disabled:saving }, saving?'Saving…':editing?'Update Campaign':'Create Campaign') ) ) ); } // ── Sponsors ────────────────────────────────────────────────────────────────── function TabSponsors() { const [sponsors, setSponsors] = useState([]); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); const [form, setForm] = useState({ name:'',subtitle:'',description:'',link:'#',active:true,sortOrder:0 }); const [logoFile, setLogoFile] = useState(null); const [saving, setSaving] = useState(false); const load = () => window.RNTECH_API.getSponsors().then(setSponsors).catch(()=>{}); useEffect(()=>{ load(); },[]); const openNew = () => { setEditing(null); setForm({ name:'',subtitle:'',description:'',link:'#',active:true,sortOrder:0 }); setLogoFile(null); setShowForm(true); }; const openEdit = (s) => { setEditing(s); setForm({ name:s.name,subtitle:s.subtitle,description:s.description,link:s.link,active:s.active,sortOrder:s.sortOrder }); setLogoFile(null); setShowForm(true); }; const save = async (e) => { e.preventDefault(); setSaving(true); try { let sp; if (editing) sp = await window.RNTECH_API.updateSponsor(editing.id, form); else sp = await window.RNTECH_API.createSponsor(form); if (logoFile) await window.RNTECH_API.uploadLogo(sp.id, logoFile); setShowForm(false); load(); } catch(err) { alert(err.message); } finally { setSaving(false); } }; return React.createElement('div', null, React.createElement('div', { style:{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:20} }, React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.5)',fontSize:14} }, sponsors.length+' sponsors'), React.createElement(GlowButton, { small:true,onClick:openNew },'+ Add Sponsor') ), React.createElement('div', { style:{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(240px,1fr))',gap:16} }, sponsors.map(s=>React.createElement(FuturisticCard, { key:s.id }, s.logo&&React.createElement('img', { src:s.logo,alt:s.name,style:{height:56,width:56,objectFit:'contain',marginBottom:10,display:'block'} }), React.createElement('div', { style:{...FS,fontWeight:800,fontSize:14,color:'#fff',textTransform:'uppercase',marginBottom:4} }, s.name), React.createElement('div', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:12,marginBottom:12} }, s.subtitle), React.createElement('div', { style:{display:'flex',gap:8} }, React.createElement(GlowButton, { small:true,variant:'ghost',onClick:()=>openEdit(s) },'Edit'), React.createElement(GlowButton, { small:true,variant:'danger',onClick:()=>window.RNTECH_API.deleteSponsor(s.id).then(load) },'Remove') ) )) ), showForm&&React.createElement(Modal, { title:editing?'Edit Sponsor':'Add Sponsor', onClose:()=>setShowForm(false) }, React.createElement('form', { onSubmit:save }, React.createElement(AdminInput, { label:'Name',value:form.name,onChange:e=>setForm(f=>({...f,name:e.target.value})),required:true }), React.createElement(AdminInput, { label:'Subtitle',value:form.subtitle,onChange:e=>setForm(f=>({...f,subtitle:e.target.value})),placeholder:'St Albans' }), React.createElement(AdminTextarea, { label:'Description',value:form.description,onChange:e=>setForm(f=>({...f,description:e.target.value})),rows:2 }), React.createElement(AdminInput, { label:'Link URL',value:form.link,onChange:e=>setForm(f=>({...f,link:e.target.value})) }), React.createElement(AdminInput, { label:'Sort Order',type:'number',value:form.sortOrder,onChange:e=>setForm(f=>({...f,sortOrder:parseInt(e.target.value)||0})) }), React.createElement('div', { style:{marginBottom:20} }, React.createElement('label', { style:labelSt },'Logo Image'), React.createElement('input', { type:'file',accept:'image/*',onChange:e=>setLogoFile(e.target.files[0]),style:{...FS,color:'rgba(255,255,255,0.6)',fontSize:13} }) ), React.createElement(GlowButton, { type:'submit',style:{width:'100%'},disabled:saving }, saving?'Saving…':editing?'Update Sponsor':'Add Sponsor') ) ) ); } // ── Waitlist ────────────────────────────────────────────────────────────────── function TabWaitlist() { const [events, setEvents] = useState([]); const [selectedId, setSelectedId] = useState(''); const [interests, setInterests] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { window.RNTECH_API.getEvents().then(ev=>{ setEvents(ev); if(ev.length) setSelectedId(nextUpcomingId(ev)); }).catch(()=>{}); },[]); useEffect(() => { if (!selectedId) return; setLoading(true); window.RNTECH_API.adminGetInterest(selectedId).then(d=>{ setInterests(d); setLoading(false); }).catch(()=>setLoading(false)); },[selectedId]); const reload = () => window.RNTECH_API.adminGetInterest(selectedId).then(setInterests).catch(()=>{}); const approve = async (id) => { try { await window.RNTECH_API.approveInterest(id); reload(); } catch(err) { alert(err.message); } }; const reject = async (id) => { try { await window.RNTECH_API.rejectInterest(id); reload(); } catch(err) { alert(err.message); } }; const removeInterest = async (item) => { if (!window.confirm(`Remove ${item.firstName || item.name} from the waitlist entirely?`)) return; try { await window.RNTECH_API.deleteInterest(item.id); reload(); } catch(err) { alert(err.message); } }; const pending = interests.filter(i=>i.status==='pending').length; const approved = interests.filter(i=>i.status==='approved').length; const rejected = interests.filter(i=>i.status==='rejected').length; const statusColor = { pending:'#f59e0b', approved:'#10b981', rejected:'#ef4444' }; return React.createElement('div', null, React.createElement('div', { style:{display:'flex',gap:10,alignItems:'center',marginBottom:24,flexWrap:'wrap'} }, React.createElement('select', { value:selectedId,onChange:e=>setSelectedId(e.target.value),style:{...inputSt,width:'auto',marginBottom:0,minWidth:200} }, events.map(ev=>React.createElement('option', { key:ev.id,value:ev.id,style:{background:'#040c18'} }, ev.title)) ), React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.4)',fontSize:13} }, pending+' pending · '+approved+' approved · '+rejected+' rejected') ), React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.35)',fontSize:13,marginBottom:20,lineHeight:1.6} }, 'People register interest here before you open the guest list. Approve to automatically issue them a ticket. Set an event\'s Registration Mode to "Interest Only" to enable this flow.' ), loading && React.createElement(Spinner), !loading && interests.length===0 && React.createElement('p', { style:{...FS,color:'rgba(255,255,255,0.3)'} }, 'No interest registrations yet for this event.'), !loading && interests.length>0 && React.createElement('div', { style:{background:'rgba(255,255,255,0.02)',border:'1px solid rgba(255,255,255,0.08)',overflow:'hidden'} }, React.createElement('table', { style:{width:'100%',borderCollapse:'collapse'} }, React.createElement('thead', null, React.createElement('tr', { style:{borderBottom:'1px solid rgba(255,255,255,0.08)'} }, ['First Name','Last Name','Instagram','Submitted','Status','Action'].map(h=> React.createElement('th', { key:h, style:{...FS,padding:'12px 14px',textAlign:'left',color:'rgba(255,255,255,0.4)',fontSize:11,letterSpacing:'0.1em',textTransform:'uppercase',fontWeight:600} }, h) ) ) ), React.createElement('tbody', null, interests.map((item,i)=>React.createElement('tr', { key:item.id, style:{borderBottom:'1px solid rgba(255,255,255,0.04)',background:i%2?'rgba(255,255,255,0.01)':'transparent'} }, React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.9)'} }, item.firstName||item.name), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:13,color:'rgba(255,255,255,0.9)'} }, item.lastName||''), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12} }, item.instagram ? React.createElement('a', { href:'https://www.instagram.com/'+item.instagram, target:'_blank', rel:'noopener noreferrer', style:{color:'#6ba3d4',textDecoration:'none'} }, item.instagram) : React.createElement('span', { style:{color:'rgba(255,255,255,0.3)'} }, '—') ), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12,color:'rgba(255,255,255,0.3)'} }, item.createdAt.slice(0,10)), React.createElement('td', { style:{...FS,padding:'10px 14px',fontSize:12,color:statusColor[item.status]||'#fff',fontWeight:700,textTransform:'uppercase'} }, item.status), React.createElement('td', { style:{padding:'10px 14px',display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'} }, item.status==='pending' && React.createElement('button', { onClick:()=>approve(item.id), style:{...FS,background:'rgba(16,185,129,0.15)',border:'1px solid rgba(16,185,129,0.3)',color:'#10b981',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Approve'), item.status==='pending' && React.createElement('button', { onClick:()=>reject(item.id), style:{...FS,background:'rgba(239,68,68,0.1)',border:'1px solid rgba(239,68,68,0.3)',color:'#ef4444',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Reject'), item.status==='approved' && React.createElement('span', { style:{...FS,color:'#10b981',fontSize:12} }, '✓ Ticket issued'), item.status==='rejected' && React.createElement('span', { style:{...FS,color:'rgba(255,255,255,0.3)',fontSize:12} }, 'Rejected'), React.createElement('button', { onClick:()=>removeInterest(item), style:{...FS,background:'rgba(239,68,68,0.08)',border:'1px solid rgba(239,68,68,0.25)',color:'#ef4444',cursor:'pointer',padding:'4px 10px',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.06em'} }, 'Remove') ) )) ) ) ) ); } // ── Main Admin Page ─────────────────────────────────────────────────────────── function PageAdmin({ user }) { const [tab, setTab] = useState('overview'); if (!user || user.role !== 'admin') return React.createElement('div', { style:{padding:'100px 24px',textAlign:'center',...FS,color:'rgba(255,255,255,0.5)'} }, 'Admin access required.'); const renderTab = () => { switch(tab) { case 'overview': return React.createElement(TabOverview); case 'events': return React.createElement(TabEvents); case 'mixes': return React.createElement(TabMixes); case 'guestlist': return React.createElement(TabGuestList); case 'waitlist': return React.createElement(TabWaitlist); case 'attendees': return React.createElement(TabAttendees); case 'messages': return React.createElement(TabMessages); case 'campaigns': return React.createElement(TabCampaigns); case 'sponsors': return React.createElement(TabSponsors); default: return null; } }; return React.createElement('div', { style:{padding:'100px 24px 100px',maxWidth:1100,margin:'0 auto'} }, React.createElement(SectionHeader, { title:'Admin', subtitle:'RNTECH Management Panel' }), React.createElement(TabBar, { tabs:ADMIN_TABS, active:tab, onChange:setTab }), renderTab() ); } window.PageAdmin = PageAdmin;