File size: 8,225 Bytes
be02369 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
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) => {
// webkitCompassHeading is for iOS
const heading = (event as any).webkitCompassHeading || (360 - event.alpha!);
setCompassHeading(heading);
};
const requestPermissions = useCallback(async () => {
setPermission('prompting');
// Geolocation
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 });
// Device Orientation (Compass)
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;
|