Files
Astronome/frontend/src/components/layout/Sidebar.tsx
T
2026-04-10 00:02:00 +02:00

204 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}