/* Transport — live bus tracking */ function MapCanvas({ live }) { const STOPS = [ { id:1, x:40, y:52, name:'Cypress Trace', time:'3:12 PM' }, { id:2, x:63, y:33, name:'Morton Ranch Rd', time:'3:20 PM' }, { id:3, x:10, y:86, name:'Your stop · Home', time:'3:25 PM' }, ]; const seg = live ? Math.max(0, Math.min(2, live.progress * 2)) : 1; const doneCount = Math.floor(seg); const nextStopId = live ? Math.min(3, doneCount + 1) : 2; const [sel, setSel] = React.useState(nextStopId); React.useEffect(() => { setSel(nextStopId); }, [nextStopId]); // live bus position interpolated along the stop chain by progress const i = Math.min(1, Math.floor(seg)), frac = seg - i; const busX = live ? STOPS[i].x + (STOPS[i+1].x - STOPS[i].x) * frac : null; const busY = live ? STOPS[i].y + (STOPS[i+1].y - STOPS[i].y) * frac : null; const cur = STOPS.find(s => s.id === sel) || STOPS[1]; const isDone = (s) => live ? (s.id - 1) < doneCount : s.id === 1; const isNext = (s) => s.id === nextStopId; return (
{/* route line */} {/* bus — live position by progress, else the ambient CSS travel animation */} {live ? (
) : (
)} {/* tappable stops */} {STOPS.map(s => { const on = s.id === sel, done = isDone(s), nxt = isNext(s); return ( ); })} {/* stop detail chip */}
{cur.name}
{isDone(cur) ? 'Arrived' : 'ETA'} · {cur.time}
{isNext(cur) && NEXT STOP}
); } function TransportScreen({ onNav, onBack }) { const [notify, setNotify] = React.useState(false); const [reportOpen, setReportOpen] = React.useState(false); const [reported, setReported] = React.useState(false); const [reason, setReason] = React.useState('Stayed after class'); const [live, setLive] = React.useState(null); React.useEffect(() => { const read = () => { try { setLive(JSON.parse(localStorage.getItem('jl_buspos') || 'null')); } catch(e){} }; read(); const onStore = (e) => { if (e.key === 'jl_buspos') read(); }; window.addEventListener('storage', onStore); const iv = setInterval(read, 1500); return () => { window.removeEventListener('storage', onStore); clearInterval(iv); }; }, []); const REASONS = ['Stayed after class', 'Running late', 'Felt sick', 'Other']; const scans = [ { n:jlChild().name, t:'3:05 PM', c:jlChild().color, done:true }, { n:'James Walker', t:'3:07 PM', c:'blue', done:true }, { n:'Sophia Perez', t:'3:08 PM', c:'gold', done:true }, ]; return (
{/* missed-bus confirmation banner */} {reported && (
School & family notified
{jlChild().first} missed Bus #12 · {reason}
)} {/* driver */}
Driver
Mark Thompson
4.9
{/* live source attribution */}
{live ? "Live from driver's phone GPS · updated just now" : 'Bus not on route yet · showing last plan'}
{/* next stop / ETA */}
NEXT STOP
{live ? live.nextStop : 'Morton Ranch Rd'}
ETA
{live ? live.eta : 5} min
3:20 PM
{/* notify toggle */} {/* missed the bus */} {/* scanned students */} On Board
{scans.map((s,i)=>( {i>0 && }
w[0]).join('')} color={s.c} size={38} />
{s.n}
Scanned on · {s.t}
))}
{/* bottom sheet: report missed bus */} {reportOpen && (
setReportOpen(false)} style={{ position:'absolute', inset:0, zIndex:60, background:'rgba(10,20,40,.4)', display:'flex', alignItems:'flex-end' }}>
e.stopPropagation()} style={{ width:'100%', background:'#fff', borderRadius:'22px 22px 0 0', padding:'10px 18px 34px' }}>
Report Missed Bus
{jlChild().name} · Bus #12
We'll instantly notify the school office and your family so someone can arrange a safe pickup.
What happened?
{REASONS.map(r => { const on = r===reason; return ( ); })}
Notifies Katy Middle office & Ashley Hatten
)}
); } Object.assign(window, { TransportScreen, MapCanvas });