<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LuxHarbor Weather</title>
<!-- 1. Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 2. Import Map for React modules -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client",
"lucide-react": "https://esm.sh/[email protected]"
}
}
</script>
<!-- 3. Babel for JSX compilation -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
/* Custom scrollbar hiding utility */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</head>
<body class="bg-slate-950 text-slate-200">
<div id="root"></div>
<!-- 4. Your Application Code -->
<script type="text/babel" data-type="module">
import React, { useState, useEffect, useMemo } from 'react';
import { createRoot } from 'react-dom/client';
import {
Search, MapPin, Cloud, Sun, CloudSun, CloudRain, Wind,
Droplets, Thermometer, ArrowRight, Copy, Check, Menu, X,
ChevronDown, ChevronUp, Eye, Sunrise, Sunset, Umbrella,
Navigation, Share2, Code, Layout, Palette, Settings, MoreHorizontal
} from 'lucide-react';
/**
* --- MOCK DATA GENERATORS & MODELS ---
*/
const CONDITIONS = [
{ text: "Sunny", icon: Sun, color: "text-yellow-400" },
{ text: "Partly Cloudy", icon: CloudSun, color: "text-yellow-200" },
{ text: "Cloudy", icon: Cloud, color: "text-gray-400" },
{ text: "Rain Shower", icon: CloudRain, color: "text-blue-400" },
{ text: "Heavy Rain", icon: CloudRain, color: "text-blue-600" },
];
const generateWeather = (city) => {
const currentCond = CONDITIONS[Math.floor(Math.random() * CONDITIONS.length)];
// Current Data
const current = {
temp: 72,
feels_like: 75,
condition_text: currentCond.text,
icon: currentCond.icon,
iconColor: currentCond.color,
high: 78,
low: 65,
wind_speed: "8 mph",
wind_dir: "NW",
humidity: "45%",
dew_point: "58°",
pressure: "29.92 in",
uv_index: "6 of 10",
visibility: "10 mi",
sunrise: "6:23 am",
sunset: "8:14 pm",
moon_phase: "Waxing Gibbous",
outlook_text: `Expect ${currentCond.text.toLowerCase()} conditions to continue until late afternoon. Wind gusts may increase slightly.`
};
// Hourly Data (Next 24h)
const hourly = Array.from({ length: 24 }, (_, i) => {
const hour = (new Date().getHours() + i) % 24;
const timeLabel = hour === 0 ? "12 am" : hour > 12 ? `${hour - 12} pm` : `${hour} am`;
const cond = CONDITIONS[Math.floor(Math.random() * CONDITIONS.length)];
return {
timestamp: i,
time: timeLabel,
temp: 72 - Math.floor(Math.random() * 10) + (i > 6 && i < 18 ? 10 : 0),
condition_text: cond.text,
icon: cond.icon,
precip_chance: Math.floor(Math.random() * 30) + "%",
wind: `${5 + Math.floor(Math.random() * 10)} mph ${['N','NE','E','SE','S','SW','W','NW'][Math.floor(Math.random()*8)]}`,
humidity: `${40 + Math.floor(Math.random() * 40)}%`,
feels_like: `${70 + Math.floor(Math.random() * 10)}°`,
uv_index: i > 6 && i < 18 ? Math.floor(Math.random() * 8) : 0,
cloud_cover: `${Math.floor(Math.random() * 100)}%`
};
});
// Daily Data (10 Days)
const days = ["Today", "Fri 12", "Sat 13", "Sun 14", "Mon 15", "Tue 16", "Wed 17", "Thu 18", "Fri 19", "Sat 20"];
const daily = days.map((day, i) => {
const cond = CONDITIONS[Math.floor(Math.random() * CONDITIONS.length)];
return {
date: day,
high: 70 + Math.floor(Math.random() * 15),
low: 55 + Math.floor(Math.random() * 10),
condition_text: cond.text,
icon: cond.icon,
precip_chance: Math.floor(Math.random() * 60) + "%",
wind: `${5 + Math.floor(Math.random() * 15)} mph`,
humidity: `${50 + Math.floor(Math.random() * 30)}%`,
sunrise: "6:25 am",
sunset: "8:10 pm"
};
});
return { location: { name: city, id: city.toLowerCase() }, current, hourly, daily };
};
const SAMPLE_PROMPTS = [
{
id: 'p1',
title: 'Morning Walk Planner',
description: 'Determine the best time for a walk based on conditions.',
category: 'Daily Planning',
tags: ['health', 'active'],
template: "Analyze the forecast for {{city}}. Given the current temp of {{temp}} and {{condition}}, suggest the best 1-hour window for a walk today.",
},
{
id: 'p2',
title: 'Travel Commute Risk',
description: 'Assess driving risks for the next 24 hours.',
category: 'Travel',
tags: ['safety', 'commute'],
template: "I am driving in {{city}}. Review the hourly forecast. Are there any visibility ({{visibility}}) or precipitation risks I should know about?",
},
{
id: 'p3',
title: 'Event Go/No-Go',
description: 'Decision helper for outdoor events.',
category: 'Events',
tags: ['planning', 'outdoor'],
template: "I have an outdoor event planned in {{city}}. With a high of {{high}} and precip chance of {{precip}}, should I move it indoors?",
},
{
id: 'p4',
title: 'Allergy & Air Quality',
description: 'Check air quality impact on health.',
category: 'Health',
tags: ['medical', 'wellness'],
template: "Check the current humidity ({{humidity}}) and wind ({{wind}}) in {{city}}. Advise on allergy risks for today.",
},
];
/**
* --- UTILITIES ---
*/
const cn = (...classes) => classes.filter(Boolean).join(' ');
/**
* --- COMPONENTS ---
*/
// 1. HEADER
const Header = ({ activeTab, onTabChange, onSearch, searchQuery, setSearchQuery, onOpenWidget }) => (
<header className="bg-slate-900 text-white shadow-lg sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4">
{/* Top Bar */}
<div className="flex flex-col md:flex-row items-center justify-between py-3 gap-4">
{/* Logo */}
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shadow-blue-500/20 shadow-lg">
<CloudSun size={20} strokeWidth={2.5} />
</div>
<span className="text-xl font-bold tracking-tight">LuxHarbor <span className="text-blue-400 font-light">Weather</span></span>
</div>
{/* Search */}
<form onSubmit={(e) => { e.preventDefault(); onSearch(searchQuery); }} className="relative w-full md:w-96 group">
<input
type="text"
placeholder="Search city or ZIP..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-800 border border-slate-700 text-slate-100 rounded-full py-2 pl-10 pr-4 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
/>
<Search className="absolute left-3 top-2.5 text-slate-400 h-4 w-4" />
</form>
{/* Actions - Hidden for visitor */}
<div className="hidden md:flex w-8"></div>
</div>
{/* Navigation Tabs */}
<nav className="flex items-center gap-1 md:gap-6 overflow-x-auto scrollbar-hide -mb-px pt-2">
{['Today', 'Hourly', '10-Day', 'Radar', 'Prompt Library'].map(tab => (
<button
key={tab}
onClick={() => onTabChange(tab)}
className={cn(
"px-2 md:px-0 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap",
activeTab === tab
? "border-blue-500 text-white"
: "border-transparent text-slate-400 hover:text-slate-200 hover:border-slate-700"
)}
>
{tab}
</button>
))}
</nav>
</div>
</header>
);
// 2. TODAY VIEW
const TodayView = ({ data, onNavigate }) => {
const { current, location } = data;
const Icon = current.icon;
return (
<div className="space-y-6 animate-in fade-in duration-500">
{/* Location Header */}
<div className="flex justify-between items-baseline">
<h2 className="text-2xl font-bold text-slate-100 flex items-center gap-2">
<MapPin size={24} className="text-blue-500" />
{location.name}, US
</h2>
<span className="text-slate-400 text-sm">As of {new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})} EST</span>
</div>
{/* Main Current Conditions Card */}
<div className="bg-slate-800 rounded-xl p-6 md:p-8 shadow-xl border border-slate-700/50">
<div className="flex flex-col md:flex-row justify-between">
{/* Left: Big Temp & Condition */}
<div className="flex-1 mb-6 md:mb-0">
<div className="flex items-center gap-4 mb-2">
<span className={`text-4xl md:text-5xl font-bold ${current.iconColor}`}>{current.temp}°</span>
<div className="flex flex-col">
<span className="text-xl font-medium text-slate-100">{current.condition_text}</span>
<span className="text-slate-400">Day {current.high}° • Night {current.low}°</span>
</div>
</div>
<div className="mt-4 inline-block bg-slate-700/50 px-3 py-1 rounded-full text-sm font-medium text-blue-200">
Feels Like {current.feels_like}
</div>
</div>
{/* Right: Grid of Metrics */}
<div className="flex-1 grid grid-cols-2 gap-y-4 gap-x-8 text-sm border-t md:border-t-0 md:border-l border-slate-700 pt-6 md:pt-0 md:pl-8">
<MetricRow icon={Wind} label="Wind" value={`${current.wind_dir} ${current.wind_speed}`} />
<MetricRow icon={Droplets} label="Humidity" value={current.humidity} />
<MetricRow icon={Sun} label="UV Index" value={current.uv_index} />
<MetricRow icon={Eye} label="Visibility" value={current.visibility} />
<MetricRow icon={Thermometer} label="Dew Point" value={current.dew_point} />
<MetricRow icon={Navigation} label="Pressure" value={current.pressure} />
<MetricRow icon={Sunrise} label="Sunrise" value={current.sunrise} />
<MetricRow icon={Sunset} label="Sunset" value={current.sunset} />
</div>
</div>
</div>
{/* Outlook Strip */}
<div className="bg-blue-900/20 border border-blue-900/50 p-4 rounded-lg flex items-start gap-3">
<InfoIcon className="text-blue-400 shrink-0 mt-0.5" size={18} />
<div>
<h4 className="font-semibold text-blue-100 text-sm uppercase tracking-wide mb-1">Outlook</h4>
<p className="text-blue-200 text-sm">{current.outlook_text}</p>
</div>
</div>
<button
onClick={() => onNavigate('Hourly')}
className="w-full py-3 bg-slate-800 hover:bg-slate-700 text-blue-400 font-medium rounded-lg border border-slate-700 transition-colors flex items-center justify-center gap-2"
>
View Hourly Forecast <ArrowRight size={16} />
</button>
{/* Content Blocks */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4">
<ContentCard title="Travel & Commute" type="travel">
Roads are dry with good visibility. No major weather-related delays expected on I-95 corridor this evening.
</ContentCard>
<ContentCard title="Health & Allergies" type="health">
Air quality is Moderate. Pollen counts are low, but sensitive groups should limit prolonged outdoor exertion.
</ContentCard>
</div>
<FooterNote />
</div>
);
};
// 3. HOURLY VIEW
const HourlyView = ({ data }) => {
const [expandedRow, setExpandedRow] = useState(null);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-slate-100">Hourly Forecast</h2>
<span className="text-slate-400 text-sm">Next 24 Hours</span>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-xl overflow-hidden shadow-xl">
{data.hourly.map((hour, idx) => {
const isExpanded = expandedRow === idx;
const Icon = hour.icon;
return (
<div key={idx} className="border-b border-slate-700/50 last:border-0">
{/* Summary Row */}
<div
onClick={() => setExpandedRow(isExpanded ? null : idx)}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-slate-700/30 transition-colors"
>
<div className="w-20 font-medium text-slate-200">{hour.time}</div>
<div className="flex items-center gap-2 w-16 text-2xl font-bold text-slate-100">{hour.temp}°</div>
<div className="flex items-center gap-3 flex-1 px-4">
<Icon size={20} className="text-slate-400" />
<span className="text-slate-300 text-sm hidden md:block">{hour.condition_text}</span>
</div>
<div className="hidden md:flex items-center gap-2 w-24 text-blue-400 text-sm">
<Umbrella size={14} /> {hour.precip_chance}
</div>
<div className="hidden md:flex items-center gap-2 w-24 text-slate-400 text-sm">
<Wind size={14} /> {hour.wind}
</div>
<div className="text-slate-500">
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</div>
</div>
{/* Detailed Expansion */}
{isExpanded && (
<div className="bg-slate-900/50 p-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm border-t border-slate-700/50 animate-in slide-in-from-top-1">
<DetailBox label="Feels Like" value={hour.feels_like} />
<DetailBox label="Humidity" value={hour.humidity} />
<DetailBox label="UV Index" value={hour.uv_index} />
<DetailBox label="Cloud Cover" value={hour.cloud_cover} />
<DetailBox label="Wind" value={hour.wind} />
<DetailBox label="Rain Chance" value={hour.precip_chance} />
</div>
)}
</div>
);
})}
</div>
<FooterNote />
</div>
);
};
// 4. 10-DAY VIEW
const TenDayView = ({ data }) => {
const [expandedDay, setExpandedDay] = useState(null);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<h2 className="text-xl font-bold text-slate-100">10-Day Forecast</h2>
<div className="bg-slate-800 border border-slate-700 rounded-xl overflow-hidden shadow-xl">
{data.daily.map((day, idx) => {
const isExpanded = expandedDay === idx;
const Icon = day.icon;
return (
<div key={idx} className="border-b border-slate-700/50 last:border-0">
<div
onClick={() => setExpandedDay(isExpanded ? null : idx)}
className="flex items-center justify-between p-4 cursor-pointer hover:bg-slate-700/30 transition-colors"
>
<div className="w-24 font-medium text-slate-200">{day.date}</div>
<div className="w-20 text-center">
<span className="text-xl font-bold text-slate-100">{day.high}°</span>
<span className="text-slate-500 text-sm ml-2">{day.low}°</span>
</div>
<div className="flex items-center gap-3 flex-1 px-4 justify-center md:justify-start">
<Icon size={20} className="text-slate-400" />
<span className="text-slate-300 text-sm hidden md:block">{day.condition_text}</span>
</div>
<div className="hidden md:flex items-center gap-2 w-24 text-blue-400 text-sm">
<Umbrella size={14} /> {day.precip_chance}
</div>
<div className="text-slate-500">
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</div>
</div>
{isExpanded && (
<div className="bg-slate-900/50 p-4 grid grid-cols-2 gap-4 text-sm border-t border-slate-700/50">
<DetailBox label="Humidity" value={day.humidity} />
<DetailBox label="Wind" value={day.wind} />
<DetailBox label="Sunrise" value={day.sunrise} />
<DetailBox label="Sunset" value={day.sunset} />
<div className="col-span-2 text-slate-400 text-xs mt-2 italic">
LuxHarbor Tip: A great day for outdoor activities in the morning before the heat sets in.
</div>
</div>
)}
</div>
);
})}
</div>
<FooterNote />
</div>
);
};
// 5. PROMPT LIBRARY
const PromptLibrary = ({ prompts, onUse }) => {
const [filter, setFilter] = useState('All');
const filtered = filter === 'All' ? prompts : prompts.filter(p => p.category === filter);
return (
<div className="animate-in fade-in zoom-in duration-300 space-y-6">
<div className="bg-gradient-to-r from-blue-900/50 to-slate-800 p-8 rounded-xl border border-slate-700 text-center">
<h2 className="text-2xl font-bold text-white mb-2">Weather Prompt Library</h2>
<p className="text-blue-200 max-w-xl mx-auto">
AI-ready templates that automatically adapt to the current forecast. Select a prompt to inject live data.
</p>
</div>
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
{['All', 'Daily Planning', 'Travel', 'Events', 'Health'].map(cat => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={cn(
"px-4 py-2 rounded-full text-sm font-medium border transition-colors whitespace-nowrap",
filter === cat
? "bg-blue-600 border-blue-600 text-white shadow-lg shadow-blue-900/20"
: "bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700"
)}
>
{cat}
</button>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filtered.map(prompt => (
<div key={prompt.id} className="bg-slate-800 border border-slate-700 p-6 rounded-xl hover:border-blue-500/50 transition-colors flex flex-col">
<div className="flex justify-between items-start mb-4">
<span className="text-xs font-bold text-blue-400 bg-blue-900/30 px-2 py-1 rounded uppercase tracking-wide">
{prompt.category}
</span>
<Settings size={16} className="text-slate-600" />
</div>
<h3 className="text-lg font-bold text-slate-100 mb-2">{prompt.title}</h3>
<p className="text-slate-400 text-sm mb-6 flex-1">{prompt.description}</p>
<button
onClick={() => onUse(prompt)}
className="w-full py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition-colors flex items-center justify-center gap-2"
>
<Share2 size={16} /> Use with Current Forecast
</button>
</div>
))}
</div>
<FooterNote />
</div>
);
};
// 6. WIDGET BUILDER
const WidgetBuilder = ({ weather }) => {
const [config, setConfig] = useState({
theme: 'dark', // dark, light, transparent
layout: 'sidebar', // sidebar, compact
units: 'F'
});
const embedCode = `<iframe src="https://luxharborweather.com/widget?id=${weather.location.id}&theme=${config.theme}&layout=${config.layout}" width="100%" height="400" frameborder="0"></iframe>`;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-4">
{/* Controls */}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-slate-100 mb-2">Widget Builder</h2>
<p className="text-slate-400">Customize and embed a free weather widget for your site.</p>
</div>
<div className="bg-slate-800 border border-slate-700 rounded-xl p-6 space-y-6">
<div>
<label className="block text-sm font-medium text-slate-300 mb-3 flex items-center gap-2">
<Palette size={16} /> Theme
</label>
<div className="grid grid-cols-3 gap-3">
{['dark', 'light', 'transparent'].map(t => (
<button
key={t}
onClick={() => setConfig({...config, theme: t})}
className={cn(
"py-2 px-4 rounded-lg border text-sm capitalize transition-all",
config.theme === t
? "border-blue-500 bg-blue-500/10 text-blue-400 ring-1 ring-blue-500"
: "border-slate-600 bg-slate-700 text-slate-300 hover:bg-slate-600"
)}
>
{t}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-3 flex items-center gap-2">
<Layout size={16} /> Layout
</label>
<div className="grid grid-cols-2 gap-3">
{['sidebar', 'compact'].map(l => (
<button
key={l}
onClick={() => setConfig({...config, layout: l})}
className={cn(
"py-2 px-4 rounded-lg border text-sm capitalize transition-all",
config.layout === l
? "border-blue-500 bg-blue-500/10 text-blue-400 ring-1 ring-blue-500"
: "border-slate-600 bg-slate-700 text-slate-300 hover:bg-slate-600"
)}
>
{l}
</button>
))}
</div>
</div>
</div>
<div className="bg-slate-900 rounded-xl border border-slate-800 p-4 relative group">
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => navigator.clipboard.writeText(embedCode)}
className="bg-blue-600 text-white text-xs px-2 py-1 rounded hover:bg-blue-500"
>
Copy
</button>
</div>
<pre className="text-slate-400 font-mono text-xs whitespace-pre-wrap break-all">
{embedCode}
</pre>
</div>
</div>
{/* Live Preview */}
<div className="bg-slate-900/50 rounded-xl border-2 border-dashed border-slate-700 p-8 flex items-center justify-center">
{/* Mock Widget Iframe */}
<div className={cn(
"rounded-lg overflow-hidden shadow-2xl transition-all duration-300",
config.layout === 'sidebar' ? "w-64" : "w-full max-w-sm",
config.theme === 'light' ? "bg-white text-slate-900" : config.theme === 'transparent' ? "bg-black/40 text-white backdrop-blur-md border border-white/10" : "bg-slate-800 text-white"
)}>
<div className="p-4 border-b border-current/10 flex justify-between items-center">
<span className="font-bold text-sm">Baltimore, US</span>
<CloudSun size={16} />
</div>
<div className="p-6 text-center">
<div className="text-4xl font-bold mb-1">72°</div>
<div className="text-sm opacity-80">Partly Cloudy</div>
</div>
{config.layout === 'sidebar' && (
<div className="bg-current/5 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>Tomorrow</span> <span>76°/62°</span></div>
<div className="flex justify-between"><span>Saturday</span> <span>70°/55°</span></div>
<div className="flex justify-between"><span>Sunday</span> <span>68°/54°</span></div>
</div>
)}
<div className="p-2 text-[10px] text-center opacity-50 bg-current/5">
Powered by LuxHarbor
</div>
</div>
</div>
</div>
);
};
// HELPER COMPONENTS
const MetricRow = ({ icon: Icon, label, value }) => (
<div className="flex items-center gap-3 py-2 border-b border-slate-700/50 last:border-0">
<Icon className="text-blue-500 shrink-0" size={18} />
<span className="text-slate-400 flex-1">{label}</span>
<span className="text-slate-200 font-medium">{value}</span>
</div>
);
const DetailBox = ({ label, value }) => (
<div className="bg-slate-800/50 p-2 rounded border border-slate-700/50">
<span className="block text-slate-500 text-xs uppercase mb-1">{label}</span>
<span className="block text-slate-200 font-medium">{value}</span>
</div>
);
const ContentCard = ({ title, children, type }) => (
<div className="bg-slate-800 border border-slate-700 p-5 rounded-xl">
<h3 className="font-bold text-slate-100 mb-3 flex items-center gap-2">
{type === 'travel' ? <Navigation size={18} className="text-blue-400" /> : <Thermometer size={18} className="text-red-400" />}
{title}
</h3>
<p className="text-slate-400 text-sm leading-relaxed">{children}</p>
</div>
);
const FooterNote = () => (
<div className="text-center text-xs text-slate-600 pt-8 pb-4">
Demo data only; connect to your preferred weather API for production use. <br/>
© 2025 LuxHarbor Weather.
</div>
);
const InfoIcon = ({ className, size }) => <div className={className}><svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></div>;
const PromptModal = ({ isOpen, onClose, prompt, weather }) => {
if (!isOpen) return null;
const interpolated = prompt.template
.replace('{{city}}', weather.location.name)
.replace('{{temp}}', weather.current.temp + '°')
.replace('{{condition}}', weather.current.condition_text)
.replace('{{humidity}}', weather.current.humidity)
.replace('{{wind}}', weather.current.wind_speed)
.replace('{{high}}', weather.current.high + '°')
.replace('{{precip}}', weather.daily[0].precip_chance)
.replace('{{visibility}}', weather.current.visibility);
return (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-slate-900 border border-slate-700 w-full max-w-lg rounded-xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-4 border-b border-slate-700 flex justify-between items-center bg-slate-800/50">
<h3 className="text-slate-100 font-bold">Use Prompt</h3>
<button onClick={onClose}><X size={20} className="text-slate-400 hover:text-white" /></button>
</div>
<div className="p-6 space-y-4">
<div className="bg-blue-900/20 border border-blue-900/50 p-3 rounded text-xs text-blue-300">
Current weather data for {weather.location.name} has been injected.
</div>
<textarea
readOnly
className="w-full h-32 bg-slate-950 border border-slate-800 rounded-lg p-3 text-slate-300 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
value={interpolated}
/>
<div className="flex gap-3 justify-end">
<button onClick={onClose} className="px-4 py-2 text-slate-400 hover:text-white text-sm">Cancel</button>
<button
onClick={() => {
navigator.clipboard.writeText(interpolated);
onClose();
}}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium flex items-center gap-2"
>
<Copy size={16} /> Copy Prompt
</button>
</div>
</div>
</div>
</div>
);
};
/**
* --- MAIN APP ---
*/
function LuxHarborApp() {
const [activeTab, setActiveTab] = useState('Today');
const [weatherData, setWeatherData] = useState(null);
const [searchQuery, setSearchQuery] = useState('Baltimore');
const [loading, setLoading] = useState(true);
// Modal State
const [modalOpen, setModalOpen] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState(null);
// Initial Load
useEffect(() => {
handleSearch('Baltimore');
}, []);
const handleSearch = (city) => {
setLoading(true);
// Simulate API Fetch
setTimeout(() => {
setWeatherData(generateWeather(city));
setLoading(false);
}, 600);
};
const handleOpenPrompt = (prompt) => {
setSelectedPrompt(prompt);
setModalOpen(true);
};
const renderContent = () => {
if (loading) return (
<div className="flex flex-col items-center justify-center py-32">
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-4"></div>
<div className="text-slate-400 animate-pulse">Fetching forecast data...</div>
</div>
);
switch(activeTab) {
case 'Today': return <TodayView data={weatherData} onNavigate={setActiveTab} />;
case 'Hourly': return <HourlyView data={weatherData} />;
case '10-Day': return <TenDayView data={weatherData} />;
case 'Radar': return (
<div className="bg-slate-800 border border-slate-700 rounded-xl h-96 flex flex-col items-center justify-center text-slate-500 animate-in fade-in">
<MapPin size={48} className="mb-4 opacity-50" />
<h3 className="text-xl font-bold text-slate-300">Interactive Radar</h3>
<p className="text-sm">Map integration coming soon.</p>
</div>
);
case 'Prompt Library': return <PromptLibrary prompts={SAMPLE_PROMPTS} onUse={handleOpenPrompt} />;
case 'Widget': return <WidgetBuilder weather={weatherData} />;
default: return null;
}
};
return (
<div className="min-h-screen bg-slate-950 text-slate-200 font-sans selection:bg-blue-500/30">
<Header
activeTab={activeTab === 'Widget' ? 'Today' : activeTab} // Keep main tab active if widget modal
onTabChange={setActiveTab}
onSearch={handleSearch}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
onOpenWidget={() => setActiveTab('Widget')}
/>
<main className="max-w-4xl mx-auto px-4 py-8">
{renderContent()}
</main>
<PromptModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
prompt={selectedPrompt}
weather={weatherData}
/>
</div>
);
}
// --- RENDER APP ---
const root = createRoot(document.getElementById('root'));
root.render(<LuxHarborApp />);
</script>
</body>
</html>