Initial Commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
interface Props {
|
||||
level?: 'warning' | 'critical';
|
||||
temp?: number;
|
||||
dewPoint?: number;
|
||||
}
|
||||
|
||||
export default function DewAlert({ level, temp, dewPoint }: Props) {
|
||||
if (!level) return null;
|
||||
|
||||
const margin = temp != null && dewPoint != null ? (temp - dewPoint).toFixed(1) : null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: level === 'critical' ? 'rgba(224,82,82,0.2)' : 'rgba(232,192,48,0.15)',
|
||||
border: `1px solid ${level === 'critical' ? 'var(--danger)' : 'var(--warn)'}`,
|
||||
borderRadius: 4,
|
||||
padding: '10px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
color: level === 'critical' ? 'var(--danger)' : 'var(--warn)',
|
||||
}}>
|
||||
<span style={{ fontSize: 16 }}>⚠</span>
|
||||
<span>
|
||||
DEW POINT ALERT — {level === 'critical' ? 'CRITICAL' : 'WARNING'}
|
||||
{margin && ` — Margin: ${margin}°C`}
|
||||
{level === 'critical'
|
||||
? ' — Condensation imminent. Protect optics immediately.'
|
||||
: ' — Risk of dew forming. Enable dew heaters.'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
interface Props {
|
||||
status?: 'go' | 'marginal' | 'nogo' | null;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const config = {
|
||||
go: { color: 'var(--good)', label: 'GO', bg: 'rgba(61,186,114,0.15)' },
|
||||
marginal: { color: 'var(--warn)', label: 'MARGINAL', bg: 'rgba(232,192,48,0.15)' },
|
||||
nogo: { color: 'var(--danger)', label: 'NO-GO', bg: 'rgba(224,82,82,0.15)' },
|
||||
};
|
||||
|
||||
export default function GoNogo({ status, compact }: Props) {
|
||||
const cfg = status ? config[status] : null;
|
||||
|
||||
if (!cfg) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: compact ? '2px 8px' : '6px 14px',
|
||||
borderRadius: 4,
|
||||
background: 'var(--bg-panel)',
|
||||
color: 'var(--text-lo)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: compact ? 10 : 12,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
}}>
|
||||
UNKNOWN
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: compact ? '2px 8px' : '8px 18px',
|
||||
borderRadius: 4,
|
||||
background: cfg.bg,
|
||||
color: cfg.color,
|
||||
border: `1px solid ${cfg.color}`,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: compact ? 10 : 13,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.15em',
|
||||
}}>
|
||||
{cfg.label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState } from 'react';
|
||||
import type { WeatherData } from '../../api/types';
|
||||
import GoNogo from './GoNogo';
|
||||
|
||||
interface Props {
|
||||
weather: WeatherData;
|
||||
}
|
||||
|
||||
const SEEING_LABELS: Record<number, string> = {
|
||||
1: '0.5″ (Excellent)', 2: '0.75″ (Good)', 3: '1.0″ (Good)',
|
||||
4: '1.25″ (Average)', 5: '1.5″ (Average)', 6: '2.0″ (Poor)',
|
||||
7: '2.5″ (Poor)', 8: '>3″ (Bad)',
|
||||
};
|
||||
|
||||
const TRANSP_LABELS: Record<number, string> = {
|
||||
1: 'Excellent', 2: 'Good', 3: 'Good', 4: 'Average',
|
||||
5: 'Average', 6: 'Poor', 7: 'Poor', 8: 'Bad',
|
||||
};
|
||||
|
||||
const CC_LABELS: Record<number, string> = {
|
||||
1: '0–6% (Clear)', 2: '6–19%', 3: '19–31%', 4: '31–44%',
|
||||
5: '44–56%', 6: '56–69%', 7: '69–81%', 8: '81–94%', 9: '94–100% (Overcast)',
|
||||
};
|
||||
|
||||
const WIND_DIRS: Record<string, string> = {
|
||||
N: '↑N', NE: '↗NE', E: '→E', SE: '↘SE',
|
||||
S: '↓S', SW: '↙SW', W: '←W', NW: '↖NW',
|
||||
};
|
||||
|
||||
export default function WeatherCard({ weather }: Props) {
|
||||
const [showReasons, setShowReasons] = useState(false);
|
||||
|
||||
const margin = weather.temp_c != null && weather.dew_point_c != null
|
||||
? weather.temp_c - weather.dew_point_c
|
||||
: null;
|
||||
|
||||
const windStr = weather.wind10m
|
||||
? `${WIND_DIRS[weather.wind10m.direction] ?? weather.wind10m.direction} ${weather.wind10m.speed} m/s`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'var(--bg-panel)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 6,
|
||||
padding: 16,
|
||||
}}>
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<GoNogo status={weather.go_nogo} />
|
||||
{weather.go_nogo === 'marginal' && weather.go_nogo_reasons && weather.go_nogo_reasons.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowReasons(v => !v)}
|
||||
title="Why marginal?"
|
||||
style={{
|
||||
fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--warn)',
|
||||
border: '1px solid var(--warn)', borderRadius: 3, padding: '1px 6px', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
why?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showReasons && weather.go_nogo_reasons && (
|
||||
<div style={{
|
||||
background: 'rgba(232,192,48,0.08)', border: '1px solid var(--warn)',
|
||||
borderRadius: 3, padding: '6px 10px', marginBottom: 10,
|
||||
}}>
|
||||
{weather.go_nogo_reasons.map((r, i) => (
|
||||
<div key={i} style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--warn)', marginBottom: 2 }}>
|
||||
⚠ {r}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{([
|
||||
['Temperature', weather.temp_c != null ? `${weather.temp_c.toFixed(1)}°C` : null],
|
||||
['Humidity', weather.humidity_pct != null ? `${weather.humidity_pct.toFixed(0)}%` : null],
|
||||
['Dew Point', weather.dew_point_c != null ? `${weather.dew_point_c.toFixed(1)}°C` : null],
|
||||
['Dew Margin', margin != null ? `${margin.toFixed(1)}°C${margin < 4 ? ' ⚠' : ' ✓'}` : null],
|
||||
['Cloud cover', weather.cloudcover ? CC_LABELS[weather.cloudcover] ?? `${weather.cloudcover}/9` : null],
|
||||
['Seeing', weather.seeing ? SEEING_LABELS[weather.seeing] ?? `${weather.seeing}/8` : null],
|
||||
['Transparency', weather.transparency ? TRANSP_LABELS[weather.transparency] ?? `${weather.transparency}/8` : null],
|
||||
['Wind', windStr],
|
||||
['Atm. stability', weather.lifted_index != null ? `LI ${weather.lifted_index}${weather.lifted_index < -2 ? ' (unstable)' : weather.lifted_index >= 2 ? ' (stable)' : ''}` : null],
|
||||
] as [string, string | null][]).filter(([, v]) => v != null).map(([label, value]) => (
|
||||
<tr key={label}>
|
||||
<td style={{
|
||||
color: 'var(--text-lo)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
paddingBottom: 5,
|
||||
width: '45%',
|
||||
}}>{label}</td>
|
||||
<td style={{
|
||||
color: (label === 'Dew Margin' && margin != null && margin < 4) ? 'var(--danger)'
|
||||
: (label === 'Atm. stability' && (weather.lifted_index ?? 0) < -2) ? 'var(--warn)'
|
||||
: 'var(--text-hi)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
textAlign: 'right',
|
||||
}}>{value as string}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user