Files
Shadowbroker/frontend/src/components/OnboardingModal.tsx
T
anoracleofra-code 8cddf6794d feat: v0.4 — satellite imagery, KiwiSDR radio, LOCATE bar & security cleanup
New features:
- NASA GIBS (MODIS Terra) daily satellite imagery with 30-day time slider
- Esri World Imagery high-res satellite layer (sub-meter, zoom 18+)
- KiwiSDR SDR receivers on map with embedded radio tuner
- Sentinel-2 intel card — right-click for recent satellite photo popup
- LOCATE bar — search by coordinates or place name (Nominatim geocoding)
- SATELLITE style preset in bottom bar cycling
- v0.4 changelog modal on first launch

Fixes:
- Satellite imagery renders below data icons (imagery-ceiling anchor)
- Sentinel-2 opens full-res PNG directly (not STAC catalog JSON)
- Light/dark theme: UI stays dark, only map basemap changes

Security:
- Removed test files with hardcoded API keys from tracking
- Removed .git_backup directory from tracking
- Updated .gitignore to exclude test files, dev scripts, cache files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Former-commit-id: e89e992293
2026-03-09 17:46:33 -06:00

291 lines
16 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X, ExternalLink, Key, Shield, Radar, Globe, Satellite, Ship, Radio } from "lucide-react";
const STORAGE_KEY = "shadowbroker_onboarding_complete";
const API_GUIDES = [
{
name: "OpenSky Network",
icon: <Radar size={14} className="text-cyan-400" />,
required: true,
description: "Flight tracking with global ADS-B coverage. Provides real-time aircraft positions.",
steps: [
"Create a free account at opensky-network.org",
"Go to Dashboard → OAuth → Create Client",
"Copy your Client ID and Client Secret",
"Paste both into Settings → Aviation",
],
url: "https://opensky-network.org/index.php?option=com_users&view=registration",
color: "cyan",
},
{
name: "AIS Stream",
icon: <Ship size={14} className="text-blue-400" />,
required: true,
description: "Real-time vessel tracking via AIS (Automatic Identification System).",
steps: [
"Register at aisstream.io",
"Navigate to your API Keys page",
"Generate a new API key",
"Paste it into Settings → Maritime",
],
url: "https://aisstream.io/authenticate",
color: "blue",
},
];
const FREE_SOURCES = [
{ name: "ADS-B Exchange", desc: "Military & general aviation", icon: <Radar size={12} /> },
{ name: "USGS Earthquakes", desc: "Global seismic data", icon: <Globe size={12} /> },
{ name: "CelesTrak", desc: "2,000+ satellite orbits", icon: <Satellite size={12} /> },
{ name: "GDELT Project", desc: "Global conflict events", icon: <Globe size={12} /> },
{ name: "RainViewer", desc: "Weather radar overlay", icon: <Globe size={12} /> },
{ name: "OpenMHz", desc: "Radio scanner feeds", icon: <Radio size={12} /> },
{ name: "RSS Feeds", desc: "NPR, BBC, Reuters, AP", icon: <Globe size={12} /> },
{ name: "Yahoo Finance", desc: "Defense stocks & oil", icon: <Globe size={12} /> },
];
interface OnboardingModalProps {
onClose: () => void;
onOpenSettings: () => void;
}
const OnboardingModal = React.memo(function OnboardingModal({ onClose, onOpenSettings }: OnboardingModalProps) {
const [step, setStep] = useState(0);
const handleDismiss = () => {
localStorage.setItem(STORAGE_KEY, "true");
onClose();
};
const handleOpenSettings = () => {
localStorage.setItem(STORAGE_KEY, "true");
onClose();
onOpenSettings();
};
return (
<AnimatePresence>
{/* Backdrop */}
<motion.div
key="onboarding-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[10000]"
onClick={handleDismiss}
/>
{/* Modal */}
<motion.div
key="onboarding-modal"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-0 z-[10001] flex items-center justify-center pointer-events-none"
>
<div
className="w-[580px] max-h-[85vh] bg-[var(--bg-secondary)]/98 border border-cyan-900/50 rounded-xl shadow-[0_0_80px_rgba(0,200,255,0.08)] pointer-events-auto flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-6 pb-4 border-b border-[var(--border-primary)]/80">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center">
<Shield size={20} className="text-cyan-400" />
</div>
<div>
<h2 className="text-sm font-bold tracking-[0.2em] text-[var(--text-primary)] font-mono">MISSION BRIEFING</h2>
<span className="text-[9px] text-[var(--text-muted)] font-mono tracking-widest">FIRST-TIME SETUP</span>
</div>
</div>
<button
onClick={handleDismiss}
className="w-8 h-8 rounded-lg border border-[var(--border-primary)] hover:border-red-500/50 flex items-center justify-center text-[var(--text-muted)] hover:text-red-400 transition-all hover:bg-red-950/20"
>
<X size={14} />
</button>
</div>
</div>
{/* Step Indicators */}
<div className="flex gap-2 px-6 pt-4">
{["Welcome", "API Keys", "Free Sources"].map((label, i) => (
<button
key={label}
onClick={() => setStep(i)}
className={`flex-1 py-1.5 text-[9px] font-mono tracking-widest rounded border transition-all ${
step === i
? "border-cyan-500/50 text-cyan-400 bg-cyan-950/20"
: "border-[var(--border-primary)] text-[var(--text-muted)] hover:border-[var(--border-secondary)] hover:text-[var(--text-secondary)]"
}`}
>
{label.toUpperCase()}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto styled-scrollbar p-6">
{step === 0 && (
<div className="space-y-4">
<div className="text-center py-4">
<div className="text-lg font-bold tracking-[0.3em] text-[var(--text-primary)] font-mono mb-2">
S H A D O W <span className="text-cyan-400">B R O K E R</span>
</div>
<p className="text-[11px] text-[var(--text-secondary)] font-mono leading-relaxed max-w-md mx-auto">
Real-time OSINT dashboard aggregating 12+ live intelligence sources.
Flights, ships, satellites, earthquakes, conflicts, and more all on one map.
</p>
</div>
<div className="bg-yellow-950/20 border border-yellow-500/20 rounded-lg p-4">
<div className="flex items-start gap-2">
<Key size={14} className="text-yellow-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-[11px] text-yellow-400 font-mono font-bold mb-1">API Keys Required</p>
<p className="text-[10px] text-[var(--text-secondary)] font-mono leading-relaxed">
Two API keys are needed for full functionality: <span className="text-cyan-400">OpenSky Network</span> (flights) and <span className="text-blue-400">AIS Stream</span> (ships).
Both are free. Without them, some panels will show no data.
</p>
</div>
</div>
</div>
<div className="bg-green-950/20 border border-green-500/20 rounded-lg p-4">
<div className="flex items-start gap-2">
<Globe size={14} className="text-green-500 mt-0.5 flex-shrink-0" />
<div>
<p className="text-[11px] text-green-400 font-mono font-bold mb-1">8 Sources Work Immediately</p>
<p className="text-[10px] text-[var(--text-secondary)] font-mono leading-relaxed">
Military aircraft, satellites, earthquakes, global conflicts, weather radar, radio scanners, news, and market data all work out of the box no keys needed.
</p>
</div>
</div>
</div>
</div>
)}
{step === 1 && (
<div className="space-y-4">
{API_GUIDES.map((api) => (
<div key={api.name} className={`rounded-lg border border-${api.color}-900/30 bg-${api.color}-950/10 p-4`}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
{api.icon}
<span className="text-xs font-mono text-white font-bold">{api.name}</span>
<span className="text-[8px] font-mono px-1.5 py-0.5 rounded border border-yellow-500/30 text-yellow-400 bg-yellow-950/20">REQUIRED</span>
</div>
<a
href={api.url}
target="_blank"
rel="noopener noreferrer"
className={`text-[10px] font-mono text-${api.color}-400 hover:text-${api.color}-300 flex items-center gap-1 transition-colors`}
>
GET KEY <ExternalLink size={10} />
</a>
</div>
<p className="text-[10px] text-[var(--text-secondary)] font-mono mb-3">{api.description}</p>
<ol className="space-y-1.5">
{api.steps.map((s, i) => (
<li key={i} className="flex items-start gap-2">
<span className={`text-[9px] font-mono text-${api.color}-500 font-bold mt-0.5 w-3 flex-shrink-0`}>{i + 1}.</span>
<span className="text-[10px] text-gray-300 font-mono">{s}</span>
</li>
))}
</ol>
</div>
))}
<button
onClick={handleOpenSettings}
className="w-full py-3 rounded-lg bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 hover:bg-cyan-500/20 transition-colors text-[11px] font-mono tracking-widest flex items-center justify-center gap-2"
>
<Key size={14} />
OPEN SETTINGS TO ENTER KEYS
</button>
</div>
)}
{step === 2 && (
<div className="space-y-3">
<p className="text-[10px] text-[var(--text-secondary)] font-mono mb-3">
These data sources are completely free and require no API keys. They activate automatically on launch.
</p>
<div className="grid grid-cols-2 gap-2">
{FREE_SOURCES.map((src) => (
<div key={src.name} className="rounded-lg border border-[var(--border-primary)]/60 bg-[var(--bg-secondary)]/30 p-3 hover:border-[var(--border-secondary)] transition-colors">
<div className="flex items-center gap-2 mb-1">
<span className="text-green-500">{src.icon}</span>
<span className="text-[10px] font-mono text-[var(--text-primary)] font-medium">{src.name}</span>
</div>
<p className="text-[9px] text-[var(--text-muted)] font-mono">{src.desc}</p>
</div>
))}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-[var(--border-primary)]/80 flex items-center justify-between">
<button
onClick={() => setStep(Math.max(0, step - 1))}
className={`px-4 py-2 rounded border text-[10px] font-mono tracking-widest transition-all ${
step === 0
? "border-[var(--border-primary)] text-[var(--text-muted)] cursor-not-allowed"
: "border-[var(--border-primary)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--border-secondary)]"
}`}
disabled={step === 0}
>
PREV
</button>
<div className="flex gap-1.5">
{[0, 1, 2].map((i) => (
<div key={i} className={`w-1.5 h-1.5 rounded-full transition-colors ${step === i ? "bg-cyan-400" : "bg-[var(--border-primary)]"}`} />
))}
</div>
{step < 2 ? (
<button
onClick={() => setStep(step + 1)}
className="px-4 py-2 rounded border border-cyan-500/40 text-cyan-400 hover:bg-cyan-500/10 text-[10px] font-mono tracking-widest transition-all"
>
NEXT
</button>
) : (
<button
onClick={handleDismiss}
className="px-4 py-2 rounded bg-cyan-500/20 border border-cyan-500/40 text-cyan-400 hover:bg-cyan-500/30 text-[10px] font-mono tracking-widest transition-all"
>
LAUNCH
</button>
)}
</div>
</div>
</motion.div>
</AnimatePresence>
);
});
export function useOnboarding() {
const [showOnboarding, setShowOnboarding] = useState(false);
useEffect(() => {
const done = localStorage.getItem(STORAGE_KEY);
if (!done) {
setShowOnboarding(true);
}
}, []);
return { showOnboarding, setShowOnboarding };
}
export default OnboardingModal;