/** * DXFilterManager Component * Filter modal with tabs for Zones, Bands, Modes, Watchlist, Exclude, Settings */ import React, { useState } from 'react'; export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => { const [activeTab, setActiveTab] = useState('zones'); const [newWatchlistCall, setNewWatchlistCall] = useState(''); const [newExcludeCall, setNewExcludeCall] = useState(''); 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 newArray = current.includes(item) ? current.filter(x => x !== item) : [...current, item]; onFilterChange({ ...filters, [key]: newArray.length ? newArray : undefined }); }; const selectAll = (key, items) => { onFilterChange({ ...filters, [key]: [...items] }); }; const clearFilter = (key) => { const newFilters = { ...filters }; delete newFilters[key]; onFilterChange(newFilters); }; const clearAllFilters = () => { onFilterChange({}); }; const getActiveFilterCount = () => { let count = 0; 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; }; const tabStyle = (active) => ({ padding: '8px 16px', background: active ? 'var(--bg-tertiary)' : 'transparent', border: 'none', 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, 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 addToWatchlist = () => { if (newWatchlistCall.trim()) { const current = filters?.watchlist || []; if (!current.includes(newWatchlistCall.toUpperCase())) { onFilterChange({ ...filters, watchlist: [...current, newWatchlistCall.toUpperCase()] }); } setNewWatchlistCall(''); } }; const addToExclude = () => { if (newExcludeCall.trim()) { const current = filters?.excludeList || []; if (!current.includes(newExcludeCall.toUpperCase())) { onFilterChange({ ...filters, excludeList: [...current, newExcludeCall.toUpperCase()] }); } setNewExcludeCall(''); } }; 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 = () => (
Watchlist - Highlight these callsigns
setNewWatchlistCall(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 = () => (
Exclude List - Hide DX callsigns beginning with:
setNewExcludeCall(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}
))}
); const renderSettingsTab = () => { const retentionMinutes = filters?.spotRetentionMinutes || 30; return (
Spot Retention Time
How long to keep DX spots on the map before they expire. Shorter times show only the most recent activity.
onFilterChange({ ...filters, spotRetentionMinutes: parseInt(e.target.value) })} style={{ flex: 1, cursor: 'pointer' }} />
{retentionMinutes} min
5 min (freshest) 30 min (default)
Quick Presets
{[5, 10, 15, 20, 30].map(mins => ( ))}
); }; return (
{/* Header */}
⊘ DX Cluster Filters
{getActiveFilterCount()} filters active
{/* Tabs */}
{/* Tab Content */}
{activeTab === 'zones' && renderZonesTab()} {activeTab === 'bands' && renderBandsTab()} {activeTab === 'modes' && renderModesTab()} {activeTab === 'watchlist' && renderWatchlistTab()} {activeTab === 'exclude' && renderExcludeTab()} {activeTab === 'settings' && renderSettingsTab()}
); }; export default DXFilterManager;