proWheather / index.html
Atulsinghbirla's picture
Update index.html
a1fda50 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Pro Weather + NASA Portal | Jarvis Build</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;600;700&family=Space+Grotesk:wght@300;500;700&display=swap" rel="stylesheet">
<!-- Leaflet CSS (Map) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- Three.js (3D) - Using r128 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<!-- Leaflet JS (Map) -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<style>
/* ================= VARIABLES & RESET ================= */
:root {
--bg-dark: #0a0e1a;
--bg-panel: rgba(255, 255, 255, 0.03);
--bg-panel-hover: rgba(255, 255, 255, 0.08);
--border-light: rgba(255, 255, 255, 0.08);
--accent-primary: #00f0ff;
--accent-secondary: #a78bfa;
--accent-tertiary: #ec4899;
--accent-warning: #fbbf24;
--accent-danger: #ef4444;
--accent-success: #10b981;
--text-main: #ffffff;
--text-muted: rgba(255, 255, 255, 0.5);
--font-main: 'Outfit', sans-serif;
--font-display: 'Space Grotesk', sans-serif;
--glass-blur: blur(20px);
--radius-lg: 28px;
--radius-md: 18px;
--radius-sm: 10px;
--shadow-glow: 0 0 40px rgba(0, 240, 255, 0.15);
--shadow-card: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Light Theme */
body.light-mode {
--bg-dark: #f5f7fa;
--bg-panel: rgba(255, 255, 255, 0.8);
--bg-panel-hover: rgba(255, 255, 255, 0.95);
--border-light: rgba(0, 0, 0, 0.06);
--text-main: #1a1c2e;
--text-muted: rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 40px rgba(167, 139, 250, 0.2);
--shadow-card: 0 8px 32px rgba(0, 0, 0, 0.08);
}
* { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
body {
font-family: var(--font-main);
background: var(--bg-dark);
background-image:
radial-gradient(circle at 15% 15%, rgba(167, 139, 250, 0.12) 0%, transparent 50%),
radial-gradient(circle at 85% 85%, rgba(0, 240, 255, 0.08) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.05) 0%, transparent 70%);
color: var(--text-main);
min-height: 100vh;
overflow-x: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Scrollbar */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: rgba(0,0,0,0.05); }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
/* ================= LAYOUT ================= */
.app-wrapper {
max-width: 1400px;
margin: 0 auto;
padding: 24px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 36px;
padding: 16px 0;
animation: slideDown 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.brand h1 {
font-family: var(--font-display);
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.03em;
position: relative;
display: inline-block;
}
.brand span {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary), var(--accent-tertiary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradient-shift 3s ease infinite;
background-size: 200% 200%;
}
.header-controls {
display: flex;
gap: 12px;
align-items: center;
}
/* Buttons */
.btn {
background: var(--bg-panel);
border: 1px solid var(--border-light);
color: var(--text-main);
padding: 12px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: var(--glass-blur);
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transform: translate(-50%, -50%);
transition: width 0.4s, height 0.4s;
}
.btn:hover::before {
width: 300px;
height: 300px;
}
.btn:hover {
background: var(--bg-panel-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-card);
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
color: #000;
font-weight: 700;
box-shadow: var(--shadow-glow);
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-2px) scale(1.02);
box-shadow: 0 0 50px rgba(0, 240, 255, 0.3);
}
.btn-icon {
padding: 12px;
border-radius: 50%;
width: 44px;
height: 44px;
justify-content: center;
font-size: 1.2rem;
}
/* ================= SEARCH SECTION ================= */
.search-container {
display: flex;
gap: 12px;
margin-bottom: 36px;
background: var(--bg-panel);
padding: 10px;
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
backdrop-filter: var(--glass-blur);
box-shadow: var(--shadow-card);
animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both;
transition: all 0.3s ease;
}
.search-container:focus-within {
border-color: var(--accent-primary);
box-shadow: 0 0 30px rgba(0, 240, 255, 0.2);
}
.search-input {
flex: 1;
background: transparent;
border: none;
color: var(--text-main);
padding: 0 16px;
font-size: 1.05rem;
font-family: var(--font-main);
}
.search-input::placeholder { color: var(--text-muted); }
/* ================= SECTION 1: WEATHER & MAP ================= */
.dashboard-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 24px;
margin-bottom: 36px;
animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}
.card {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 28px;
backdrop-filter: var(--glass-blur);
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-card);
position: relative;
}
.card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.03) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.card:hover::before {
opacity: 1;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
border-color: rgba(255, 255, 255, 0.12);
}
/* Weather Hero */
.weather-hero {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: visible;
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.location-tag {
font-family: var(--font-display);
font-size: 2.8rem;
font-weight: 700;
line-height: 1.1;
background: linear-gradient(135deg, var(--text-main), var(--text-muted));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.location-date {
color: var(--text-muted);
font-size: 1rem;
margin-top: 6px;
font-weight: 300;
}
.weather-main {
display: flex;
align-items: center;
gap: 28px;
margin: 20px 0;
}
.temp-big {
font-size: 6rem;
font-weight: 700;
font-family: var(--font-display);
line-height: 0.9;
background: linear-gradient(180deg, var(--text-main) 0%, var(--accent-primary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 60px rgba(0, 240, 255, 0.3);
}
.weather-icon-lg {
width: 140px;
height: 140px;
filter: drop-shadow(0 0 25px rgba(255,255,255,0.3));
animation: float 5s ease-in-out infinite;
}
.weather-condition {
font-size: 1.6rem;
text-transform: capitalize;
color: var(--accent-primary);
font-weight: 600;
letter-spacing: -0.02em;
}
/* Weather Grid Stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-top: 28px;
}
.stat-box {
background: rgba(255,255,255,0.02);
padding: 16px;
border-radius: var(--radius-md);
text-align: center;
border: 1px solid var(--border-light);
transition: all 0.3s ease;
}
.stat-box:hover {
background: rgba(255,255,255,0.05);
transform: translateY(-2px);
}
.stat-label {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 6px;
display: block;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.stat-val {
font-size: 1.2rem;
font-weight: 700;
color: var(--text-main);
}
.aqi-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.03em;
animation: pulse 2s ease-in-out infinite;
}
.aqi-good { background: rgba(16, 185, 129, 0.2); color: var(--accent-success); }
.aqi-fair { background: rgba(251, 191, 36, 0.2); color: var(--accent-warning); }
.aqi-poor { background: rgba(239, 68, 68, 0.2); color: var(--accent-danger); }
/* Map Card */
.map-wrapper {
height: 100%;
min-height: 340px;
border-radius: var(--radius-md);
overflow: hidden;
position: relative;
}
#map {
height: 100%;
width: 100%;
z-index: 1;
filter: saturate(1.2) contrast(1.1);
}
/* Forecast Row */
.forecast-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 48px;
animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.3s both;
}
.forecast-card {
background: var(--bg-panel);
padding: 20px;
border-radius: var(--radius-md);
text-align: center;
border: 1px solid var(--border-light);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: var(--glass-blur);
}
.forecast-card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-card);
background: var(--bg-panel-hover);
}
.fc-day {
font-size: 0.95rem;
color: var(--text-muted);
margin-bottom: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.fc-icon {
width: 50px;
height: 50px;
margin: 8px auto;
display: block;
filter: drop-shadow(0 2px 8px rgba(255,255,255,0.2));
}
.fc-temp {
font-weight: 700;
margin-top: 12px;
font-size: 1.3rem;
color: var(--accent-primary);
}
/* ================= SECTION 2: NASA PORTAL ================= */
.section-title {
font-family: var(--font-display);
font-size: 1.8rem;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
letter-spacing: -0.02em;
}
.section-title::before {
content: '';
display: block;
width: 5px;
height: 32px;
background: linear-gradient(180deg, var(--accent-secondary), var(--accent-tertiary));
border-radius: 3px;
box-shadow: 0 0 15px var(--accent-secondary);
}
.nasa-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 48px;
animation: fadeInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) 0.4s both;
}
/* APOD spans 2 columns */
.nasa-apod {
grid-column: span 2;
position: relative;
height: 450px;
cursor: pointer;
overflow: hidden;
}
.apod-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
filter: brightness(0.85);
}
.nasa-apod:hover .apod-bg {
transform: scale(1.05);
filter: brightness(1);
}
.apod-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 36px;
background: linear-gradient(0deg, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 60%, transparent);
z-index: 2;
transition: padding 0.3s ease;
}
.nasa-apod:hover .apod-content {
padding-bottom: 42px;
}
.apod-tag {
background: linear-gradient(135deg, var(--accent-secondary), var(--accent-tertiary));
padding: 6px 12px;
border-radius: 6px;
font-size: 0.7rem;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 10px;
display: inline-block;
letter-spacing: 0.08em;
}
/* Lists */
.nasa-list-card {
display: flex;
flex-direction: column;
height: 450px;
}
.nasa-list-header {
font-weight: 700;
border-bottom: 2px solid var(--border-light);
padding-bottom: 14px;
margin-bottom: 14px;
font-size: 1.05rem;
display: flex;
align-items: center;
gap: 8px;
}
.scroll-list {
overflow-y: auto;
flex: 1;
padding-right: 6px;
}
.news-item {
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid var(--border-light);
transition: all 0.2s ease;
}
.news-item:last-child { border: none; }
.news-item:hover {
padding-left: 8px;
border-color: var(--accent-primary);
}
.news-title {
font-size: 0.95rem;
font-weight: 600;
margin-bottom: 6px;
color: var(--accent-primary);
cursor: pointer;
transition: all 0.2s ease;
line-height: 1.4;
}
.news-title:hover {
color: var(--accent-secondary);
text-decoration: underline;
}
.news-meta {
font-size: 0.78rem;
color: var(--text-muted);
font-weight: 400;
}
/* Space Weather */
.space-weather-card {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(11, 13, 26, 0.5));
}
.sw-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 0.75rem;
font-weight: bold;
margin-right: 8px;
letter-spacing: 0.05em;
}
.bg-danger { background: rgba(239, 68, 68, 0.25); color: var(--accent-danger); }
.bg-warn { background: rgba(251, 191, 36, 0.25); color: var(--accent-warning); }
.bg-info { background: rgba(0, 240, 255, 0.2); color: var(--accent-primary); }
/* Image Search Results */
.img-search-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 14px;
margin-top: 18px;
max-height: 340px;
overflow-y: auto;
}
.img-result {
aspect-ratio: 1;
background: #000;
border-radius: var(--radius-sm);
overflow: hidden;
position: relative;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.img-result img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.img-result:hover {
transform: scale(1.05);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
z-index: 10;
}
.img-result:hover img {
transform: scale(1.15);
}
/* ================= 3D SOLAR SYSTEM MODAL ================= */
.solar-modal {
position: fixed;
inset: 0;
background: #000;
z-index: 9999;
display: flex;
flex-direction: column;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.solar-modal.active {
opacity: 1;
pointer-events: auto;
}
.solar-ui {
position: absolute;
top: 24px;
left: 24px;
right: 24px;
display: flex;
justify-content: space-between;
z-index: 10;
pointer-events: none;
}
.solar-ui button { pointer-events: auto; }
.solar-canvas { width: 100%; height: 100%; }
/* ================= UTILS ================= */
.hidden { display: none !important; }
.loader {
border: 4px solid rgba(255,255,255,0.1);
border-top: 4px solid var(--accent-primary);
border-radius: 50%;
width: 48px;
height: 48px;
animation: spin 0.8s linear infinite;
}
.error-message {
background: rgba(239, 68, 68, 0.15);
border: 1px solid var(--accent-danger);
color: var(--accent-danger);
padding: 16px 20px;
border-radius: var(--radius-md);
margin: 20px 0;
font-weight: 600;
animation: shake 0.5s ease;
}
/* ================= ANIMATIONS ================= */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-15px); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
/* Mobile Responsive */
@media (max-width: 900px) {
.app-wrapper { padding: 16px; }
.dashboard-grid { grid-template-columns: 1fr; }
.nasa-grid { grid-template-columns: 1fr; }
.nasa-apod { grid-column: span 1; height: 320px; }
.forecast-row { grid-template-columns: repeat(2, 1fr); }
.forecast-card:nth-child(5) { grid-column: span 2; }
.temp-big { font-size: 4rem; }
.weather-main { flex-direction: column; text-align: center; }
.weather-header { flex-direction: column; gap: 12px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.header-controls { gap: 8px; }
.btn { padding: 10px 14px; font-size: 0.85rem; }
.section-title { font-size: 1.4rem; }
}
</style>
</head>
<body>
<div class="app-wrapper">
<!-- Header -->
<header>
<div class="brand">
<h1>ProWeather <span>+</span></h1>
</div>
<div class="header-controls">
<button class="btn btn-primary" onclick="openSolarSystem()">
<span>🪐 3D Solar</span>
</button>
<button class="btn btn-icon" onclick="toggleTheme()" title="Toggle Theme">
🌗
</button>
</div>
</header>
<!-- Search -->
<div class="search-container">
<input type="text" id="cityInput" class="search-input" placeholder="Search city (e.g., Tokyo, New York, London)..." onkeypress="handleEnter(event)">
<button class="btn btn-primary" id="searchBtn" onclick="initApp()">Search</button>
<button class="btn" onclick="getGeoLocation()">📍 Location</button>
</div>
<!-- Loading Indicator -->
<div id="loadingState" class="hidden" style="text-align: center; padding: 60px;">
<div class="loader" style="margin: 0 auto 16px;"></div>
<p style="color: var(--text-muted); font-size: 1.1rem; font-weight: 500;">Scanning Atmospheric Data...</p>
</div>
<!-- Error Message -->
<div id="errorMessage" class="hidden error-message"></div>
<!-- WEATHER SECTION -->
<div id="weatherDashboard" class="hidden">
<div class="dashboard-grid">
<!-- Main Weather Card -->
<div class="card weather-hero">
<div class="weather-header">
<div>
<h2 class="location-tag" id="cityName">--</h2>
<p class="location-date" id="currentDate">--</p>
</div>
<div id="aqiBadge" class="aqi-badge hidden">AQI: --</div>
</div>
<div class="weather-main">
<img id="weatherIcon" class="weather-icon-lg" src="" alt="Weather">
<div>
<div class="temp-big" id="temperature">--°</div>
<div class="weather-condition" id="description">--</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-box">
<span class="stat-label">Humidity</span>
<div class="stat-val" id="humidity">--%</div>
</div>
<div class="stat-box">
<span class="stat-label">Wind</span>
<div class="stat-val" id="wind">-- m/s</div>
</div>
<div class="stat-box">
<span class="stat-label">Pressure</span>
<div class="stat-val" id="pressure">-- hPa</div>
</div>
<div class="stat-box">
<span class="stat-label">Feels Like</span>
<div class="stat-val" id="feelslike">--°</div>
</div>
</div>
</div>
<!-- Map Card -->
<div class="card" style="padding: 0; overflow: hidden;">
<div class="map-wrapper">
<div id="map"></div>
</div>
</div>
</div>
<!-- Forecast Row -->
<h3 class="section-title">5-Day Forecast</h3>
<div id="forecastRow" class="forecast-row"></div>
</div>
<!-- NASA SECTION -->
<div id="nasaSection" class="hidden">
<h3 class="section-title">NASA Intelligence Hub</h3>
<div class="nasa-grid">
<!-- APOD -->
<div class="card nasa-apod" id="apodCard">
<div class="apod-bg" id="apodBg"></div>
<div class="apod-content">
<span class="apod-tag">Image of the Day</span>
<h2 style="font-size: 1.5rem; margin-bottom: 8px; font-weight: 700;" id="apodTitle">Loading Space View...</h2>
<p style="font-size: 0.9rem; opacity: 0.85; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;" id="apodDesc">Fetching latest imagery from deep space...</p>
</div>
</div>
<!-- Space Weather -->
<div class="card nasa-list-card space-weather-card">
<div class="nasa-list-header">⚡ Space Weather</div>
<div class="scroll-list" id="spaceWeatherList">
<p style="padding: 12px; color: var(--text-muted); text-align: center;">Monitoring solar activity...</p>
</div>
</div>
<!-- Earth Watch -->
<div class="card nasa-list-card">
<div class="nasa-list-header">🌍 Earth Events</div>
<div class="scroll-list" id="earthNewsList">
<p style="padding: 12px; color: var(--text-muted); text-align: center;">Scanning natural events...</p>
</div>
</div>
<!-- NASA Image Search -->
<div class="card" style="grid-column: 1 / -1;">
<div class="nasa-list-header">🔭 NASA Image Library</div>
<div style="display: flex; gap: 12px;">
<input type="text" id="nasaSearchInput" class="search-input" style="background: rgba(0,0,0,0.15); font-size: 0.95rem; border: 1px solid var(--border-light); border-radius: var(--radius-sm); padding: 12px 16px;" placeholder="Search galaxies, stars, missions..." onkeypress="handleNasaSearchEnter(event)">
<button class="btn btn-primary" onclick="searchNasaImages()">Find</button>
</div>
<div id="nasaImageResults" class="img-search-grid"></div>
</div>
<!-- Tech Projects -->
<div class="card nasa-list-card">
<div class="nasa-list-header">🚀 TechPort</div>
<div class="scroll-list" id="techList">
<p style="padding: 12px; color: var(--text-muted); text-align: center;">Loading projects...</p>
</div>
</div>
<!-- Asteroids -->
<div class="card nasa-list-card">
<div class="nasa-list-header">☄️ Near-Earth Objects</div>
<div class="scroll-list" id="neoList">
<p style="padding: 12px; color: var(--text-muted); text-align: center;">Scanning sky...</p>
</div>
</div>
<!-- Mars Rover -->
<div class="card" style="grid-column: span 1; height: 450px; position: relative; overflow: hidden;">
<div class="nasa-list-header" style="position: absolute; top: 20px; left: 20px; z-index: 2; background: rgba(0,0,0,0.7); padding: 6px 12px; border-radius: 8px; backdrop-filter: blur(10px);">🤖 Mars Rover</div>
<img id="marsImg" src="" style="width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease;" alt="Mars" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
</div>
</div>
</div>
</div>
<!-- 3D Solar System Modal -->
<div id="solarModal" class="solar-modal">
<div class="solar-ui">
<h2 style="color: white; font-family: var(--font-display); font-size: 1.8rem;">Interactive Solar System</h2>
<button class="btn" style="background: rgba(255,255,255,0.15); color: white; border: 1px solid rgba(255,255,255,0.2);" onclick="closeSolarSystem()">Close ✕</button>
</div>
<div id="solarContainer" class="solar-canvas"></div>
</div>
<script>
/* ================= CONFIGURATION ================= */
const API_KEYS = {
WEATHER: "7d9745a7db624a8a511e10c0b9890f71",
NASA: "FaAyJO3S976lsFoLz6izgd06bTEikTvYmC0MflK4"
};
let mapInstance = null;
let mapMarker = null;
const $ = id => document.getElementById(id);
/* ================= ERROR HANDLING ================= */
function showError(message) {
const errorEl = $("errorMessage");
errorEl.textContent = "⚠️ " + message;
errorEl.classList.remove("hidden");
setTimeout(() => errorEl.classList.add("hidden"), 5000);
}
/* ================= APP INITIALIZATION ================= */
async function initApp() {
const city = $("cityInput").value.trim();
if (!city) {
showError("Please enter a city name");
return;
}
toggleLoading(true);
$("errorMessage").classList.add("hidden");
try {
const weatherData = await fetchWeather(city);
if (weatherData) {
renderWeather(weatherData);
fetchForecast(weatherData.coord.lat, weatherData.coord.lon);
fetchAQI(weatherData.coord.lat, weatherData.coord.lon);
updateMap(weatherData.coord.lat, weatherData.coord.lon, weatherData.name);
$("weatherDashboard").classList.remove("hidden");
$("nasaSection").classList.remove("hidden");
loadNASAData();
}
} catch (error) {
console.error("App Error:", error);
showError(error.message || "Failed to fetch data. Please try again.");
} finally {
toggleLoading(false);
}
}
function handleEnter(e) {
if (e.key === 'Enter') initApp();
}
function handleNasaSearchEnter(e) {
if (e.key === 'Enter') searchNasaImages();
}
function toggleLoading(show) {
$("loadingState").classList.toggle("hidden", !show);
if(show) {
$("weatherDashboard").classList.add("hidden");
$("nasaSection").classList.add("hidden");
}
}
/* ================= WEATHER LOGIC ================= */
async function fetchWeather(city) {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${API_KEYS.WEATHER}&units=metric`;
const res = await fetch(url);
if (!res.ok) {
if (res.status === 404) throw new Error("City not found");
if (res.status === 401) throw new Error("Invalid API key");
throw new Error("Weather service unavailable");
}
return await res.json();
}
function renderWeather(data) {
$("cityName").textContent = `${data.name}, ${data.sys.country}`;
$("currentDate").textContent = new Date().toLocaleDateString('en-US', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
});
$("temperature").textContent = `${Math.round(data.main.temp)}°`;
$("description").textContent = data.weather[0].description;
$("humidity").textContent = `${data.main.humidity}%`;
$("wind").textContent = `${data.wind.speed} m/s`;
$("pressure").textContent = `${data.main.pressure} hPa`;
$("feelslike").textContent = `${Math.round(data.main.feels_like)}°`;
const iconCode = data.weather[0].icon;
$("weatherIcon").src = `https://openweathermap.org/img/wn/${iconCode}@4x.png`;
$("weatherIcon").alt = data.weather[0].description;
}
async function fetchAQI(lat, lon) {
try {
const url = `https://api.openweathermap.org/data/2.5/air_pollution?lat=${lat}&lon=${lon}&appid=${API_KEYS.WEATHER}`;
const res = await fetch(url);
if (!res.ok) throw new Error("AQI unavailable");
const data = await res.json();
const aqi = data.list[0].main.aqi;
const badge = $("aqiBadge");
badge.classList.remove("hidden", "aqi-good", "aqi-fair", "aqi-poor");
let label = "Unknown";
if (aqi <= 2) { label = "Good"; badge.classList.add("aqi-good"); }
else if (aqi <= 3) { label = "Fair"; badge.classList.add("aqi-fair"); }
else { label = "Poor"; badge.classList.add("aqi-poor"); }
badge.textContent = `Air Quality: ${label}`;
} catch (e) {
console.warn("AQI Error:", e);
}
}
async function fetchForecast(lat, lon) {
try {
const url = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${API_KEYS.WEATHER}&units=metric`;
const res = await fetch(url);
if (!res.ok) throw new Error("Forecast unavailable");
const data = await res.json();
const container = $("forecastRow");
container.innerHTML = "";
const daily = data.list.filter(item => item.dt_txt.includes("12:00:00")).slice(0, 5);
daily.forEach(day => {
const date = new Date(day.dt * 1000).toLocaleDateString("en-US", { weekday: 'short' });
const div = document.createElement("div");
div.className = "forecast-card";
div.innerHTML = `
<div class="fc-day">${date}</div>
<img class="fc-icon" src="https://openweathermap.org/img/wn/${day.weather[0].icon}@2x.png" alt="${day.weather[0].description}">
<div class="fc-temp">${Math.round(day.main.temp)}°</div>
`;
container.appendChild(div);
});
} catch (e) {
console.warn("Forecast Error:", e);
}
}
function getGeoLocation() {
if (!navigator.geolocation) {
showError("Geolocation not supported by your browser");
return;
}
toggleLoading(true);
navigator.geolocation.getCurrentPosition(
async (pos) => {
try {
const { latitude, longitude } = pos.coords;
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEYS.WEATHER}&units=metric`;
const res = await fetch(url);
const data = await res.json();
$("cityInput").value = data.name;
initApp();
} catch (e) {
showError("Failed to get location data");
toggleLoading(false);
}
},
(error) => {
showError("Location access denied");
toggleLoading(false);
}
);
}
/* ================= MAP LOGIC ================= */
function updateMap(lat, lon, title) {
try {
if (!mapInstance) {
mapInstance = L.map('map').setView([lat, lon], 11);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19
}).addTo(mapInstance);
} else {
mapInstance.setView([lat, lon], 11);
}
if (mapMarker) mapInstance.removeLayer(mapMarker);
const customIcon = L.divIcon({
className: 'custom-pin',
html: `<div style="background: var(--accent-primary); width: 16px; height: 16px; border-radius: 50%; box-shadow: 0 0 20px var(--accent-primary), 0 0 40px var(--accent-primary);"></div>`
});
mapMarker = L.marker([lat, lon], { icon: customIcon }).addTo(mapInstance)
.bindPopup(`<b style="font-family: var(--font-display);">${title}</b>`)
.openPopup();
setTimeout(() => mapInstance.invalidateSize(), 400);
} catch (e) {
console.error("Map Error:", e);
}
}
/* ================= NASA LOGIC ================= */
let nasaLoaded = false;
async function loadNASAData() {
if (nasaLoaded) return;
nasaLoaded = true;
// APOD
fetch(`https://api.nasa.gov/planetary/apod?api_key=${API_KEYS.NASA}`)
.then(r => r.json())
.then(data => {
const imgUrl = data.media_type === "image" ? data.url : (data.thumbnail_url || "https://apod.nasa.gov/apod/image/2101/NGC1365_Hubble_960.jpg");
$("apodBg").style.backgroundImage = `url('${imgUrl}')`;
$("apodTitle").textContent = data.title || "Astronomy Picture";
$("apodDesc").textContent = data.explanation || "Explore the cosmos";
})
.catch(e => console.warn("APOD Error:", e));
// TechPort
fetch(`https://api.nasa.gov/techport/api/projects?api_key=${API_KEYS.NASA}`)
.then(r => r.json())
.then(data => {
const list = data.projects?.slice(0, 10) || [];
const container = $("techList");
container.innerHTML = list.length ? "" : "<p style='padding:12px;text-align:center;color:var(--text-muted)'>No projects available</p>";
list.forEach(p => {
const item = document.createElement("div");
item.className = "news-item";
item.innerHTML = `
<div class="news-title" onclick="window.open('https://techport.nasa.gov/view/${p.id}', '_blank')">
🚀 ${p.title}
</div>
<div class="news-meta">Updated: ${new Date(p.lastUpdated).toLocaleDateString()}</div>
`;
container.appendChild(item);
});
})
.catch(e => console.warn("TechPort Error:", e));
// NEO
const today = new Date().toISOString().split("T")[0];
fetch(`https://api.nasa.gov/neo/rest/v1/feed?start_date=${today}&end_date=${today}&api_key=${API_KEYS.NASA}`)
.then(r => r.json())
.then(data => {
const asteroids = data.near_earth_objects[today] || [];
const container = $("neoList");
container.innerHTML = asteroids.length ? "" : "<p style='padding:12px;text-align:center;color:var(--text-muted)'>No objects detected today</p>";
asteroids.slice(0, 8).forEach(a => {
const size = Math.round(a.estimated_diameter.meters.estimated_diameter_max);
const haz = a.is_potentially_hazardous_asteroid ? "⚠️" : "✅";
const item = document.createElement("div");
item.className = "news-item";
item.innerHTML = `
<div class="news-title">${haz} ${a.name}</div>
<div class="news-meta">~${size}m • ${parseInt(a.close_approach_data[0].relative_velocity.kilometers_per_hour).toLocaleString()} km/h</div>
`;
container.appendChild(item);
});
})
.catch(e => console.warn("NEO Error:", e));
// Mars Rover
fetch(`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/latest_photos?api_key=${API_KEYS.NASA}`)
.then(r => r.json())
.then(data => {
if(data.latest_photos && data.latest_photos.length > 0) {
$("marsImg").src = data.latest_photos[0].img_src;
}
})
.catch(e => console.warn("Mars Error:", e));
loadSpaceWeather();
loadEarthEvents();
}
/* ================= SPACE WEATHER ================= */
async function loadSpaceWeather() {
const container = $("spaceWeatherList");
const endDate = new Date().toISOString().split("T")[0];
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
try {
const res = await fetch(`https://api.nasa.gov/DONKI/FLR?startDate=${startDate}&endDate=${endDate}&api_key=${API_KEYS.NASA}`);
if (!res.ok) throw new Error("DONKI unavailable");
const data = await res.json();
container.innerHTML = "";
if (data && data.length > 0) {
data.slice(0, 6).forEach(f => {
const item = document.createElement("div");
item.className = "news-item";
const power = f.classType || "Unk";
let badgeClass = "bg-info";
if(power.startsWith("M")) badgeClass = "bg-warn";
if(power.startsWith("X")) badgeClass = "bg-danger";
item.innerHTML = `
<div class="news-title">
<span class="sw-badge ${badgeClass}">${power}</span> Solar Flare Detected
</div>
<div class="news-meta">${new Date(f.beginTime).toLocaleString()}</div>
`;
container.appendChild(item);
});
} else {
container.innerHTML = "<p style='padding:12px;text-align:center;opacity:0.7'>No major solar activity this week</p>";
}
} catch(e) {
console.warn("DONKI Error:", e);
container.innerHTML = "<p style='padding:12px;text-align:center;color:var(--accent-danger)'>Data unavailable</p>";
}
}
/* ================= EARTH EVENTS ================= */
async function loadEarthEvents() {
const container = $("earthNewsList");
try {
const res = await fetch("https://eonet.gsfc.nasa.gov/api/v2.1/events?limit=10");
if (!res.ok) throw new Error("EONET unavailable");
const data = await res.json();
container.innerHTML = "";
if (data.events && data.events.length > 0) {
data.events.forEach(ev => {
const item = document.createElement("div");
item.className = "news-item";
const cat = ev.categories[0]?.title || "Event";
item.innerHTML = `
<div class="news-title" onclick="window.open('${ev.sources[0]?.url || '#'}', '_blank')">
🌍 ${ev.title}
</div>
<div class="news-meta">${cat}${new Date(ev.geometries[0].date).toLocaleDateString()}</div>
`;
container.appendChild(item);
});
} else {
container.innerHTML = "<p style='padding:12px;text-align:center;opacity:0.7'>No recent events</p>";
}
} catch(e) {
console.warn("EONET Error:", e);
container.innerHTML = "<p style='padding:12px;text-align:center;color:var(--accent-danger)'>Data unavailable</p>";
}
}
/* ================= IMAGE SEARCH ================= */
async function searchNasaImages() {
const query = $("nasaSearchInput").value.trim();
if(!query) {
showError("Please enter a search term");
return;
}
const container = $("nasaImageResults");
container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--text-muted)'>Searching NASA archives...</p>";
try {
const res = await fetch(`https://images-api.nasa.gov/search?q=${encodeURIComponent(query)}&media_type=image`);
if (!res.ok) throw new Error("Search failed");
const data = await res.json();
const items = data.collection?.items?.slice(0, 12) || [];
container.innerHTML = "";
if(items.length === 0) {
container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--text-muted)'>No images found. Try different keywords.</p>";
return;
}
items.forEach(item => {
const imgUrl = item.links?.[0]?.href;
const title = item.data?.[0]?.title || "NASA Image";
if (!imgUrl) return;
const div = document.createElement("div");
div.className = "img-result";
div.title = title;
div.innerHTML = `<img src="${imgUrl}" alt="${title}" loading="lazy">`;
div.onclick = () => window.open(imgUrl, '_blank');
container.appendChild(div);
});
} catch(e) {
console.error("Image Search Error:", e);
container.innerHTML = "<p style='padding:16px;text-align:center;color:var(--accent-danger)'>Search failed. Please try again.</p>";
}
}
/* ================= 3D SOLAR SYSTEM ================= */
let scene, camera, renderer, animationId, solarControls;
const planets = [];
function openSolarSystem() {
$("solarModal").classList.add("active");
if(!scene) initSolarSystem();
else animateSolar();
}
function closeSolarSystem() {
$("solarModal").classList.remove("active");
if(animationId) cancelAnimationFrame(animationId);
}
function initSolarSystem() {
const container = $("solarContainer");
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 25, 110);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
solarControls = new THREE.OrbitControls(camera, renderer.domElement);
solarControls.enableDamping = true;
solarControls.dampingFactor = 0.05;
const ambientLight = new THREE.AmbientLight(0x404040, 0.8);
scene.add(ambientLight);
const sunLight = new THREE.PointLight(0xffffff, 2.5, 350);
scene.add(sunLight);
// Starfield
const starGeo = new THREE.BufferGeometry();
const starCount = 3000;
const posArray = new Float32Array(starCount * 3);
for(let i=0; i<starCount*3; i++) {
posArray[i] = (Math.random() - 0.5) * 1000;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 0.7});
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
const textureLoader = new THREE.TextureLoader();
const createPlanet = (size, textureUrl, distance, speed) => {
const geometry = new THREE.SphereGeometry(size, 32, 32);
const material = new THREE.MeshStandardMaterial({
map: textureLoader.load(textureUrl),
});
const mesh = new THREE.Mesh(geometry, material);
const obj = new THREE.Object3D();
obj.add(mesh);
mesh.position.x = distance;
scene.add(obj);
return { mesh, obj, speed, selfSpeed: Math.random() * 0.02 + 0.01 };
};
// Sun
const sunGeo = new THREE.SphereGeometry(12, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({
map: textureLoader.load('https://threejs.org/examples/textures/planets/sun.jpg'),
emissive: 0xffaa00,
emissiveIntensity: 0.5
});
const sun = new THREE.Mesh(sunGeo, sunMat);
scene.add(sun);
// Planets
planets.push(createPlanet(1.5, 'https://threejs.org/examples/textures/planets/mercury.jpg', 20, 0.024));
planets.push(createPlanet(2.8, 'https://threejs.org/examples/textures/planets/venus_atmos_2048.jpg', 32, 0.018));
planets.push(createPlanet(3.0, 'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg', 44, 0.012));
planets.push(createPlanet(2.2, 'https://threejs.org/examples/textures/planets/mars_1k_color.jpg', 56, 0.01));
planets.push(createPlanet(6.0, 'https://threejs.org/examples/textures/planets/jupiter2_1k.jpg', 80, 0.005));
window.addEventListener('resize', onWindowResize, false);
animateSolar();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animateSolar() {
animationId = requestAnimationFrame(animateSolar);
planets.forEach(p => {
p.obj.rotation.y += p.speed;
p.mesh.rotation.y += p.selfSpeed;
});
solarControls.update();
renderer.render(scene, camera);
}
/* ================= THEME TOGGLE ================= */
function toggleTheme() {
document.body.classList.toggle("light-mode");
}
// Initialize on load
window.addEventListener('DOMContentLoaded', () => {
console.log("🚀 ProWeather + NASA Portal loaded successfully!");
});
</script>
</body>
</html>