| 1 | "use client"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import { api, SatelliteCam } from "@/lib/api"; |
| 4 | |
| 5 | export default function SatellitesPage() { |
| 6 | const [sats, setSats] = useState<SatelliteCam[]>([]); |
| 7 | const [err, setErr] = useState<string | null>(null); |
| 8 | const [tick, setTick] = useState(0); |
| 9 | |
| 10 | function load() { api.satellites().then((r) => setSats(r.satellites)).catch((e) => |
| 11 | setErr(e.status === 401 ? "Sign in to view satellite cameras." : "Load failed.")); } |
| 12 | useEffect(() => { load(); const t = setInterval(() => { load(); setTick((x) => x + 1); }, 15000); return () => clearInterval(t); }, []); |
| 13 | |
| 14 | const byConst: Record<string, number> = {}; |
| 15 | sats.forEach((s) => { const k = s.constellation || "other"; byConst[k] = (byConst[k] || 0) + 1; }); |
| 16 | |
| 17 | return ( |
| 18 | <div> |
| 19 | <h1>Satellite Cameras <span className="badge ok"><span className="live-dot" /> tracking</span></h1> |
| 20 | <p className="sub">Real imaging / Earth-observation satellites (Planet SkySat & Dove, Earth-resources). |
| 21 | Positions propagated live via SGP4 - refreshes every 15s.</p> |
| 22 | |
| 23 | <div className="row" style={{ margin: "10px 0" }}> |
| 24 | {Object.entries(byConst).map(([k, v]) => <span key={k} className="badge amber">{k}: {v}</span>)} |
| 25 | <span className="muted">{sats.length} satellites tracked · update #{tick}</span> |
| 26 | </div> |
| 27 | {err && <div className="badge danger">{err}</div>} |
| 28 | |
| 29 | <div className="card scroll" style={{ maxHeight: "70vh" }}> |
| 30 | <table> |
| 31 | <thead><tr><th>Satellite</th><th>Constellation</th><th>Lat</th><th>Lon</th><th>Alt (km)</th><th>Period</th></tr></thead> |
| 32 | <tbody> |
| 33 | {sats.map((s) => ( |
| 34 | <tr key={s.identifier}> |
| 35 | <td>🛰 {s.label}</td> |
| 36 | <td><span className="badge amber">{s.constellation}</span></td> |
| 37 | <td className="mono">{s.lat?.toFixed(2)}</td> |
| 38 | <td className="mono">{s.lon?.toFixed(2)}</td> |
| 39 | <td className="mono">{s.altitude_km}</td> |
| 40 | <td className="muted">{s.period_min ? Math.round(s.period_min) + " min" : "-"}</td> |
| 41 | </tr> |
| 42 | ))} |
| 43 | {!sats.length && !err && <tr><td colSpan={6} className="muted">No satellites loaded yet.</td></tr>} |
| 44 | </tbody> |
| 45 | </table> |
| 46 | </div> |
| 47 | </div> |
| 48 | ); |
| 49 | } |