Yuki / views /SunTrackerView.tsx
Severian's picture
Upload 43 files
be02369 verified
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;