<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Search</title>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script src="/_sdk/element_sdk.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { font-family: 'Outfit', sans-serif; }
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); }
50% { box-shadow: 0 0 40px rgba(99, 102, 241, 0.6); }
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.animate-float { animation: float 6s ease-in-out infinite; }
.animate-float-delayed { animation: float 6s ease-in-out infinite 2s; }
.animate-pulse-glow { animation: pulse-glow 3s ease-in-out infinite; }
.animate-gradient {
background-size: 200% 200%;
animation: gradient-shift 8s ease infinite;
}
.animate-spin-slow { animation: spin-slow 20s linear infinite; }
.glass-card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.weather-card {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(168, 85, 247, 0.2) 100%);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.forecast-card {
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.input-glow:focus {
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3), 0 0 30px rgba(99, 102, 241, 0.2);
}
.blob {
border-radius: 40% 60% 70% 30% / 40% 50% 60% 50%;
}
.wind-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
position: relative;
}
.wind-dial {
position: relative;
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid rgba(147, 197, 253, 0.3);
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.1);
}
.wind-arrow-small {
width: 24px;
height: 24px;
transition: transform 0.5s ease;
}
</style>
<style>body { box-sizing: border-box; }</style>
<script src="/_sdk/data_sdk.js" type="text/javascript"></script>
</head>
<body class="h-full overflow-auto">
<div id="app-wrapper" class="min-h-full w-full bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 animate-gradient relative overflow-hidden"><!-- Animated Background Elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-10 left-10 w-72 h-72 bg-indigo-500/20 blob animate-float blur-3xl"></div>
<div class="absolute bottom-20 right-10 w-96 h-96 bg-purple-500/20 blob animate-float-delayed blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-gradient-to-r from-indigo-500/10 to-purple-500/10 rounded-full animate-spin-slow blur-3xl"></div>
</div>
<div class="relative z-10 px-4 py-8 md:py-12 max-w-4xl mx-auto"><!-- Header -->
<header class="text-center mb-10">
<div class="inline-flex items-center gap-3 mb-4">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/30">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
</div>
</div>
<h1 id="app-title" class="text-4xl md:text-5xl font-bold bg-gradient-to-r from-white via-purple-200 to-indigo-200 bg-clip-text text-transparent mb-3">Weather Search</h1>
<p class="text-purple-200/70 text-lg">Enter a US zip code to get current conditions</p>
</header><!-- Search Form -->
<form id="search-form" class="mb-8">
<div class="flex flex-col gap-3">
<div class="glass-card rounded-3xl p-2"><input type="text" id="zip-input" inputmode="numeric" pattern="[0-9]{5}" maxlength="5" placeholder="Enter zip code (e.g., 90210)" class="w-full bg-white/5 border-0 rounded-2xl px-6 py-4 text-white placeholder-purple-300/50 text-lg focus:outline-none input-glow transition-all duration-300" aria-label="Zip code">
</div><button type="submit" id="search-btn" class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white px-8 py-4 rounded-2xl font-semibold text-lg transition-all duration-300 hover:scale-105 active:scale-95 shadow-lg shadow-indigo-500/30 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg><span>Search</span> </button>
</div>
</form><!-- Error Message -->
<div id="error-container" class="hidden mb-6">
<div class="glass-card rounded-2xl p-4 border-red-500/30 bg-red-500/10">
<div class="flex items-center gap-3 text-red-300">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p id="error-message" class="text-sm"></p>
</div>
</div>
</div><!-- Loading State -->
<div id="loading-container" class="hidden">
<div class="weather-card rounded-3xl p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full border-4 border-purple-300/30 border-t-purple-400 animate-spin"></div>
<p class="text-purple-200">Fetching weather data...</p>
</div>
</div><!-- Weather Results -->
<div id="weather-container" class="hidden">
<div class="weather-card rounded-3xl p-6 md:p-8 animate-pulse-glow mb-8"><!-- Location Header -->
<div class="text-center mb-8">
<div class="inline-flex items-center gap-2 bg-white/10 rounded-full px-4 py-2 mb-4">
<svg class="w-4 h-4 text-purple-300" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg><span id="location-name" class="text-purple-200 text-sm font-medium"></span>
</div>
<div id="temperature" class="text-7xl md:text-8xl font-bold bg-gradient-to-r from-white to-purple-200 bg-clip-text text-transparent"></div>
<p class="text-purple-300/70 mt-2">Current Temperature</p>
</div><!-- Weather Stats Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"><!-- Wind Direction Card -->
<div class="glass-card rounded-2xl p-5 group hover:bg-white/10 transition-all duration-300">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-cyan-400/20 to-blue-500/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<svg id="wind-arrow" class="w-8 h-8 text-cyan-300 transition-transform duration-500" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</div>
<div>
<p class="text-purple-300/70 text-sm mb-1">Wind Direction</p>
<p id="wind-direction" class="text-2xl font-bold text-white"></p>
<p id="wind-speed" class="text-sm text-purple-300/50"></p>
</div>
</div>
</div><!-- Barometric Pressure Card -->
<div class="glass-card rounded-2xl p-5 group hover:bg-white/10 transition-all duration-300">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-amber-400/20 to-orange-500/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<svg class="w-8 h-8 text-amber-300" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<p class="text-purple-300/70 text-sm mb-1">Barometric Pressure</p>
<p id="pressure" class="text-2xl font-bold text-white"></p>
<p id="pressure-trend" class="text-sm text-purple-300/50"></p>
</div>
</div>
</div>
</div><!-- Last Updated -->
<div class="mt-6 pt-4 border-t border-white/10 text-center">
<p id="last-updated" class="text-purple-300/50 text-sm"></p>
</div>
</div><!-- 4-Day Forecast -->
<div>
<h2 class="text-2xl font-bold text-white mb-4">4-Day Forecast</h2>
<div id="forecast-container" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Forecast cards will be populated here -->
</div>
</div>
</div><!-- Empty State -->
<div id="empty-state" class="text-center py-12">
<div class="w-24 h-24 mx-auto mb-6 rounded-full bg-gradient-to-br from-indigo-500/20 to-purple-500/20 flex items-center justify-center">
<svg class="w-12 h-12 text-purple-300/50" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<h3 class="text-xl font-semibold text-purple-200 mb-2">Enter a Zip Code</h3>
<p class="text-purple-300/50">Search for current weather conditions in any US location</p>
</div>
</div>
</div>
<script>
// Default configuration
const defaultConfig = {
app_title: 'Weather Search',
search_placeholder: 'Enter zip code (e.g., 90210)',
background_color: '#0f172a',
surface_color: 'rgba(255, 255, 255, 0.08)',
text_color: '#e9d5ff',
primary_action_color: '#6366f1',
secondary_action_color: '#a855f7',
font_family: 'Outfit',
font_size: 16
};
// Initialize Element SDK
if (window.elementSdk) {
window.elementSdk.init({
defaultConfig,
onConfigChange: async (config) => {
const title = config.app_title || defaultConfig.app_title;
const placeholder = config.search_placeholder || defaultConfig.search_placeholder;
const fontFamily = config.font_family || defaultConfig.font_family;
const fontSize = config.font_size || defaultConfig.font_size;
const primaryColor = config.primary_action_color || defaultConfig.primary_action_color;
const secondaryColor = config.secondary_action_color || defaultConfig.secondary_action_color;
document.getElementById('app-title').textContent = title;
document.getElementById('zip-input').placeholder = placeholder;
// Apply font
document.body.style.fontFamily = `${fontFamily}, Outfit, sans-serif`;
document.getElementById('app-title').style.fontSize = `${fontSize * 2.5}px`;
// Apply colors to buttons
const searchBtn = document.getElementById('search-btn');
searchBtn.style.background = `linear-gradient(to right, ${primaryColor}, ${secondaryColor})`;
},
mapToCapabilities: (config) => ({
recolorables: [
{
get: () => config.primary_action_color || defaultConfig.primary_action_color,
set: (value) => { config.primary_action_color = value; window.elementSdk.setConfig({ primary_action_color: value }); }
},
{
get: () => config.secondary_action_color || defaultConfig.secondary_action_color,
set: (value) => { config.secondary_action_color = value; window.elementSdk.setConfig({ secondary_action_color: value }); }
}
],
borderables: [],
fontEditable: {
get: () => config.font_family || defaultConfig.font_family,
set: (value) => { config.font_family = value; window.elementSdk.setConfig({ font_family: value }); }
},
fontSizeable: {
get: () => config.font_size || defaultConfig.font_size,
set: (value) => { config.font_size = value; window.elementSdk.setConfig({ font_size: value }); }
}
}),
mapToEditPanelValues: (config) => new Map([
['app_title', config.app_title || defaultConfig.app_title],
['search_placeholder', config.search_placeholder || defaultConfig.search_placeholder]
])
});
}
// DOM Elements
const form = document.getElementById('search-form');
const zipInput = document.getElementById('zip-input');
const searchBtn = document.getElementById('search-btn');
const loadingContainer = document.getElementById('loading-container');
const weatherContainer = document.getElementById('weather-container');
const errorContainer = document.getElementById('error-container');
const errorMessage = document.getElementById('error-message');
const emptyState = document.getElementById('empty-state');
const forecastContainer = document.getElementById('forecast-container');
// Convert degrees to compass direction
function degreesToDirection(degrees) {
const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const index = Math.round(degrees / 22.5) % 16;
return directions[index];
}
// Get pressure trend description
function getPressureTrend(pressure) {
if (pressure > 1020) return 'High pressure - Fair weather';
if (pressure < 1000) return 'Low pressure - Stormy';
return 'Normal pressure - Stable';
}
// Format time
function formatTime(isoString) {
const date = new Date(isoString);
return date.toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
}
// Format date for forecast
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
}
// Show error
function showError(message) {
errorMessage.textContent = message;
errorContainer.classList.remove('hidden');
loadingContainer.classList.add('hidden');
weatherContainer.classList.add('hidden');
emptyState.classList.add('hidden');
}
// Hide error
function hideError() {
errorContainer.classList.add('hidden');
}
// Set loading state
function setLoading(isLoading) {
if (isLoading) {
loadingContainer.classList.remove('hidden');
weatherContainer.classList.add('hidden');
emptyState.classList.add('hidden');
searchBtn.disabled = true;
} else {
loadingContainer.classList.add('hidden');
searchBtn.disabled = false;
}
}
// Create forecast card
function createForecastCard(date, tempMax, tempMin, windDir, windSpeed) {
const card = document.createElement('div');
card.className = 'forecast-card rounded-2xl p-4 text-center hover:bg-white/10 transition-all duration-300';
const directionLabel = degreesToDirection(windDir);
card.innerHTML = `
<p class="text-purple-300 text-sm font-medium mb-3">${date}</p>
<div class="mb-4">
<p class="text-2xl font-bold text-white mb-1">${Math.round(tempMax)}°</p>
<p class="text-sm text-purple-300/60">${Math.round(tempMin)}°</p>
</div>
<div class="wind-indicator mb-3">
<div class="wind-dial">
<svg class="wind-arrow-small text-cyan-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="transform: rotate(${windDir + 180}deg);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</div>
</div>
<p class="text-lg font-semibold text-cyan-300 mb-1">${directionLabel}</p>
<p class="text-xs text-purple-300/50">${Math.round(windSpeed)} mph</p>
`;
return card;
}
// Fetch weather data
async function fetchWeather(zipCode) {
hideError();
setLoading(true);
try {
// First, geocode the zip code using Open-Meteo's geocoding API
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${zipCode}&count=1&language=en&format=json&country=US`
);
if (!geoResponse.ok) {
throw new Error('Unable to connect to weather service');
}
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
// Try with zip code as postal code search
const postalResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${zipCode}&count=10&language=en&format=json`
);
const postalData = await postalResponse.json();
// Filter for US results
const usResult = postalData.results?.find(r => r.country_code === 'US');
if (!usResult) {
throw new Error('Could not find location for this zip code. Please try a city name or different zip code.');
}
geoData.results = [usResult];
}
const location = geoData.results[0];
const { latitude, longitude, name, admin1 } = location;
// Fetch weather data with 4-day forecast
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,wind_direction_10m,surface_pressure&daily=temperature_2m_max,temperature_2m_min,wind_direction_10m_dominant,wind_speed_10m_max&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`
);
if (!weatherResponse.ok) {
throw new Error('Unable to fetch weather data');
}
const weatherData = await weatherResponse.json();
const current = weatherData.current;
const daily = weatherData.daily;
// Update current weather UI
document.getElementById('location-name').textContent = `${name}${admin1 ? `, ${admin1}` : ''}`;
document.getElementById('temperature').textContent = `${Math.round(current.temperature_2m)}°F`;
const windDir = degreesToDirection(current.wind_direction_10m);
document.getElementById('wind-direction').textContent = windDir;
document.getElementById('wind-speed').textContent = `${Math.round(current.wind_speed_10m)} mph`;
// Rotate wind arrow
const windArrow = document.getElementById('wind-arrow');
windArrow.style.transform = `rotate(${current.wind_direction_10m + 180}deg)`;
const pressure = Math.round(current.surface_pressure);
document.getElementById('pressure').textContent = `${pressure} hPa`;
document.getElementById('pressure-trend').textContent = getPressureTrend(pressure);
document.getElementById('last-updated').textContent = `Last updated: ${formatTime(current.time)}`;
// Populate 4-day forecast
forecastContainer.innerHTML = '';
for (let i = 0; i < 4; i++) {
const card = createForecastCard(
formatDate(daily.time[i]),
daily.temperature_2m_max[i],
daily.temperature_2m_min[i],
daily.wind_direction_10m_dominant[i],
daily.wind_speed_10m_max[i]
);
forecastContainer.appendChild(card);
}
setLoading(false);
weatherContainer.classList.remove('hidden');
emptyState.classList.add('hidden');
} catch (error) {
setLoading(false);
showError(error.message || 'An error occurred while fetching weather data');
}
}
// Form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
const zipCode = zipInput.value.trim();
if (!zipCode) {
showError('Please enter a zip code');
return;
}
if (!/^\d{5}$/.test(zipCode)) {
showError('Please enter a valid 5-digit US zip code');
return;
}
fetchWeather(zipCode);
});
// Allow only numeric input
zipInput.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 5);
});
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9d1ec77151a41098',t:'MTc3MTc2NjQwOC4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>