Initial Commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useTonight } from '../../hooks/useTonight';
|
||||
import { useWeather, useForecast } from '../../hooks/useWeather';
|
||||
import MoonPhaseIcon from '../sky/MoonPhaseIcon';
|
||||
import GoNogo from '../weather/GoNogo';
|
||||
|
||||
const SEEING_LABELS: Record<number, string> = {
|
||||
1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″',
|
||||
5: '1.5″', 6: '2.0″', 7: '2.5″', 8: '>3″',
|
||||
};
|
||||
const TRANSP_LABELS: Record<number, string> = {
|
||||
1: 'Excellent', 2: 'Good', 3: 'Good', 4: 'Average',
|
||||
5: 'Average', 6: 'Poor', 7: 'Poor', 8: 'Bad',
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ path: '/dashboard', label: 'Dashboard', icon: '⬡' },
|
||||
{ path: '/targets', label: 'Targets', icon: '✦' },
|
||||
{ path: '/calendar', label: 'Calendar', icon: '◫' },
|
||||
{ path: '/stats', label: 'Statistics', icon: '▤' },
|
||||
{ path: '/gallery', label: 'Gallery', icon: '⬚' },
|
||||
{ path: '/solar-system', label: 'Solar System', icon: '◉' },
|
||||
{ path: '/settings', label: 'Settings', icon: '⚙' },
|
||||
];
|
||||
|
||||
function fmtTime(utc?: string): string {
|
||||
if (!utc) return '—';
|
||||
return new Date(utc).toLocaleTimeString('fr-FR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'Europe/Paris',
|
||||
});
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
const { data: tonight } = useTonight();
|
||||
const { data: weather } = useWeather();
|
||||
const { data: forecast } = useForecast();
|
||||
|
||||
// First forecast slot = current/nearest 3-hour window
|
||||
const slot = (forecast as { dataseries?: { seeing?: number; transparency?: number; cloudcover?: number }[] })?.dataseries?.[0];
|
||||
|
||||
const darkStart = tonight?.true_dark_start_utc;
|
||||
const darkEnd = tonight?.true_dark_end_utc;
|
||||
const darkStr = darkStart && darkEnd
|
||||
? `${fmtTime(darkStart)}–${fmtTime(darkEnd)}`
|
||||
: '—';
|
||||
|
||||
const dewMargin = weather?.temp_c != null && weather?.dew_point_c != null
|
||||
? (weather.temp_c - weather.dew_point_c).toFixed(1)
|
||||
: null;
|
||||
|
||||
const seeingMap: Record<number, string> = {
|
||||
1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″',
|
||||
5: '1.5″', 6: '2.0″', 7: '2.5″', 8: '>3″',
|
||||
};
|
||||
|
||||
return (
|
||||
<aside style={{
|
||||
width: 220,
|
||||
minWidth: 220,
|
||||
background: 'var(--bg-deep)',
|
||||
borderRight: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{/* Logo */}
|
||||
<div style={{
|
||||
padding: '20px 20px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{
|
||||
fontFamily: 'var(--font-display)',
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
color: 'var(--amber)',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Astronome
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav style={{ padding: '8px 0', flex: 1, overflow: 'auto' }}>
|
||||
{navItems.map(item => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
style={({ isActive }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '9px 20px',
|
||||
color: isActive ? 'var(--text-hi)' : 'var(--text-mid)',
|
||||
background: isActive ? 'var(--bg-hover)' : 'transparent',
|
||||
borderLeft: `2px solid ${isActive ? 'var(--amber)' : 'transparent'}`,
|
||||
fontFamily: 'var(--font-display)',
|
||||
fontSize: 13,
|
||||
fontWeight: isActive ? 700 : 400,
|
||||
letterSpacing: '0.05em',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.1s',
|
||||
})}
|
||||
>
|
||||
<span style={{ fontSize: 14, opacity: 0.7 }}>{item.icon}</span>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Tonight widget */}
|
||||
<div style={{
|
||||
borderTop: '1px solid var(--border)',
|
||||
padding: '12px 16px',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--text-lo)',
|
||||
letterSpacing: '0.1em',
|
||||
marginBottom: 8,
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Tonight
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{[
|
||||
['Dusk', fmtTime(tonight?.astro_dusk_utc)],
|
||||
['Dawn', fmtTime(tonight?.astro_dawn_utc)],
|
||||
['Dark', darkStr],
|
||||
].map(([label, value]) => (
|
||||
<tr key={label}>
|
||||
<td style={{ color: 'var(--text-lo)', fontSize: 11, fontFamily: 'var(--font-mono)', paddingBottom: 3 }}>{label}</td>
|
||||
<td style={{ color: 'var(--text-mid)', fontSize: 11, fontFamily: 'var(--font-mono)', textAlign: 'right' }}>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td style={{ color: 'var(--text-lo)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>Moon</td>
|
||||
<td style={{ textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 4 }}>
|
||||
<span style={{ color: 'var(--text-mid)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>
|
||||
{tonight?.moon_illumination != null
|
||||
? `${Math.round(tonight.moon_illumination * 100)}%`
|
||||
: '—'}
|
||||
</span>
|
||||
{tonight?.moon_illumination != null && (
|
||||
<MoonPhaseIcon illumination={tonight.moon_illumination} size={14} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Conditions widget */}
|
||||
<div style={{
|
||||
borderTop: '1px solid var(--border)',
|
||||
padding: '12px 16px',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--text-lo)',
|
||||
letterSpacing: '0.1em',
|
||||
marginBottom: 8,
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Conditions
|
||||
</div>
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<GoNogo status={weather?.go_nogo} compact />
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{[
|
||||
['Temp', weather?.temp_c != null ? `${weather.temp_c.toFixed(1)}°C` : '—'],
|
||||
['Dew Δ', dewMargin ? `${dewMargin}°C ${parseFloat(dewMargin) < 4 ? '⚠' : '✓'}` : '—'],
|
||||
['Seeing', slot?.seeing ? SEEING_LABELS[slot.seeing] ?? '—' : '—'],
|
||||
['Transp', slot?.transparency ? TRANSP_LABELS[slot.transparency] ?? '—' : '—'],
|
||||
].map(([label, value]) => (
|
||||
<tr key={label}>
|
||||
<td style={{ color: 'var(--text-lo)', fontSize: 11, fontFamily: 'var(--font-mono)', paddingBottom: 3 }}>{label}</td>
|
||||
<td style={{
|
||||
color: label === 'Dew Δ' && dewMargin && parseFloat(dewMargin) < 4
|
||||
? 'var(--danger)'
|
||||
: 'var(--text-mid)',
|
||||
fontSize: 11,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
textAlign: 'right',
|
||||
}}>{value as string}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user