From 1647b5133b4178d98cecb48d90b8c68a38e42b2c Mon Sep 17 00:00:00 2001 From: accius Date: Sun, 1 Feb 2026 23:26:08 -0500 Subject: [PATCH] module updates --- src/components/ContestPanel.jsx | 28 +- src/components/DXFilterManager.jsx | 647 ++++++++++++++++------------- src/hooks/useSatellites.js | 7 +- 3 files changed, 377 insertions(+), 305 deletions(-) diff --git a/src/components/ContestPanel.jsx b/src/components/ContestPanel.jsx index 815ed49..3991a7d 100644 --- a/src/components/ContestPanel.jsx +++ b/src/components/ContestPanel.jsx @@ -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 }) => { ) : data && data.length > 0 ? (
- {data.slice(0, 5).map((contest, i) => ( + {data.slice(0, 6).map((contest, i) => (
{
)}
+ + {/* Contest Calendar Credit */} +
+ + WA7BNM Contest Calendar + +
); }; diff --git a/src/components/DXFilterManager.jsx b/src/components/DXFilterManager.jsx index 9dac74c..90bd77f 100644 --- a/src/components/DXFilterManager.jsx +++ b/src/components/DXFilterManager.jsx @@ -1,360 +1,407 @@ /** * 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); - - // ITU Zones (1-90) - const ituZones = Array.from({ length: 90 }, (_, i) => i + 1); - - // Toggle functions + + 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' } + ]; + + 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']; + 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] }); + onFilterChange({ ...filters, [key]: newArray.length ? newArray : undefined }); }; - - const clearZones = (type) => { - onFilterChange({ ...filters, [type]: 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 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 selectAll = (key, items) => { + onFilterChange({ ...filters, [key]: [...items] }); }; - - 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 = () => { onFilterChange({}); }; - + 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: 'JetBrains Mono', - fontSize: '11px', - fontWeight: active ? '700' : '400' + fontFamily: 'inherit' }); - - 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 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', - 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 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', + color: selected ? 'var(--accent-cyan)' : 'var(--text-secondary)', + fontSize: '12px', + cursor: 'pointer', + fontFamily: 'JetBrains Mono, monospace' + }); + + const renderZonesTab = () => ( +
+ {/* Continents */} +
+
+ Continents +
+
+ {continents.map(c => ( + + ))} +
+
+ + {/* CQ Zones */} +
+
+ CQ Zones +
+ + +
+
+
+ {Array.from({ length: 40 }, (_, i) => i + 1).map(zone => ( + + ))} +
+
+ + {/* ITU Zones */} +
+
+ ITU Zones +
+ + +
+
+
+ {Array.from({ length: 90 }, (_, i) => i + 1).map(zone => ( + + ))} +
+
+
+ ); + + const renderBandsTab = () => ( +
+
+ HF/VHF/UHF Bands +
+ + +
+
+
+ {bands.map(band => ( + + ))} +
+
+ ); + + const renderModesTab = () => ( +
+
+ Operating Modes +
+ + +
+
+
+ {modes.map(mode => ( + + ))} +
+
+ ); + + 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 ( +
+
+
+ Watchlist - Highlight these callsigns +
+
+ 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' + }} + /> + +
+
+
+ {(filters?.watchlist || []).map(call => ( +
+ {call} + +
+ ))} +
+
+ +
+
+ ); + }; + + 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 ( +
+
+
+ Exclude List - Hide these callsigns +
+
+ 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' + }} + /> + +
+
+
+ {(filters?.excludeList || []).map(call => ( +
+ {call} + +
+ ))} +
+
+ ); + }; + return (
+ }}>
e.stopPropagation()}> + }}> {/* Header */} -
-

🔍 DX Cluster Filters

-
- {getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active +
+ 🔍 DX Cluster Filters +
+
+ {getActiveFilterCount()} filters active
- -
- + {/* Tabs */} -
- {['zones', 'bands', 'modes', 'watchlist'].map(tab => ( - - ))} +
+ + + + +
- + {/* Tab Content */} -
- {activeTab === 'zones' && ( -
- {/* CQ Zones */} -
-
- CQ Zones -
- - -
-
-
- {cqZones.map(zone => ( - - ))} -
-
- - {/* Continents */} -
-
Continents
-
- {CONTINENTS.map(({ code, name }) => ( - - ))} -
-
-
- )} - - {activeTab === 'bands' && ( -
-
Select bands to show
-
- {HF_BANDS.map(band => ( - - ))} -
-
- )} - - {activeTab === 'modes' && ( -
-
Select modes to show
-
- {MODES.map(mode => ( - - ))} -
-
- )} - - {activeTab === 'watchlist' && ( -
- {/* Watchlist Only Toggle */} -
- -
- - {/* Add to Watchlist */} -
-
Watchlist (highlight these calls)
-
- 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' - }} - /> - -
-
- {(filters?.watchlist || []).map(call => ( - - {call} - - - ))} -
-
- - {/* Exclude List */} -
-
Exclude List (hide these calls)
-
- 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' - }} - /> - -
-
- {(filters?.excludeList || []).map(call => ( - - {call} - - - ))} -
-
-
- )} +
+ {activeTab === 'zones' && renderZonesTab()} + {activeTab === 'bands' && renderBandsTab()} + {activeTab === 'modes' && renderModesTab()} + {activeTab === 'watchlist' && renderWatchlistTab()} + {activeTab === 'exclude' && renderExcludeTab()}
diff --git a/src/hooks/useSatellites.js b/src/hooks/useSatellites.js index d553669..9b8a23d 100644 --- a/src/hooks/useSatellites.js +++ b/src/hooks/useSatellites.js @@ -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;