import React, { useState, useEffect } from 'react'; import { Calendar, Clock, Sunset, MapPin, Info, Sun, Cloud, CloudRain, CloudSnow, CloudLightning, CloudFog, CloudSun, Shirt, Sparkles, CloudOff } from 'lucide-react'; // --- 定数・設定 --- // 釧路市の座標 const KUSHIRO_LAT = 42.9849; const KUSHIRO_LNG = 144.3817; // --- 天文学的計算ロジック (簡易版SunCalc) --- const toRad = (angle) => angle * (Math.PI / 180); const toDeg = (angle) => angle * (180 / Math.PI); // 日没時間を計算する関数 const calculateSunset = (date, lat, lng) => { const startOfDay = new Date(date); startOfDay.setHours(0, 0, 0, 0); const start = new Date(startOfDay.getFullYear(), 0, 0); const diff = startOfDay - start; const oneDay = 1000 * 60 * 60 * 24; const dayOfYear = Math.floor(diff / oneDay); const lngHour = lng / 15; const t = dayOfYear + ((18 - lngHour) / 24); const M = (0.9856 * t) - 3.289; let L = M + (1.916 * Math.sin(toRad(M))) + (0.020 * Math.sin(toRad(2 * M))) + 282.634; L = L % 360; if (L < 0) L += 360; let RA = toDeg(Math.atan(0.91764 * Math.tan(toRad(L)))); RA = RA % 360; if (RA < 0) RA += 360; const Lquadrant = (Math.floor(L / 90)) * 90; const RAquadrant = (Math.floor(RA / 90)) * 90; RA = RA + (Lquadrant - RAquadrant); RA = RA / 15; const sinDec = 0.39782 * Math.sin(toRad(L)); const cosDec = Math.cos(Math.asin(sinDec)); const zenith = 90.833; const cosH = (Math.cos(toRad(zenith)) - (sinDec * Math.sin(toRad(lat)))) / (cosDec * Math.cos(toRad(lat))); if (cosH > 1 || cosH < -1) return null; const H = toDeg(Math.acos(cosH)) / 15; const T = H + RA - (0.06571 * t) - 6.622; let UT = T - lngHour; UT = UT % 24; if (UT < 0) UT += 24; let jstHours = UT + 9; if (jstHours >= 24) jstHours -= 24; const hours = Math.floor(jstHours); const minutes = Math.floor((jstHours - hours) * 60); const seconds = Math.floor(((jstHours - hours) * 60 - minutes) * 60); const sunsetDate = new Date(startOfDay); sunsetDate.setHours(hours, minutes, seconds); return sunsetDate; }; // --- 天気関連ユーティリティ --- // WMOコードをアイコンとテキストに変換 const getWeatherInfo = (code) => { switch (true) { case (code === 0): return { icon: , label: "快晴", color: "text-orange-400" }; case (code >= 1 && code <= 3): return { icon: , label: "晴れ・曇り", color: "text-yellow-200" }; case (code === 45 || code === 48): return { icon: , label: "霧", color: "text-gray-300" }; case (code >= 51 && code <= 67): case (code >= 80 && code <= 82): return { icon: , label: "雨", color: "text-blue-300" }; case (code >= 71 && code <= 77): case (code >= 85 && code <= 86): return { icon: , label: "雪", color: "text-white" }; case (code >= 95 && code <= 99): return { icon: , label: "雷雨", color: "text-purple-300" }; default: return { icon: , label: "不明", color: "text-gray-400" }; } }; export default function App() { const [selectedDate, setSelectedDate] = useState(new Date()); const [sunsetTime, setSunsetTime] = useState(null); const [timeUntilSunset, setTimeUntilSunset] = useState(""); const [isPastSunset, setIsPastSunset] = useState(false); // 天気データ用ステート const [weatherData, setWeatherData] = useState(null); const [currentWeather, setCurrentWeather] = useState(null); const [loadingWeather, setLoadingWeather] = useState(false); const formatDateValue = (date) => { return date.toISOString().split('T')[0]; }; const handleDateChange = (e) => { const newDate = new Date(e.target.value); if (!isNaN(newDate.getTime())) { setSelectedDate(newDate); } }; // 日没計算 useEffect(() => { const time = calculateSunset(selectedDate, KUSHIRO_LAT, KUSHIRO_LNG); setSunsetTime(time); }, [selectedDate]); // 天気取得 useEffect(() => { const fetchWeather = async () => { setLoadingWeather(true); try { const response = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${KUSHIRO_LAT}&longitude=${KUSHIRO_LNG}&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FTokyo&past_days=3` ); const data = await response.json(); setWeatherData(data); } catch (error) { console.error("Failed to fetch weather data", error); } finally { setLoadingWeather(false); } }; fetchWeather(); }, []); // 選択日の天気抽出 useEffect(() => { if (!weatherData || !weatherData.daily) { setCurrentWeather(null); return; } const dateStr = formatDateValue(selectedDate); const index = weatherData.daily.time.indexOf(dateStr); if (index !== -1) { setCurrentWeather({ code: weatherData.daily.weather_code[index], maxTemp: weatherData.daily.temperature_2m_max[index], minTemp: weatherData.daily.temperature_2m_min[index], }); } else { setCurrentWeather(null); } }, [selectedDate, weatherData]); // カウントダウン useEffect(() => { if (!sunsetTime) return; const checkTime = () => { const now = new Date(); const isToday = now.toDateString() === selectedDate.toDateString(); if (isToday) { const diff = sunsetTime - now; if (diff > 0) { const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); setTimeUntilSunset(`${hours}時間 ${minutes}分 ${seconds}秒`); setIsPastSunset(false); } else { setTimeUntilSunset("本日の日没は過ぎました"); setIsPastSunset(true); } } else { setTimeUntilSunset(""); setIsPastSunset(false); } }; const timer = setInterval(checkTime, 1000); checkTime(); return () => clearInterval(timer); }, [sunsetTime, selectedDate]); const formatTime = (date) => { if (!date) return "--:--"; return date.toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' }); }; // --- 新機能ロジック --- // 幣舞橋のおすすめ情報判定 (春分・秋分の前後1ヶ月) const getRecommendationMessage = () => { const month = selectedDate.getMonth() + 1; const day = selectedDate.getDate(); // 日付を数値化 (例: 3月20日 -> 320) const dateNum = month * 100 + day; // 春分 (3/20頃) 前後1ヶ月: 2/20 (220) ~ 4/20 (420) if (dateNum >= 220 && dateNum <= 420) { return { text: "幣舞橋から夕日がよく見えます!この時期は橋の延長線上に夕日が沈む絶好のシーズンです。", highlight: true }; } // 秋分 (9/23頃) 前後1ヶ月: 8/23 (823) ~ 10/23 (1023) if (dateNum >= 823 && dateNum <= 1023) { return { text: "幣舞橋から夕日がよく見えます!秋の夕暮れは特にドラマチックです。", highlight: true }; } // 通常メッセージ if (month >= 12 || month <= 2) return { text: "冬の空気は澄んでおり、夕日がくっきりと見えます。", highlight: false }; return { text: "世界三大夕日の一つ、釧路の夕暮れをお楽しみください。", highlight: false }; }; // 気温に基づいた服装アドバイス const getClothingAdvice = () => { if (!currentWeather || currentWeather.minTemp === null) return null; const min = currentWeather.minTemp; const max = currentWeather.maxTemp; if (max < 5) return "極寒です。厚手のダウン、帽子、手袋で完全防備を。"; if (min < 0) return "氷点下の冷え込み。厚手のコートやダウン、マフラーが必須です。"; if (min < 10) return "寒いです。コートやジャケット、ストールがあると安心です。"; if (min < 15) return "肌寒く感じます。セーターやウインドブレーカーを用意しましょう。"; if (min < 20) return "涼しいです。長袖シャツやカーディガンなどの羽織るものを。"; return "比較的過ごしやすいですが、海風が冷たいことがあります。薄手の上着を。"; }; const weatherInfo = currentWeather ? getWeatherInfo(currentWeather.code) : null; const recommendation = getRecommendationMessage(); const clothingAdvice = getClothingAdvice(); return (
{/* Header */}

釧路の夕日と天気

北海道 釧路市

{/* Date Selector */}
{/* Recommendation Banner (Conditional) */}
{recommendation.highlight ? : }

{recommendation.text}

{/* Main Info Grid */}
{/* Sunset Card */}
SUNSET TIME
{sunsetTime ? formatTime(sunsetTime) : "--:--"}
{timeUntilSunset && !isPastSunset && (
あと {timeUntilSunset.split(' ')[0]}時間 {timeUntilSunset.split(' ')[1]}分
)}
{/* Weather Card */} {currentWeather ? (
Forecast
{weatherInfo.label}
High: {currentWeather.maxTemp}° Low: {currentWeather.minTemp}°
{React.cloneElement(weatherInfo.icon, { size: 48 })}
{/* Clothing Advice Section */} {clothingAdvice && (

{clothingAdvice}

)}
) : (
{loadingWeather ? ( <>

Loading...

) : (

天気予報データなし

※ 予報は直近の過去と未来(約2週間)の範囲内でのみ利用可能です。日没時間は引き続き正確に計算されます。

)}
)}
{/* Footer */}

Kushiro Sunset & Weather

); }