module updates

pull/6/head
accius 3 days ago
parent 6afa55c7bd
commit 1647b5133b

@ -1,6 +1,6 @@
/**
* ContestPanel Component
* Displays upcoming contests (compact version)
* Displays upcoming contests with contestcalendar.com credit
*/
import React from 'react';
@ -11,6 +11,7 @@ export const ContestPanel = ({ data, loading }) => {
case 'SSB': return 'var(--accent-amber)';
case 'RTTY': return 'var(--accent-purple)';
case 'FT8': case 'FT4': return 'var(--accent-green)';
case 'Mixed': return 'var(--text-secondary)';
default: return 'var(--text-secondary)';
}
};
@ -37,12 +38,12 @@ export const ContestPanel = ({ data, loading }) => {
</div>
) : data && data.length > 0 ? (
<div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono, monospace' }}>
{data.slice(0, 5).map((contest, i) => (
{data.slice(0, 6).map((contest, i) => (
<div
key={`${contest.name}-${i}`}
style={{
padding: '4px 0',
borderBottom: i < Math.min(data.length, 5) - 1 ? '1px solid var(--border-color)' : 'none'
borderBottom: i < Math.min(data.length, 6) - 1 ? '1px solid var(--border-color)' : 'none'
}}
>
<div style={{
@ -67,6 +68,27 @@ export const ContestPanel = ({ data, loading }) => {
</div>
)}
</div>
{/* Contest Calendar Credit */}
<div style={{
marginTop: '6px',
paddingTop: '6px',
borderTop: '1px solid var(--border-color)',
textAlign: 'right'
}}>
<a
href="https://www.contestcalendar.com"
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: '9px',
color: 'var(--text-muted)',
textDecoration: 'none'
}}
>
WA7BNM Contest Calendar
</a>
</div>
</div>
);
};

@ -1,66 +1,43 @@
/**
* DXFilterManager Component
* Modal for DX cluster filtering (zones, bands, modes, watchlist)
* Filter modal with tabs for Zones, Bands, Modes, Watchlist, Exclude
*/
import React, { useState } from 'react';
import { HF_BANDS, CONTINENTS, MODES } from '../utils/callsign.js';
export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => {
const [activeTab, setActiveTab] = useState('zones');
const [newWatchlistCall, setNewWatchlistCall] = useState('');
const [newExcludeCall, setNewExcludeCall] = useState('');
// CQ Zones (1-40)
const cqZones = Array.from({ length: 40 }, (_, i) => i + 1);
if (!isOpen) return null;
const continents = [
{ code: 'NA', name: 'North America' },
{ code: 'SA', name: 'South America' },
{ code: 'EU', name: 'Europe' },
{ code: 'AF', name: 'Africa' },
{ code: 'AS', name: 'Asia' },
{ code: 'OC', name: 'Oceania' },
{ code: 'AN', name: 'Antarctica' }
];
// ITU Zones (1-90)
const ituZones = Array.from({ length: 90 }, (_, i) => i + 1);
const bands = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '11m', '10m', '6m', '2m', '70cm'];
const modes = ['CW', 'SSB', 'FT8', 'FT4', 'RTTY', 'PSK', 'JT65', 'JS8', 'SSTV', 'AM', 'FM'];
// Toggle functions
const toggleArrayItem = (key, item) => {
const current = filters?.[key] || [];
const current = filters[key] || [];
const newArray = current.includes(item)
? current.filter(i => i !== item)
? current.filter(x => x !== item)
: [...current, item];
onFilterChange({ ...filters, [key]: newArray.length > 0 ? newArray : undefined });
};
const selectAllZones = (type, zones) => {
onFilterChange({ ...filters, [type]: [...zones] });
};
const clearZones = (type) => {
onFilterChange({ ...filters, [type]: undefined });
onFilterChange({ ...filters, [key]: newArray.length ? newArray : undefined });
};
const addToWatchlist = () => {
if (!newWatchlistCall.trim()) return;
const current = filters?.watchlist || [];
const call = newWatchlistCall.toUpperCase().trim();
if (!current.includes(call)) {
onFilterChange({ ...filters, watchlist: [...current, call] });
}
setNewWatchlistCall('');
const selectAll = (key, items) => {
onFilterChange({ ...filters, [key]: [...items] });
};
const removeFromWatchlist = (call) => {
const current = filters?.watchlist || [];
onFilterChange({ ...filters, watchlist: current.filter(c => c !== call) });
};
const addToExclude = () => {
if (!newExcludeCall.trim()) return;
const current = filters?.excludeList || [];
const call = newExcludeCall.toUpperCase().trim();
if (!current.includes(call)) {
onFilterChange({ ...filters, excludeList: [...current, call] });
}
setNewExcludeCall('');
};
const removeFromExclude = (call) => {
const current = filters?.excludeList || [];
onFilterChange({ ...filters, excludeList: current.filter(c => c !== call) });
const clearFilter = (key) => {
const newFilters = { ...filters };
delete newFilters[key];
onFilterChange(newFilters);
};
const clearAllFilters = () => {
@ -69,292 +46,362 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
const getActiveFilterCount = () => {
let count = 0;
if (filters?.cqZones?.length) count++;
if (filters?.ituZones?.length) count++;
if (filters?.continents?.length) count++;
if (filters?.bands?.length) count++;
if (filters?.modes?.length) count++;
if (filters?.watchlist?.length) count++;
if (filters?.excludeList?.length) count++;
if (filters?.callsign) count++;
if (filters?.watchlistOnly) count++;
if (filters?.continents?.length) count += filters.continents.length;
if (filters?.cqZones?.length) count += filters.cqZones.length;
if (filters?.ituZones?.length) count += filters.ituZones.length;
if (filters?.bands?.length) count += filters.bands.length;
if (filters?.modes?.length) count += filters.modes.length;
if (filters?.watchlist?.length) count += filters.watchlist.length;
if (filters?.excludeList?.length) count += filters.excludeList.length;
return count;
};
if (!isOpen) return null;
const tabStyle = (active) => ({
padding: '8px 16px',
background: active ? 'var(--accent-cyan)' : 'transparent',
color: active ? '#000' : 'var(--text-secondary)',
background: active ? 'var(--bg-tertiary)' : 'transparent',
border: 'none',
borderRadius: '4px 4px 0 0',
borderBottom: active ? '2px solid var(--accent-cyan)' : '2px solid transparent',
color: active ? 'var(--accent-cyan)' : 'var(--text-muted)',
fontSize: '13px',
cursor: 'pointer',
fontFamily: 'inherit'
});
const chipStyle = (selected) => ({
padding: '6px 12px',
background: selected ? 'rgba(0, 221, 255, 0.2)' : 'var(--bg-tertiary)',
border: `1px solid ${selected ? 'var(--accent-cyan)' : 'var(--border-color)'}`,
borderRadius: '4px',
color: selected ? 'var(--accent-cyan)' : 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono',
fontSize: '11px',
fontWeight: active ? '700' : '400'
fontFamily: 'JetBrains Mono, monospace'
});
const pillStyle = (active) => ({
padding: '4px 10px',
background: active ? 'rgba(0, 255, 136, 0.3)' : 'rgba(60,60,60,0.5)',
border: `1px solid ${active ? '#00ff88' : '#444'}`,
color: active ? '#00ff88' : '#888',
const zoneButtonStyle = (selected) => ({
width: '36px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: selected ? 'rgba(0, 221, 255, 0.2)' : 'var(--bg-tertiary)',
border: `1px solid ${selected ? 'var(--accent-cyan)' : 'var(--border-color)'}`,
borderRadius: '4px',
fontSize: '10px',
color: selected ? 'var(--accent-cyan)' : 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono',
transition: 'all 0.15s'
fontFamily: 'JetBrains Mono, monospace'
});
const renderZonesTab = () => (
<div>
{/* Continents */}
<div style={{ marginBottom: '20px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '10px' }}>
Continents
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{continents.map(c => (
<button
key={c.code}
onClick={() => toggleArrayItem('continents', c.code)}
style={chipStyle(filters?.continents?.includes(c.code))}
>
{c.code} - {c.name}
</button>
))}
</div>
</div>
{/* CQ Zones */}
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>CQ Zones</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => selectAll('cqZones', Array.from({length: 40}, (_, i) => i + 1))} style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}>Select All</button>
<button onClick={() => clearFilter('cqZones')} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}>Clear</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(15, 1fr)', gap: '4px' }}>
{Array.from({ length: 40 }, (_, i) => i + 1).map(zone => (
<button
key={zone}
onClick={() => toggleArrayItem('cqZones', zone)}
style={zoneButtonStyle(filters?.cqZones?.includes(zone))}
>
{zone}
</button>
))}
</div>
</div>
{/* ITU Zones */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>ITU Zones</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => selectAll('ituZones', Array.from({length: 90}, (_, i) => i + 1))} style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}>Select All</button>
<button onClick={() => clearFilter('ituZones')} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}>Clear</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(15, 1fr)', gap: '4px' }}>
{Array.from({ length: 90 }, (_, i) => i + 1).map(zone => (
<button
key={zone}
onClick={() => toggleArrayItem('ituZones', zone)}
style={zoneButtonStyle(filters?.ituZones?.includes(zone))}
>
{zone}
</button>
))}
</div>
</div>
</div>
);
const renderBandsTab = () => (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>HF/VHF/UHF Bands</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => selectAll('bands', bands)} style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}>Select All</button>
<button onClick={() => clearFilter('bands')} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}>Clear</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{bands.map(band => (
<button
key={band}
onClick={() => toggleArrayItem('bands', band)}
style={chipStyle(filters?.bands?.includes(band))}
>
{band}
</button>
))}
</div>
</div>
);
const renderModesTab = () => (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>Operating Modes</span>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => selectAll('modes', modes)} style={{ background: 'none', border: 'none', color: 'var(--accent-cyan)', fontSize: '12px', cursor: 'pointer' }}>Select All</button>
<button onClick={() => clearFilter('modes')} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}>Clear</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{modes.map(mode => (
<button
key={mode}
onClick={() => toggleArrayItem('modes', mode)}
style={chipStyle(filters?.modes?.includes(mode))}
>
{mode}
</button>
))}
</div>
</div>
);
const renderWatchlistTab = () => {
const [newCall, setNewCall] = useState('');
const addToWatchlist = () => {
if (newCall.trim()) {
const current = filters?.watchlist || [];
if (!current.includes(newCall.toUpperCase())) {
onFilterChange({ ...filters, watchlist: [...current, newCall.toUpperCase()] });
}
setNewCall('');
}
};
return (
<div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
Watchlist - Highlight these callsigns
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={newCall}
onChange={(e) => setNewCall(e.target.value.toUpperCase())}
onKeyPress={(e) => e.key === 'Enter' && addToWatchlist()}
placeholder="Enter callsign..."
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '4px',
color: 'var(--text-primary)',
fontSize: '13px',
fontFamily: 'JetBrains Mono'
}}
/>
<button onClick={addToWatchlist} style={{ padding: '8px 16px', background: 'var(--accent-cyan)', border: 'none', borderRadius: '4px', color: '#000', fontWeight: '600', cursor: 'pointer' }}>Add</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{(filters?.watchlist || []).map(call => (
<div key={call} style={{ ...chipStyle(true), display: 'flex', alignItems: 'center', gap: '8px' }}>
{call}
<button onClick={() => toggleArrayItem('watchlist', call)} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: 0, fontSize: '14px' }}>×</button>
</div>
))}
</div>
<div style={{ marginTop: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-secondary)', fontSize: '12px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={filters?.watchlistOnly || false}
onChange={(e) => onFilterChange({ ...filters, watchlistOnly: e.target.checked || undefined })}
/>
Show only watchlist callsigns
</label>
</div>
</div>
);
};
const renderExcludeTab = () => {
const [newCall, setNewCall] = useState('');
const addToExclude = () => {
if (newCall.trim()) {
const current = filters?.excludeList || [];
if (!current.includes(newCall.toUpperCase())) {
onFilterChange({ ...filters, excludeList: [...current, newCall.toUpperCase()] });
}
setNewCall('');
}
};
return (
<div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
Exclude List - Hide these callsigns
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={newCall}
onChange={(e) => setNewCall(e.target.value.toUpperCase())}
onKeyPress={(e) => e.key === 'Enter' && addToExclude()}
placeholder="Enter callsign..."
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '4px',
color: 'var(--text-primary)',
fontSize: '13px',
fontFamily: 'JetBrains Mono'
}}
/>
<button onClick={addToExclude} style={{ padding: '8px 16px', background: 'var(--accent-red)', border: 'none', borderRadius: '4px', color: '#fff', fontWeight: '600', cursor: 'pointer' }}>Add</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{(filters?.excludeList || []).map(call => (
<div key={call} style={{ ...chipStyle(false), background: 'rgba(255, 68, 68, 0.2)', borderColor: 'var(--accent-red)', color: 'var(--accent-red)', display: 'flex', alignItems: 'center', gap: '8px' }}>
{call}
<button onClick={() => toggleArrayItem('excludeList', call)} style={{ background: 'none', border: 'none', color: 'var(--accent-red)', cursor: 'pointer', padding: 0, fontSize: '14px' }}>×</button>
</div>
))}
</div>
</div>
);
};
return (
<div style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.8)',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10000
}} onClick={onClose}>
}}>
<div style={{
background: 'var(--bg-primary)',
background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
width: '90%',
maxWidth: '700px',
width: '700px',
maxHeight: '85vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}} onClick={e => e.stopPropagation()}>
}}>
{/* Header */}
<div style={{
padding: '16px 20px',
borderBottom: '1px solid var(--border-color)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
alignItems: 'center',
padding: '16px 20px',
borderBottom: '1px solid var(--border-color)'
}}>
<div>
<h2 style={{ margin: 0, fontSize: '18px', color: 'var(--accent-cyan)' }}>🔍 DX Cluster Filters</h2>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px' }}>
{getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active
<div style={{ fontSize: '18px', fontWeight: '700', color: 'var(--accent-cyan)' }}>
🔍 DX Cluster Filters
</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
{getActiveFilterCount()} filters active
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={clearAllFilters} style={{
padding: '8px 16px',
background: 'rgba(255, 100, 100, 0.2)',
border: '1px solid #ff6666',
color: '#ff6666',
borderRadius: '6px',
cursor: 'pointer',
fontFamily: 'JetBrains Mono',
fontSize: '11px'
}}>
<button
onClick={clearAllFilters}
style={{
padding: '8px 16px',
background: 'rgba(255, 68, 102, 0.2)',
border: '1px solid var(--accent-red)',
borderRadius: '6px',
color: 'var(--accent-red)',
fontSize: '13px',
cursor: 'pointer'
}}
>
Clear All
</button>
<button onClick={onClose} style={{
padding: '8px 16px',
background: 'var(--accent-cyan)',
border: 'none',
color: '#000',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: '600',
fontSize: '12px'
}}>
<button
onClick={onClose}
style={{
padding: '8px 20px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '13px',
fontWeight: '600',
cursor: 'pointer'
}}
>
Done
</button>
</div>
</div>
{/* Tabs */}
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color)', padding: '0 20px' }}>
{['zones', 'bands', 'modes', 'watchlist'].map(tab => (
<button key={tab} onClick={() => setActiveTab(tab)} style={tabStyle(activeTab === tab)}>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color)' }}>
<button onClick={() => setActiveTab('zones')} style={tabStyle(activeTab === 'zones')}>Zones</button>
<button onClick={() => setActiveTab('bands')} style={tabStyle(activeTab === 'bands')}>Bands</button>
<button onClick={() => setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>Modes</button>
<button onClick={() => setActiveTab('watchlist')} style={tabStyle(activeTab === 'watchlist')}>Watchlist</button>
<button onClick={() => setActiveTab('exclude')} style={tabStyle(activeTab === 'exclude')}>Exclude</button>
</div>
{/* Tab Content */}
<div style={{ padding: '20px', overflow: 'auto', flex: 1 }}>
{activeTab === 'zones' && (
<div>
{/* CQ Zones */}
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '10px' }}>
<span style={{ fontWeight: '600' }}>CQ Zones</span>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => selectAllZones('cqZones', cqZones)} style={{ ...pillStyle(false), fontSize: '9px' }}>Select All</button>
<button onClick={() => clearZones('cqZones')} style={{ ...pillStyle(false), fontSize: '9px' }}>Clear</button>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{cqZones.map(zone => (
<button key={zone} onClick={() => toggleArrayItem('cqZones', zone)} style={{ ...pillStyle(filters?.cqZones?.includes(zone)), width: '36px', textAlign: 'center' }}>
{zone}
</button>
))}
</div>
</div>
{/* Continents */}
<div>
<div style={{ fontWeight: '600', marginBottom: '10px' }}>Continents</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{CONTINENTS.map(({ code, name }) => (
<button key={code} onClick={() => toggleArrayItem('continents', code)} style={{ ...pillStyle(filters?.continents?.includes(code)), padding: '8px 16px' }}>
{code} - {name}
</button>
))}
</div>
</div>
</div>
)}
{activeTab === 'bands' && (
<div>
<div style={{ fontWeight: '600', marginBottom: '10px' }}>Select bands to show</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{HF_BANDS.map(band => (
<button key={band} onClick={() => toggleArrayItem('bands', band)} style={{ ...pillStyle(filters?.bands?.includes(band)), padding: '10px 20px', fontSize: '12px' }}>
{band}
</button>
))}
</div>
</div>
)}
{activeTab === 'modes' && (
<div>
<div style={{ fontWeight: '600', marginBottom: '10px' }}>Select modes to show</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{MODES.map(mode => (
<button key={mode} onClick={() => toggleArrayItem('modes', mode)} style={{ ...pillStyle(filters?.modes?.includes(mode)), padding: '10px 20px', fontSize: '12px' }}>
{mode}
</button>
))}
</div>
</div>
)}
{activeTab === 'watchlist' && (
<div>
{/* Watchlist Only Toggle */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={filters?.watchlistOnly || false}
onChange={e => onFilterChange({ ...filters, watchlistOnly: e.target.checked })}
style={{ width: '18px', height: '18px' }}
/>
<span style={{ fontWeight: '600' }}>Show ONLY watchlist callsigns</span>
</label>
</div>
{/* Add to Watchlist */}
<div style={{ marginBottom: '20px' }}>
<div style={{ fontWeight: '600', marginBottom: '10px' }}>Watchlist (highlight these calls)</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
<input
type="text"
value={newWatchlistCall}
onChange={e => setNewWatchlistCall(e.target.value)}
onKeyPress={e => e.key === 'Enter' && addToWatchlist()}
placeholder="Add callsign..."
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '4px',
color: 'var(--text-primary)',
fontFamily: 'JetBrains Mono'
}}
/>
<button onClick={addToWatchlist} style={{ ...pillStyle(true), padding: '8px 16px' }}>Add</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{(filters?.watchlist || []).map(call => (
<span key={call} style={{
padding: '4px 10px',
background: 'rgba(0, 255, 136, 0.2)',
border: '1px solid var(--accent-green)',
borderRadius: '4px',
color: 'var(--accent-green)',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}>
{call}
<button onClick={() => removeFromWatchlist(call)} style={{
background: 'none',
border: 'none',
color: 'var(--accent-red)',
cursor: 'pointer',
padding: 0,
fontSize: '14px'
}}>×</button>
</span>
))}
</div>
</div>
{/* Exclude List */}
<div>
<div style={{ fontWeight: '600', marginBottom: '10px' }}>Exclude List (hide these calls)</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
<input
type="text"
value={newExcludeCall}
onChange={e => setNewExcludeCall(e.target.value)}
onKeyPress={e => e.key === 'Enter' && addToExclude()}
placeholder="Add callsign..."
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '4px',
color: 'var(--text-primary)',
fontFamily: 'JetBrains Mono'
}}
/>
<button onClick={addToExclude} style={{ ...pillStyle(false), padding: '8px 16px', borderColor: '#ff6666', color: '#ff6666' }}>Add</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{(filters?.excludeList || []).map(call => (
<span key={call} style={{
padding: '4px 10px',
background: 'rgba(255, 100, 100, 0.2)',
border: '1px solid var(--accent-red)',
borderRadius: '4px',
color: 'var(--accent-red)',
fontSize: '11px',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}>
{call}
<button onClick={() => removeFromExclude(call)} style={{
background: 'none',
border: 'none',
color: 'var(--accent-red)',
cursor: 'pointer',
padding: 0,
fontSize: '14px'
}}>×</button>
</span>
))}
</div>
</div>
</div>
)}
<div style={{ padding: '20px', overflowY: 'auto', flex: 1 }}>
{activeTab === 'zones' && renderZonesTab()}
{activeTab === 'bands' && renderBandsTab()}
{activeTab === 'modes' && renderModesTab()}
{activeTab === 'watchlist' && renderWatchlistTab()}
{activeTab === 'exclude' && renderExcludeTab()}
</div>
</div>
</div>

@ -62,10 +62,13 @@ export const useSatellites = (observerLocation) => {
};
Object.entries(tleData).forEach(([name, tle]) => {
if (!tle.line1 || !tle.line2) return;
// Server returns tle1/tle2, handle both formats
const line1 = tle.line1 || tle.tle1;
const line2 = tle.line2 || tle.tle2;
if (!line1 || !line2) return;
try {
const satrec = satellite.twoline2satrec(tle.line1, tle.line2);
const satrec = satellite.twoline2satrec(line1, line2);
const positionAndVelocity = satellite.propagate(satrec, now);
if (!positionAndVelocity.position) return;

Loading…
Cancel
Save

Powered by TurnKey Linux.