Initial Commit
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { useState } from 'react';
|
||||
import { useTonight } from '../hooks/useTonight';
|
||||
import { useWeather, useForecast } from '../hooks/useWeather';
|
||||
import { useTargets } from '../hooks/useTargets';
|
||||
import { useStats } from '../hooks/useStats';
|
||||
import GoNogo from '../components/weather/GoNogo';
|
||||
import DewAlert from '../components/weather/DewAlert';
|
||||
import MoonPhaseIcon from '../components/sky/MoonPhaseIcon';
|
||||
import DetailDrawer from '../components/targets/DetailDrawer';
|
||||
import type { Target } from '../api/types';
|
||||
|
||||
const FILTER_LABELS: Record<string, string> = {
|
||||
sv220: 'HaOIII', c2: 'SII/OIII', sv260: 'LP', uvir: 'UV/IR',
|
||||
};
|
||||
const CC_LABELS: Record<number, string> = {
|
||||
1: 'Clear', 2: 'Clear', 3: 'Mostly clear', 4: 'Partly cloudy',
|
||||
5: 'Partly cloudy', 6: 'Cloudy', 7: 'Mostly cloudy', 8: 'Overcast', 9: 'Overcast',
|
||||
};
|
||||
const CC_COLOR = (n: number) => n <= 2 ? 'var(--good)' : n <= 4 ? 'var(--teal)' : n <= 6 ? 'var(--warn)' : 'var(--danger)';
|
||||
|
||||
function fmtTime(utc?: string): string {
|
||||
if (!utc) return '—';
|
||||
return new Date(utc).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' });
|
||||
}
|
||||
function fmtDuration(min?: number): string {
|
||||
if (!min) return '—';
|
||||
const h = Math.floor(min / 60);
|
||||
const m = min % 60;
|
||||
return `${h}h ${m < 10 ? '0' : ''}${m}m`;
|
||||
}
|
||||
function fmtIntTotal(min: number): string {
|
||||
if (min < 60) return `${min} min`;
|
||||
const h = (min / 60).toFixed(1);
|
||||
return `${h} h`;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data: tonight } = useTonight();
|
||||
const { data: weather } = useWeather();
|
||||
const { data: forecast } = useForecast();
|
||||
const { data: targets } = useTargets({ tonight: true, limit: 5 });
|
||||
const { data: stats } = useStats();
|
||||
const [expandedTarget, setExpandedTarget] = useState<Target | null>(null);
|
||||
|
||||
const moonPct = tonight?.moon_illumination != null
|
||||
? `${Math.round(tonight.moon_illumination * 100)}%`
|
||||
: '—';
|
||||
|
||||
// Next 4 forecast slots for mini weather bar (3h each = 12h ahead)
|
||||
const slots = (forecast as { dataseries?: { cloudcover?: number; seeing?: number; timepoint?: number }[] })?.dataseries?.slice(0, 8) ?? [];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px 28px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 20 }}>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: 20, fontWeight: 700, color: 'var(--text-hi)', letterSpacing: '0.04em' }}>
|
||||
Dashboard
|
||||
</h1>
|
||||
{tonight?.date && (
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-lo)' }}>
|
||||
{tonight.date}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dew alert banner */}
|
||||
{weather?.dew_alert && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<DewAlert level={weather.dew_alert} temp={weather.temp_c} dewPoint={weather.dew_point_c} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stat cards row */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
|
||||
{/* Go/No-go */}
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Tonight</div>
|
||||
<GoNogo status={weather?.go_nogo} />
|
||||
{weather?.temp_c != null && (
|
||||
<div style={{ marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mid)' }}>
|
||||
{weather.temp_c.toFixed(1)}°C · {weather.humidity_pct?.toFixed(0)}% RH
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Moon */}
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Moon</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
{tonight?.moon_illumination != null && <MoonPhaseIcon illumination={tonight.moon_illumination} size={32} />}
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 600, color: 'var(--text-hi)', lineHeight: 1 }}>{moonPct}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 3 }}>{tonight?.moon_phase_name ?? '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* True dark */}
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>True Dark</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 600, color: 'var(--teal)', lineHeight: 1 }}>
|
||||
{fmtDuration(tonight?.true_dark_minutes)}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
|
||||
{tonight?.true_dark_start_utc
|
||||
? `${fmtTime(tonight.true_dark_start_utc)} – ${fmtTime(tonight.true_dark_end_utc)}`
|
||||
: 'No full dark tonight'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats summary */}
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Logbook</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 20, fontWeight: 600, color: 'var(--amber)', lineHeight: 1 }}>
|
||||
{stats ? fmtIntTotal(stats.total_integration_min) : '—'}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-mid)', marginTop: 4 }}>
|
||||
{stats ? `${stats.total_sessions} sessions · ${stats.objects_with_keeper} keepers` : 'No data yet'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tonight timing + top targets + forecast */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr 1fr', gap: 16, marginBottom: 20 }}>
|
||||
|
||||
{/* Tonight timing */}
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>Tonight's Window</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{[
|
||||
['Dusk', fmtTime(tonight?.astro_dusk_utc)],
|
||||
['Dawn', fmtTime(tonight?.astro_dawn_utc)],
|
||||
['Moon rise', fmtTime(tonight?.moon_rise_utc)],
|
||||
['Moon set', fmtTime(tonight?.moon_set_utc)],
|
||||
['Dark start', fmtTime(tonight?.true_dark_start_utc)],
|
||||
['Dark end', fmtTime(tonight?.true_dark_end_utc)],
|
||||
].map(([label, value]) => (
|
||||
<tr key={label}>
|
||||
<td style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 11, paddingBottom: 4, paddingRight: 8 }}>{label}</td>
|
||||
<td style={{ color: 'var(--text-hi)', fontFamily: 'var(--font-mono)', fontSize: 11, textAlign: 'right' }}>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Top targets */}
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>Top Targets Tonight</div>
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, overflow: 'hidden' }}>
|
||||
{!targets?.items?.length && (
|
||||
<div style={{ padding: 16, color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
|
||||
Catalog loading…
|
||||
</div>
|
||||
)}
|
||||
{targets?.items?.map((t, i) => (
|
||||
<div key={t.id}>
|
||||
<div
|
||||
onClick={() => setExpandedTarget(expandedTarget?.id === t.id ? null : t)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
gap: 10,
|
||||
cursor: 'pointer',
|
||||
background: expandedTarget?.id === t.id ? 'var(--bg-hover)' : 'transparent',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', width: 14 }}>{i + 1}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--amber)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{t.common_name ?? t.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-lo)' }}>
|
||||
{t.name} · {t.usable_min ? `${t.usable_min}min` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, color: (t.max_alt_deg ?? 0) >= 30 ? 'var(--good)' : 'var(--warn)' }}>
|
||||
{t.max_alt_deg?.toFixed(0)}°
|
||||
</span>
|
||||
{t.recommended_filter && (
|
||||
<span className={`filter-pill ${t.recommended_filter}`} style={{ fontSize: 9 }}>
|
||||
{FILTER_LABELS[t.recommended_filter] ?? t.recommended_filter.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{expandedTarget?.id === t.id && <DetailDrawer target={t} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Forecast mini bars */}
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 10 }}>24h Forecast</div>
|
||||
<div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '12px 14px' }}>
|
||||
{slots.length === 0 && (
|
||||
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 11 }}>No forecast data</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{slots.map((slot, i) => {
|
||||
const cc = slot.cloudcover ?? 5;
|
||||
const seeing = slot.seeing ?? 5;
|
||||
const hoursAhead = (i + 1) * 3;
|
||||
const label = `+${hoursAhead}h`;
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-lo)', width: 28 }}>{label}</span>
|
||||
<div style={{ flex: 1, background: 'var(--bg-deep)', borderRadius: 2, height: 6, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: `${((9 - cc) / 8) * 100}%`,
|
||||
background: CC_COLOR(cc),
|
||||
borderRadius: 2,
|
||||
transition: 'width 0.3s',
|
||||
}} />
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: CC_COLOR(cc), width: 80 }}>
|
||||
{CC_LABELS[cc] ?? '—'}
|
||||
</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: seeing <= 3 ? 'var(--good)' : seeing <= 5 ? 'var(--warn)' : 'var(--danger)', width: 24, textAlign: 'right' }}>
|
||||
{['', '0.5″', '0.75″', '1.0″', '1.25″', '1.5″', '2.0″', '2.5″', '>3″'][seeing] ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user