|
import React, { useState, useEffect, useCallback } from 'react'; |
|
import SunCalc from 'suncalc'; |
|
import { SunClockIcon, AlertTriangleIcon } from '../components/icons'; |
|
|
|
type PermissionStatus = 'idle' | 'prompting' | 'granted' | 'denied'; |
|
|
|
const SunTrackerView: React.FC = () => { |
|
const [permission, setPermission] = useState<PermissionStatus>('idle'); |
|
const [sunData, setSunData] = useState<any>(null); |
|
const [compassHeading, setCompassHeading] = useState<number>(0); |
|
const [error, setError] = useState<string>(''); |
|
|
|
const handleOrientation = (event: DeviceOrientationEvent) => { |
|
|
|
const heading = (event as any).webkitCompassHeading || (360 - event.alpha!); |
|
setCompassHeading(heading); |
|
}; |
|
|
|
const requestPermissions = useCallback(async () => { |
|
setPermission('prompting'); |
|
|
|
|
|
if (!("geolocation" in navigator)) { |
|
setError("Geolocation is not supported by your browser."); |
|
setPermission('denied'); |
|
return; |
|
} |
|
|
|
navigator.geolocation.getCurrentPosition( |
|
(position) => { |
|
const { latitude, longitude } = position.coords; |
|
const now = new Date(); |
|
const times = SunCalc.getTimes(now, latitude, longitude); |
|
const pos = SunCalc.getPosition(now, latitude, longitude); |
|
setSunData({ ...times, ...pos }); |
|
|
|
|
|
if (typeof (DeviceOrientationEvent as any).requestPermission === 'function') { |
|
(DeviceOrientationEvent as any).requestPermission() |
|
.then((response: string) => { |
|
if (response === 'granted') { |
|
window.addEventListener('deviceorientation', handleOrientation); |
|
setPermission('granted'); |
|
} else { |
|
setError("Compass permission denied."); |
|
setPermission('denied'); |
|
} |
|
}); |
|
} else { |
|
window.addEventListener('deviceorientation', handleOrientation); |
|
setPermission('granted'); |
|
} |
|
}, |
|
(error) => { |
|
setError("Geolocation permission denied. Please enable it in your browser settings."); |
|
setPermission('denied'); |
|
} |
|
); |
|
}, []); |
|
|
|
useEffect(() => { |
|
return () => { |
|
window.removeEventListener('deviceorientation', handleOrientation); |
|
}; |
|
}, []); |
|
|
|
const renderCompass = () => ( |
|
<div className="relative w-48 h-48 mx-auto"> |
|
<div className="w-full h-full rounded-full bg-stone-100 border-4 border-stone-200 flex items-center justify-center"> |
|
<div className="absolute top-0 w-px h-full bg-stone-300"></div> |
|
<div className="absolute left-0 h-px w-full bg-stone-300"></div> |
|
<span className="absolute top-1 text-lg font-bold text-red-600">N</span> |
|
<span className="absolute bottom-1 text-lg font-bold text-stone-600">S</span> |
|
<span className="absolute left-2 text-lg font-bold text-stone-600">W</span> |
|
<span className="absolute right-2 text-lg font-bold text-stone-600">E</span> |
|
</div> |
|
{sunData && ( |
|
<div className="absolute inset-0 flex items-center justify-center transform" style={{ transform: `rotate(${-compassHeading}deg)` }}> |
|
<div className="w-8 h-8 bg-yellow-400 rounded-full shadow-lg" |
|
style={{ transform: `rotate(${sunData.azimuth * 180 / Math.PI}deg) translateY(-50px) rotate(${-sunData.azimuth * 180 / Math.PI}deg) `}} |
|
title={`Sun Position: Azimuth ${ (sunData.azimuth * 180 / Math.PI + 180).toFixed(0) }°`} |
|
/> |
|
</div> |
|
)} |
|
<div className="absolute inset-0 flex items-center justify-center transform transition-transform duration-500" style={{ transform: `rotate(${compassHeading}deg)` }}> |
|
<div className="w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-b-16 border-b-red-600 transform -translate-y-12"></div> |
|
</div> |
|
</div> |
|
); |
|
|
|
const renderSunPath = () => { |
|
if (!sunData) return null; |
|
const now = new Date(); |
|
const totalDaylight = sunData.sunset.getTime() - sunData.sunrise.getTime(); |
|
const fromSunrise = now.getTime() - sunData.sunrise.getTime(); |
|
const percentOfDay = Math.max(0, Math.min(1, fromSunrise / totalDaylight)); |
|
|
|
return ( |
|
<div className="relative h-24 w-full"> |
|
<svg viewBox="0 0 200 100" className="w-full h-full"> |
|
<path d="M 10 90 A 90 90 0 0 1 190 90" stroke="#d6d3d1" strokeWidth="4" fill="none" /> |
|
{percentOfDay > 0 && percentOfDay < 1 && ( |
|
<circle |
|
cx={10 + percentOfDay * 180} |
|
cy={90 - Math.sin(percentOfDay * Math.PI) * 80} |
|
r="8" |
|
fill="#facc15" |
|
stroke="#ca8a04" |
|
strokeWidth="2" |
|
/> |
|
)} |
|
</svg> |
|
<div className="absolute top-full w-full flex justify-between text-xs font-semibold text-stone-600"> |
|
<span>{sunData.sunrise.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> |
|
<span>{sunData.sunset.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|
|
return ( |
|
<div className="space-y-8 max-w-2xl mx-auto"> |
|
<header className="text-center"> |
|
<h2 className="text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl flex items-center justify-center gap-3"> |
|
<SunClockIcon className="w-8 h-8 text-yellow-500" /> |
|
Sun Tracker |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Find the optimal light for your trees. Use this tool to see the sun's path and current position. |
|
</p> |
|
</header> |
|
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200"> |
|
{permission === 'granted' ? ( |
|
<div className="space-y-6"> |
|
{renderCompass()} |
|
<div className="text-center"> |
|
<p className="text-lg font-bold text-stone-800">Heading: {compassHeading.toFixed(0)}°</p> |
|
<p className="text-sm text-stone-600">Point the top of your device North</p> |
|
</div> |
|
{renderSunPath()} |
|
</div> |
|
) : ( |
|
<div className="text-center py-8"> |
|
<p className="mb-4 text-stone-700">This tool requires permission to access your device's location and orientation to function.</p> |
|
<button onClick={requestPermissions} disabled={permission === 'prompting'} className="bg-yellow-500 text-white font-bold py-3 px-6 rounded-lg hover:bg-yellow-600 transition-colors disabled:bg-stone-400"> |
|
{permission === 'prompting' ? 'Waiting for Permission...' : 'Activate Sun Tracker'} |
|
</button> |
|
{error && ( |
|
<div className="mt-6 p-4 bg-red-50 text-red-700 rounded-lg flex items-center gap-3"> |
|
<AlertTriangleIcon className="w-6 h-6 flex-shrink-0" /> |
|
<p className="text-left">{error}</p> |
|
</div> |
|
)} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default SunTrackerView; |
|
|