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 && (
)}
) : (
{loadingWeather ? (
<>
Loading...
>
) : (
天気予報データなし
※ 予報は直近の過去と未来(約2週間)の範囲内でのみ利用可能です。日没時間は引き続き正確に計算されます。
)}
)}
{/* Footer */}
);
}