| 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; |
|
|