/* 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 */}
{/* 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} />
))}
{/* 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 });