diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ea2378d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,52 @@ + + + + + Asteroid Orbit Tracker + + + + +
+

Asteroid Orbit Tracker 🚀

+
+ +
+
+

Live Orbit Visualization

+ +

+ Blue: Low-risk NEO | Orange: Medium-risk NEO +

+
+ +
+

Nearby Asteroids

+
+ +
+ +
+

Risk Metrics

+
+
+
0
+
Total NEOs
+
+
+
0 LD
+
Avg Distance
+
+
+
0 km/s
+
Avg Velocity
+
+
+
+
+ + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..be84799 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "asteroid-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "chart.js": "^4.4.4", + "react-chartjs-2": "^5.2.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.6.1", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.7", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "vite": "^5.4.1" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..77752ab --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,22 @@ +import OrbitCanvas from './components/OrbitCanvas.jsx'; +import AsteroidPanel from './components/AsteroidPanel.jsx'; +import Dashboard from './components/Dashboard.jsx'; + +function App() { + return ( +
+
+

+ Asteroid Orbit Tracker 🚀 +

+
+
+ + + +
+
+ ); +} + +export default App; diff --git a/frontend/src/components/AsteroidPanel.js b/frontend/src/components/AsteroidPanel.js new file mode 100644 index 0000000..6330bfa --- /dev/null +++ b/frontend/src/components/AsteroidPanel.js @@ -0,0 +1,44 @@ +import { getAsteroids } from '../services/api.js'; + +export async function initAsteroidPanel() { + const listEl = document.getElementById('asteroidList'); + const errorEl = document.getElementById('asteroidError'); + if (!listEl) return; + + const asteroids = await getAsteroids(); + + if (!asteroids || asteroids.length === 0) { + if (errorEl) errorEl.classList.remove('hidden'); + return; + } + + listEl.innerHTML = ''; + + asteroids.slice(0, 5).forEach((ast) => { + const item = document.createElement('div'); + item.className = 'asteroid-item'; + + const name = document.createElement('div'); + name.className = 'asteroid-name'; + name.textContent = ast.name; + + const meta1 = document.createElement('div'); + meta1.className = 'asteroid-meta'; + meta1.textContent = 'Distance: ' + ast.distance + ' LD'; + + const meta2 = document.createElement('div'); + meta2.className = 'asteroid-meta'; + meta2.textContent = 'Velocity: ' + ast.velocity + ' km/s'; + + const risk = document.createElement('div'); + risk.className = 'asteroid-risk'; + risk.textContent = 'Risk: ' + ast.risk; + + item.appendChild(name); + item.appendChild(meta1); + item.appendChild(meta2); + item.appendChild(risk); + + listEl.appendChild(item); + }); +} diff --git a/frontend/src/components/Dashboard.js b/frontend/src/components/Dashboard.js new file mode 100644 index 0000000..d4a63bc --- /dev/null +++ b/frontend/src/components/Dashboard.js @@ -0,0 +1,18 @@ +import { getStats } from '../services/api.js'; + +export async function initDashboard() { + const totalEl = document.getElementById('metricTotal'); + const distEl = document.getElementById('metricAvgDistance'); + const velEl = document.getElementById('metricAvgVelocity'); + + if (!totalEl || !distEl || !velEl) return; + + const stats = await getStats(); + const total = stats.total || 0; + const avgDistance = stats.avgDistance || 0; + const avgVelocity = stats.avgVelocity || 0; + + totalEl.textContent = total; + distEl.textContent = avgDistance.toFixed(1) + ' LD'; + velEl.textContent = avgVelocity.toFixed(1) + ' km/s'; +} diff --git a/frontend/src/components/OrbitCanvas.js b/frontend/src/components/OrbitCanvas.js new file mode 100644 index 0000000..24e579a --- /dev/null +++ b/frontend/src/components/OrbitCanvas.js @@ -0,0 +1,65 @@ +export function initOrbitCanvas() { + const canvas = document.getElementById('orbitCanvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + + function resize() { + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * window.devicePixelRatio; + canvas.height = rect.height * window.devicePixelRatio; + ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0); + } + + resize(); + window.addEventListener('resize', resize); + + let t = 0; + + function draw() { + const w = canvas.clientWidth; + const h = canvas.clientHeight; + + ctx.fillStyle = 'rgba(2, 6, 23, 0.6)'; + ctx.fillRect(0, 0, w, h); + + const cx = w / 2; + const cy = h / 2; + + ctx.strokeStyle = 'rgba(148, 163, 184, 0.6)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.arc(cx, cy, 80, 0, Math.PI * 2); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(cx, cy, 140, 0, Math.PI * 2); + ctx.stroke(); + + ctx.fillStyle = '#22d3ee'; + ctx.beginPath(); + ctx.arc(cx, cy, 18, 0, Math.PI * 2); + ctx.fill(); + + const angle1 = t * 0.02; + const x1 = cx + 80 * Math.cos(angle1); + const y1 = cy + 80 * Math.sin(angle1); + ctx.fillStyle = '#3b82f6'; + ctx.beginPath(); + ctx.arc(x1, y1, 6, 0, Math.PI * 2); + ctx.fill(); + + const angle2 = t * -0.012; + const x2 = cx + 140 * Math.cos(angle2); + const y2 = cy + 140 * Math.sin(angle2); + ctx.fillStyle = '#f97316'; + ctx.beginPath(); + ctx.arc(x2, y2, 8, 0, Math.PI * 2); + ctx.fill(); + + t += 1; + requestAnimationFrame(draw); + } + + requestAnimationFrame(draw); +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..f6eda32 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,9 @@ +import { initOrbitCanvas } from './components/OrbitCanvas.js'; +import { initAsteroidPanel } from './components/AsteroidPanel.js'; +import { initDashboard } from './components/Dashboard.js'; + +window.addEventListener('DOMContentLoaded', () => { + initOrbitCanvas(); + initAsteroidPanel(); + initDashboard(); +}); diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..54b39dd --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000..d5230f5 --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,33 @@ +const API_BASE = ''; + +async function fetchJson(path) { + if (!API_BASE) throw new Error('API disabled'); + const res = await fetch(API_BASE + path); + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); +} + +export async function getAsteroids() { + try { + const data = await fetchJson('/asteroids'); + return data.asteroids || data || []; + } catch (e) { + console.warn('Using mock asteroids:', e); + return [ + { id: 1, name: '2026 AB1', distance: 3.4, velocity: 17.1, risk: 'Low' }, + { id: 2, name: 'Apollo-NEO 33', distance: 1.2, velocity: 22.4, risk: 'Medium' }, + { id: 3, name: '2025 QX9', distance: 0.8, velocity: 28.9, risk: 'Medium' }, + { id: 4, name: 'PHA-192', distance: 10.1, velocity: 12.3, risk: 'Low' }, + { id: 5, name: 'Tempel-NEO', distance: 6.7, velocity: 19.5, risk: 'Low' }, + ]; + } +} + +export async function getStats() { + try { + return await fetchJson('/stats'); + } catch (e) { + console.warn('Using mock stats:', e); + return { total: 5, avgDistance: 4.4, avgVelocity: 20.4 }; + } +} diff --git a/frontend/src/services/blank.js b/frontend/src/services/blank.js new file mode 100644 index 0000000..f70432a --- /dev/null +++ b/frontend/src/services/blank.js @@ -0,0 +1 @@ +//blank diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..9ba368b --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,147 @@ +:root { + --card: rgba(15, 23, 42, 0.9); + --border: rgba(148, 163, 184, 0.4); + --text-main: #e5e7eb; + --text-muted: #9ca3af; + --accent-blue: #3b82f6; + --accent-orange: #f97316; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: radial-gradient(circle at top, #0f172a 0, #020617 45%, #000 100%); + color: var(--text-main); + padding: 24px; +} + +.app-header { + text-align: center; + margin-bottom: 24px; +} + +.app-header h1 { + margin: 0; + font-size: 2.2rem; + background: linear-gradient(90deg, #60a5fa, #a855f7, #22d3ee); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.app-layout { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1.4fr); + gap: 20px; +} + +@media (max-width: 1024px) { + .app-layout { + grid-template-columns: 1fr; + } +} + +.card { + background: var(--card); + border-radius: 16px; + padding: 16px 18px 20px; + border: 1px solid var(--border); + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.8); +} + +.card h2 { + margin: 0 0 12px; + font-size: 1.2rem; + text-align: center; +} + +.card-orbit { + grid-column: span 2; +} + +#orbitCanvas { + width: 100%; + height: 320px; + display: block; + border-radius: 12px; + background: radial-gradient(circle at top, #020617 0, #000 70%); + border: 1px solid rgba(59, 130, 246, 0.5); +} + +.hint { + margin-top: 8px; + font-size: 0.8rem; + text-align: center; + color: var(--text-muted); +} + +.asteroid-list { + max-height: 320px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; + padding-right: 6px; +} + +.asteroid-item { + padding: 10px 12px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.85); + border-left: 4px solid var(--accent-blue); +} + +.asteroid-name { + font-weight: 600; + margin-bottom: 4px; +} + +.asteroid-meta { + font-size: 0.8rem; + color: var(--text-muted); +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 10px; +} + +.metric-box { + padding: 10px 8px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.85); + text-align: center; +} + +.metric-value { + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 4px; +} + +.metric-label { + font-size: 0.8rem; + color: var(--text-muted); +} + +.error { + margin-top: 10px; + font-size: 0.8rem; + text-align: center; + color: #f97373; +} + +.hidden { + display: none; +}