From 1ab7b18d38de813976bcc51176fbad186a9a650d Mon Sep 17 00:00:00 2001 From: accius Date: Sat, 31 Jan 2026 19:51:47 -0500 Subject: [PATCH] dx filtering --- public/index.html | 1059 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 893 insertions(+), 166 deletions(-) diff --git a/public/index.html b/public/index.html index b7c8586..0f566b9 100644 --- a/public/index.html +++ b/public/index.html @@ -703,51 +703,6 @@ const [activeSource, setActiveSource] = useState(''); const spotRetentionMs = 30 * 60 * 1000; // 30 minutes const pollInterval = 5000; // 5 seconds - - // Apply filters to spots - const applyFilters = useCallback((spots, filters) => { - if (!filters || Object.keys(filters).length === 0) return spots; - - return spots.filter(spot => { - // Zone filter - if (filters.zones && filters.zones.length > 0) { - // Extract zone from callsign prefix (simplified) - const zone = spot.zone || null; - if (zone && !filters.zones.includes(zone)) return false; - } - - // Band filter - if (filters.bands && filters.bands.length > 0) { - const freq = parseFloat(spot.freq); - const band = getBandFromFreq(freq); - if (!filters.bands.includes(band)) return false; - } - - // Mode filter - if (filters.modes && filters.modes.length > 0) { - const mode = spot.mode || detectMode(spot.comment); - if (mode && !filters.modes.includes(mode)) return false; - } - - // Callsign filter (search in DX call or spotter) - if (filters.callsign && filters.callsign.trim()) { - const search = filters.callsign.toUpperCase().trim(); - const matchesDX = spot.call && spot.call.toUpperCase().includes(search); - const matchesSpotter = spot.spotter && spot.spotter.toUpperCase().includes(search); - if (!matchesDX && !matchesSpotter) return false; - } - - // Exclude callsign patterns - if (filters.exclude && filters.exclude.trim()) { - const excludePatterns = filters.exclude.toUpperCase().split(',').map(p => p.trim()).filter(p => p); - for (const pattern of excludePatterns) { - if (spot.call && spot.call.toUpperCase().includes(pattern)) return false; - } - } - - return true; - }); - }, []); // Helper to get band from frequency const getBandFromFreq = (freq) => { @@ -763,6 +718,7 @@ if (freq >= 28000 && freq <= 29700) return '10m'; if (freq >= 50000 && freq <= 54000) return '6m'; if (freq >= 144000 && freq <= 148000) return '2m'; + if (freq >= 420000 && freq <= 450000) return '70cm'; return 'other'; }; @@ -770,12 +726,197 @@ const detectMode = (comment) => { if (!comment) return null; const upper = comment.toUpperCase(); - if (upper.includes('FT8') || upper.includes('FT4')) return 'DIGI'; + if (upper.includes('FT8')) return 'FT8'; + if (upper.includes('FT4')) return 'FT4'; if (upper.includes('CW')) return 'CW'; if (upper.includes('SSB') || upper.includes('LSB') || upper.includes('USB')) return 'SSB'; - if (upper.includes('RTTY') || upper.includes('PSK')) return 'DIGI'; + if (upper.includes('RTTY')) return 'RTTY'; + if (upper.includes('PSK')) return 'PSK'; + if (upper.includes('AM')) return 'AM'; + if (upper.includes('FM')) return 'FM'; return null; }; + + // Callsign prefix to CQ/ITU zone and continent mapping + const getCallsignInfo = (call) => { + if (!call) return { cqZone: null, ituZone: null, continent: null }; + const upper = call.toUpperCase(); + + // Simplified prefix->zone mapping (common prefixes) + const prefixMap = { + // North America + 'W': { cq: 5, itu: 8, cont: 'NA' }, 'K': { cq: 5, itu: 8, cont: 'NA' }, 'N': { cq: 5, itu: 8, cont: 'NA' }, 'AA': { cq: 5, itu: 8, cont: 'NA' }, + 'VE': { cq: 5, itu: 4, cont: 'NA' }, 'VA': { cq: 5, itu: 4, cont: 'NA' }, 'VY': { cq: 2, itu: 4, cont: 'NA' }, + 'XE': { cq: 6, itu: 10, cont: 'NA' }, 'XF': { cq: 6, itu: 10, cont: 'NA' }, + // Europe + 'G': { cq: 14, itu: 27, cont: 'EU' }, 'M': { cq: 14, itu: 27, cont: 'EU' }, '2E': { cq: 14, itu: 27, cont: 'EU' }, + 'F': { cq: 14, itu: 27, cont: 'EU' }, 'DL': { cq: 14, itu: 28, cont: 'EU' }, 'D': { cq: 14, itu: 28, cont: 'EU' }, + 'I': { cq: 15, itu: 28, cont: 'EU' }, 'IK': { cq: 15, itu: 28, cont: 'EU' }, + 'EA': { cq: 14, itu: 37, cont: 'EU' }, 'EC': { cq: 14, itu: 37, cont: 'EU' }, + 'PA': { cq: 14, itu: 27, cont: 'EU' }, 'PD': { cq: 14, itu: 27, cont: 'EU' }, + 'ON': { cq: 14, itu: 27, cont: 'EU' }, 'OZ': { cq: 14, itu: 18, cont: 'EU' }, + 'SM': { cq: 14, itu: 18, cont: 'EU' }, 'LA': { cq: 14, itu: 18, cont: 'EU' }, + 'OH': { cq: 15, itu: 18, cont: 'EU' }, 'SP': { cq: 15, itu: 28, cont: 'EU' }, + 'OK': { cq: 15, itu: 28, cont: 'EU' }, 'OM': { cq: 15, itu: 28, cont: 'EU' }, + 'HA': { cq: 15, itu: 28, cont: 'EU' }, 'HB': { cq: 14, itu: 28, cont: 'EU' }, + 'OE': { cq: 15, itu: 28, cont: 'EU' }, 'LZ': { cq: 20, itu: 28, cont: 'EU' }, + 'YO': { cq: 20, itu: 28, cont: 'EU' }, 'YU': { cq: 15, itu: 28, cont: 'EU' }, + 'UA': { cq: 16, itu: 29, cont: 'EU' }, 'R': { cq: 16, itu: 29, cont: 'EU' }, + 'UR': { cq: 16, itu: 29, cont: 'EU' }, 'UT': { cq: 16, itu: 29, cont: 'EU' }, + 'LY': { cq: 15, itu: 29, cont: 'EU' }, 'ES': { cq: 15, itu: 29, cont: 'EU' }, + 'YL': { cq: 15, itu: 29, cont: 'EU' }, 'CT': { cq: 14, itu: 37, cont: 'EU' }, + 'EI': { cq: 14, itu: 27, cont: 'EU' }, 'GI': { cq: 14, itu: 27, cont: 'EU' }, + 'GM': { cq: 14, itu: 27, cont: 'EU' }, 'GW': { cq: 14, itu: 27, cont: 'EU' }, + 'SV': { cq: 20, itu: 28, cont: 'EU' }, '9A': { cq: 15, itu: 28, cont: 'EU' }, + 'S5': { cq: 15, itu: 28, cont: 'EU' }, + // Asia + 'JA': { cq: 25, itu: 45, cont: 'AS' }, 'JH': { cq: 25, itu: 45, cont: 'AS' }, 'JR': { cq: 25, itu: 45, cont: 'AS' }, + 'HL': { cq: 25, itu: 44, cont: 'AS' }, 'DS': { cq: 25, itu: 44, cont: 'AS' }, + 'BY': { cq: 24, itu: 44, cont: 'AS' }, 'BV': { cq: 24, itu: 44, cont: 'AS' }, + 'VU': { cq: 22, itu: 41, cont: 'AS' }, 'VK': { cq: 30, itu: 59, cont: 'OC' }, + 'DU': { cq: 27, itu: 50, cont: 'OC' }, '9M': { cq: 28, itu: 54, cont: 'AS' }, + 'HS': { cq: 26, itu: 49, cont: 'AS' }, 'XV': { cq: 26, itu: 49, cont: 'AS' }, + // South America + 'LU': { cq: 13, itu: 14, cont: 'SA' }, 'PY': { cq: 11, itu: 15, cont: 'SA' }, + 'CE': { cq: 12, itu: 14, cont: 'SA' }, 'CX': { cq: 13, itu: 14, cont: 'SA' }, + 'HK': { cq: 9, itu: 12, cont: 'SA' }, 'YV': { cq: 9, itu: 12, cont: 'SA' }, + 'HC': { cq: 10, itu: 12, cont: 'SA' }, 'OA': { cq: 10, itu: 12, cont: 'SA' }, + // Africa + 'ZS': { cq: 38, itu: 57, cont: 'AF' }, '5N': { cq: 35, itu: 46, cont: 'AF' }, + 'EA8': { cq: 33, itu: 36, cont: 'AF' }, 'CN': { cq: 33, itu: 37, cont: 'AF' }, + '7X': { cq: 33, itu: 37, cont: 'AF' }, 'SU': { cq: 34, itu: 38, cont: 'AF' }, + 'ST': { cq: 34, itu: 47, cont: 'AF' }, 'ET': { cq: 37, itu: 48, cont: 'AF' }, + '5Z': { cq: 37, itu: 48, cont: 'AF' }, '5H': { cq: 37, itu: 53, cont: 'AF' }, + // Oceania + 'ZL': { cq: 32, itu: 60, cont: 'OC' }, 'FK': { cq: 32, itu: 56, cont: 'OC' }, + 'VK9': { cq: 30, itu: 60, cont: 'OC' }, 'YB': { cq: 28, itu: 51, cont: 'OC' }, + 'KH6': { cq: 31, itu: 61, cont: 'OC' }, 'KH2': { cq: 27, itu: 64, cont: 'OC' }, + // Caribbean + 'VP5': { cq: 8, itu: 11, cont: 'NA' }, 'PJ': { cq: 9, itu: 11, cont: 'SA' }, + 'HI': { cq: 8, itu: 11, cont: 'NA' }, 'CO': { cq: 8, itu: 11, cont: 'NA' }, + 'KP4': { cq: 8, itu: 11, cont: 'NA' }, 'FG': { cq: 8, itu: 11, cont: 'NA' }, + // Antarctica + 'DP0': { cq: 38, itu: 67, cont: 'AN' }, 'VP8': { cq: 13, itu: 73, cont: 'AN' }, + 'KC4': { cq: 13, itu: 67, cont: 'AN' } + }; + + // Try to match prefix (longest match first) + for (let len = 4; len >= 1; len--) { + const prefix = upper.substring(0, len); + if (prefixMap[prefix]) { + return { + cqZone: prefixMap[prefix].cq, + ituZone: prefixMap[prefix].itu, + continent: prefixMap[prefix].cont + }; + } + } + + // Fallback based on first character + const firstChar = upper[0]; + const fallbackMap = { + 'A': { cq: 21, itu: 39, cont: 'AS' }, + 'B': { cq: 24, itu: 44, cont: 'AS' }, + 'C': { cq: 14, itu: 27, cont: 'EU' }, + 'D': { cq: 14, itu: 28, cont: 'EU' }, + 'E': { cq: 14, itu: 27, cont: 'EU' }, + 'F': { cq: 14, itu: 27, cont: 'EU' }, + 'G': { cq: 14, itu: 27, cont: 'EU' }, + 'H': { cq: 14, itu: 27, cont: 'EU' }, + 'I': { cq: 15, itu: 28, cont: 'EU' }, + 'J': { cq: 25, itu: 45, cont: 'AS' }, + 'K': { cq: 5, itu: 8, cont: 'NA' }, + 'L': { cq: 13, itu: 14, cont: 'SA' }, + 'M': { cq: 14, itu: 27, cont: 'EU' }, + 'N': { cq: 5, itu: 8, cont: 'NA' }, + 'O': { cq: 15, itu: 18, cont: 'EU' }, + 'P': { cq: 11, itu: 15, cont: 'SA' }, + 'R': { cq: 16, itu: 29, cont: 'EU' }, + 'S': { cq: 15, itu: 28, cont: 'EU' }, + 'T': { cq: 37, itu: 48, cont: 'AF' }, + 'U': { cq: 16, itu: 29, cont: 'EU' }, + 'V': { cq: 5, itu: 4, cont: 'NA' }, + 'W': { cq: 5, itu: 8, cont: 'NA' }, + 'X': { cq: 6, itu: 10, cont: 'NA' }, + 'Y': { cq: 15, itu: 28, cont: 'EU' }, + 'Z': { cq: 38, itu: 57, cont: 'AF' } + }; + + if (fallbackMap[firstChar]) { + return { + cqZone: fallbackMap[firstChar].cq, + ituZone: fallbackMap[firstChar].itu, + continent: fallbackMap[firstChar].cont + }; + } + + return { cqZone: null, ituZone: null, continent: null }; + }; + + // Apply filters to spots + const applyFilters = useCallback((spots, filters) => { + if (!filters || Object.keys(filters).length === 0) return spots; + + return spots.filter(spot => { + const callInfo = getCallsignInfo(spot.call); + + // Watchlist only mode - must match watchlist + if (filters.watchlistOnly && filters.watchlist?.length > 0) { + const matchesWatchlist = filters.watchlist.some(w => + spot.call?.toUpperCase().includes(w.toUpperCase()) || + spot.spotter?.toUpperCase().includes(w.toUpperCase()) + ); + if (!matchesWatchlist) return false; + } + + // Exclude list - hide matching calls + if (filters.excludeList?.length > 0) { + const isExcluded = filters.excludeList.some(exc => + spot.call?.toUpperCase().includes(exc.toUpperCase()) || + spot.spotter?.toUpperCase().includes(exc.toUpperCase()) + ); + if (isExcluded) return false; + } + + // CQ Zone filter + if (filters.cqZones?.length > 0) { + if (!callInfo.cqZone || !filters.cqZones.includes(callInfo.cqZone)) return false; + } + + // ITU Zone filter + if (filters.ituZones?.length > 0) { + if (!callInfo.ituZone || !filters.ituZones.includes(callInfo.ituZone)) return false; + } + + // Continent filter + if (filters.continents?.length > 0) { + if (!callInfo.continent || !filters.continents.includes(callInfo.continent)) return false; + } + + // Band filter + if (filters.bands?.length > 0) { + const freq = parseFloat(spot.freq); + const band = getBandFromFreq(freq); + if (!filters.bands.includes(band)) return false; + } + + // Mode filter + if (filters.modes?.length > 0) { + const mode = spot.mode || detectMode(spot.comment); + if (!mode || !filters.modes.includes(mode)) return false; + } + + // Simple callsign search filter + if (filters.callsign?.trim()) { + const search = filters.callsign.toUpperCase().trim(); + const matchesDX = spot.call?.toUpperCase().includes(search); + const matchesSpotter = spot.spotter?.toUpperCase().includes(search); + if (!matchesDX && !matchesSpotter) return false; + } + + return true; + }); + }, []); useEffect(() => { const fetchDX = async () => { @@ -2595,32 +2736,611 @@ ); }; - const DXClusterPanel = ({ spots, loading, activeSource, showOnMap, onToggleMap, spotCount, filteredCount, filters, onFilterChange }) => { - const [showFilters, setShowFilters] = useState(false); + // ============================================ + // DX CLUSTER FILTER MANAGER - Comprehensive filtering system + // ============================================ + 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); - const bandOptions = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m']; - const modeOptions = ['CW', 'SSB', 'DIGI']; + // ITU Zones (1-90) + const ituZones = Array.from({ length: 90 }, (_, i) => i + 1); + + // Continents + 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 toggleBand = (band) => { - const current = filters?.bands || []; - const newBands = current.includes(band) - ? current.filter(b => b !== band) - : [...current, band]; - onFilterChange({ ...filters, bands: newBands.length > 0 ? newBands : undefined }); + // Bands + const bands = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm']; + + // Modes + const modes = ['CW', 'SSB', 'FT8', 'FT4', 'RTTY', 'PSK', 'AM', 'FM']; + + // Toggle functions + const toggleArrayItem = (key, item) => { + const current = filters?.[key] || []; + const newArray = current.includes(item) + ? current.filter(i => i !== item) + : [...current, item]; + onFilterChange({ ...filters, [key]: newArray.length > 0 ? newArray : undefined }); + }; + + const selectAllZones = (type, zones) => { + onFilterChange({ ...filters, [type]: [...zones] }); }; - const toggleMode = (mode) => { - const current = filters?.modes || []; - const newModes = current.includes(mode) - ? current.filter(m => m !== mode) - : [...current, mode]; - onFilterChange({ ...filters, modes: newModes.length > 0 ? newModes : undefined }); + const clearZones = (type) => { + onFilterChange({ ...filters, [type]: undefined }); }; - const hasActiveFilters = (filters?.bands?.length > 0) || (filters?.modes?.length > 0) || filters?.callsign || filters?.exclude; + 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 removeFromExclude = (call) => { + const current = filters?.excludeList || []; + onFilterChange({ ...filters, excludeList: current.filter(c => c !== call) }); + }; + + 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++; + return count; + }; + + if (!isOpen) return null; + + const tabStyle = (active) => ({ + padding: '8px 16px', + background: active ? 'var(--accent-cyan)' : 'transparent', + color: active ? '#000' : 'var(--text-secondary)', + border: 'none', + borderRadius: '4px 4px 0 0', + cursor: 'pointer', + fontFamily: 'JetBrains Mono', + fontSize: '11px', + fontWeight: active ? '700' : '400' + }); + + 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', + borderRadius: '4px', + fontSize: '10px', + cursor: 'pointer', + fontFamily: 'JetBrains Mono', + transition: 'all 0.15s' + }); + + return ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+

πŸ” DX Cluster Filters

+
+ {getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active +
+
+
+ + +
+
+ + {/* Tabs */} +
+ + + + + +
+ + {/* Tab Content */} +
+ + {/* ZONES TAB */} + {activeTab === 'zones' && ( +
+ {/* Continents */} +
+
+ Continents +
+
+ {continents.map(cont => ( + + ))} +
+
+ + {/* CQ Zones */} +
+
+ CQ Zones +
+ + +
+
+
+ {cqZones.map(zone => ( + + ))} +
+
+ + {/* ITU Zones */} +
+
+ ITU Zones +
+ + +
+
+
+ {ituZones.map(zone => ( + + ))} +
+
+
+ )} + + {/* BANDS TAB */} + {activeTab === 'bands' && ( +
+
+ Select bands to show (leave empty for all bands) +
+
+ {bands.map(band => ( + + ))} +
+ + {/* Quick select groups */} +
+
Quick Select:
+
+ + + + + +
+
+
+ )} + + {/* MODES TAB */} + {activeTab === 'modes' && ( +
+
+ Select modes to show (leave empty for all modes) +
+
+ {modes.map(mode => ( + + ))} +
+ + {/* Quick select groups */} +
+
Quick Select:
+
+ + + + +
+
+
+ )} + + {/* WATCHLIST TAB */} + {activeTab === 'watchlist' && ( +
+
+ Callsign Watchlist +
+
+ Add callsigns you want to watch for. Matching spots will be highlighted. +
+ + {/* Watchlist Only toggle */} +
+ +
+ + {/* Add callsign */} +
+ setNewWatchlistCall(e.target.value.toUpperCase())} + onKeyDown={(e) => e.key === 'Enter' && addToWatchlist()} + style={{ + flex: 1, + padding: '10px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border-color)', + borderRadius: '6px', + color: 'var(--text-primary)', + fontSize: '14px', + fontFamily: 'JetBrains Mono' + }} + /> + +
+ + {/* Watchlist items */} +
+ {(filters?.watchlist || []).map(call => ( +
+ {call} + +
+ ))} + {(!filters?.watchlist || filters.watchlist.length === 0) && ( +
No callsigns in watchlist
+ )} +
+
+ )} + + {/* EXCLUDE TAB */} + {activeTab === 'exclude' && ( +
+
+ Exclude Callsigns +
+
+ Add callsigns or prefixes to hide from the cluster. Partial matches work (e.g., "W3" hides all W3 calls). +
+ + {/* Add exclude */} +
+ setNewExcludeCall(e.target.value.toUpperCase())} + onKeyDown={(e) => e.key === 'Enter' && addToExclude()} + style={{ + flex: 1, + padding: '10px 12px', + background: 'var(--bg-secondary)', + border: '1px solid var(--border-color)', + borderRadius: '6px', + color: 'var(--text-primary)', + fontSize: '14px', + fontFamily: 'JetBrains Mono' + }} + /> + +
+ + {/* Exclude items */} +
+ {(filters?.excludeList || []).map(call => ( +
+ {call} + +
+ ))} + {(!filters?.excludeList || filters.excludeList.length === 0) && ( +
No callsigns excluded
+ )} +
+
+ )} +
+
+
+ ); + }; + + // Simple DX Cluster Panel (for sidebars) + const DXClusterPanel = ({ spots, loading, activeSource, showOnMap, onToggleMap, spotCount, filteredCount, filters, onFilterChange, onOpenFilters }) => { + const hasActiveFilters = (filters?.bands?.length > 0) || (filters?.modes?.length > 0) || + (filters?.cqZones?.length > 0) || (filters?.ituZones?.length > 0) || + (filters?.continents?.length > 0) || (filters?.watchlist?.length > 0) || + (filters?.excludeList?.length > 0) || filters?.watchlistOnly; + + // Check if spot matches watchlist for highlighting + const isWatchlistMatch = (spot) => { + if (!filters?.watchlist?.length) return false; + return filters.watchlist.some(w => + spot.call?.toUpperCase().includes(w) || + spot.spotter?.toUpperCase().includes(w) + ); + }; return ( -
+
🌐 DX CLUSTER
@@ -2629,7 +3349,7 @@ {filteredCount !== undefined && spotCount !== undefined ? `${filteredCount}/${spotCount}` : ''}
- {/* Filter controls */} - {showFilters && ( -
- {/* Callsign search */} -
- onFilterChange({ ...filters, callsign: e.target.value || undefined })} - style={{ - width: '100%', - padding: '4px 8px', - background: 'var(--bg-secondary)', - border: '1px solid var(--border-color)', - borderRadius: '4px', - color: 'var(--text-primary)', - fontSize: '11px', - fontFamily: 'JetBrains Mono' - }} - /> -
- - {/* Band filters */} -
-
Bands:
-
- {bandOptions.map(band => ( - - ))} -
-
- - {/* Mode filters */} -
-
Modes:
-
- {modeOptions.map(mode => ( - - ))} + {activeSource &&
Source: {activeSource}
} + +
+ {spots.length > 0 ? spots.map((s, i) => { + const isHighlighted = isWatchlistMatch(s); + return ( +
+ {s.freq} + {s.call} + {s.comment} + {s.time}
+ ); + }) : ( +
+ {hasActiveFilters ? 'No spots match filters' : 'No spots available'}
- - {/* Exclude patterns */} -
- onFilterChange({ ...filters, exclude: e.target.value || undefined })} - style={{ - width: '100%', - padding: '4px 8px', - background: 'var(--bg-secondary)', - border: '1px solid var(--border-color)', - borderRadius: '4px', - color: 'var(--text-primary)', - fontSize: '11px', - fontFamily: 'JetBrains Mono' - }} - /> -
+ )} +
+
+ ); + }; {/* Clear filters */} {hasActiveFilters && ( @@ -3292,6 +3954,22 @@
🌐 DX CLUSTER {dxCluster.filteredCount}/{dxCluster.spotCount}
+
- {/* Filter bar */} + {/* Quick search bar */}
setDxFilters(prev => ({ ...prev, callsign: e.target.value || undefined }))} style={{ @@ -3329,7 +4007,7 @@ fontFamily: 'JetBrains Mono' }} /> - {(dxFilters?.callsign || dxFilters?.bands?.length || dxFilters?.modes?.length) && ( + {Object.keys(dxFilters || {}).length > 0 && ( )}
+ {/* Active filters summary */} + {(dxFilters?.bands?.length || dxFilters?.cqZones?.length || dxFilters?.watchlist?.length) && ( +
+ {dxFilters.bands?.length > 0 && {dxFilters.bands.join(', ')} } + {dxFilters.cqZones?.length > 0 && | CQ: {dxFilters.cqZones.length} zones } + {dxFilters.watchlist?.length > 0 && | {dxFilters.watchlist.length} watched} +
+ )}
{dxCluster.data.slice(0, 8).map((s, i) => (
@@ -3893,6 +4580,7 @@ }, [dxLocation]); const [showSettings, setShowSettings] = useState(false); + const [showDXFilters, setShowDXFilters] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); // Map layer visibility state with localStorage persistence @@ -4107,6 +4795,12 @@ config={config} onSave={handleSaveConfig} /> + setShowDXFilters(false)} + /> ); } @@ -4282,6 +4976,22 @@ 🌐 DX CLUSTER ● LIVE
{dxCluster.filteredCount}/{dxCluster.spotCount} + - {dxCluster.activeSource && {dxCluster.activeSource}}
- {/* Filter bar */} + {/* Quick search bar */}
setDxFilters(prev => ({ ...prev, callsign: e.target.value || undefined }))} style={{ @@ -4319,7 +5028,7 @@ fontFamily: 'JetBrains Mono' }} /> - {(dxFilters?.callsign || dxFilters?.bands?.length || dxFilters?.modes?.length) && ( + {Object.keys(dxFilters || {}).length > 0 && ( )}
-
+ {/* Active filters summary */} + {(dxFilters?.bands?.length || dxFilters?.modes?.length || dxFilters?.cqZones?.length || dxFilters?.watchlist?.length) && ( +
+ {dxFilters.bands?.length > 0 && πŸ“» {dxFilters.bands.join(', ')}} + {dxFilters.modes?.length > 0 && | πŸ“‘ {dxFilters.modes.join(', ')}} + {dxFilters.cqZones?.length > 0 && | 🌍 CQ: {dxFilters.cqZones.length} zones} + {dxFilters.watchlist?.length > 0 && | πŸ‘€ {dxFilters.watchlist.length} watched} +
+ )} +
{dxCluster.data.slice(0, 8).map((s, i) => (
@@ -4462,6 +5181,14 @@ config={config} onSave={handleSaveConfig} /> + + {/* DX Cluster Filter Manager */} + setShowDXFilters(false)} + />
); };