|
|
/**
|
|
|
* PSKFilterManager Component
|
|
|
* Filter modal for PSKReporter spots - Bands, Grids, Modes
|
|
|
*/
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
|
const BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm'];
|
|
|
const MODES = ['FT8', 'FT4', 'JS8', 'WSPR', 'JT65', 'JT9', 'MSK144', 'Q65', 'FST4', 'FST4W'];
|
|
|
|
|
|
// Common grid field prefixes by region
|
|
|
const GRID_REGIONS = [
|
|
|
{ name: 'North America East', grids: ['FN', 'FM', 'EN', 'EM', 'DN', 'DM'] },
|
|
|
{ name: 'North America West', grids: ['CN', 'CM', 'DM', 'DN', 'BN', 'BM'] },
|
|
|
{ name: 'Europe', grids: ['JO', 'JN', 'IO', 'IN', 'KO', 'KN', 'LO', 'LN'] },
|
|
|
{ name: 'South America', grids: ['GG', 'GH', 'GI', 'FG', 'FH', 'FI', 'FF', 'FE'] },
|
|
|
{ name: 'Asia', grids: ['PM', 'PL', 'OM', 'OL', 'QL', 'QM', 'NM', 'NL'] },
|
|
|
{ name: 'Oceania', grids: ['QF', 'QG', 'PF', 'PG', 'RF', 'RG', 'OF', 'OG'] },
|
|
|
{ name: 'Africa', grids: ['KH', 'KG', 'JH', 'JG', 'IH', 'IG'] },
|
|
|
];
|
|
|
|
|
|
export const PSKFilterManager = ({ filters, onFilterChange, isOpen, onClose }) => {
|
|
|
const [activeTab, setActiveTab] = useState('bands');
|
|
|
const [customGrid, setCustomGrid] = useState('');
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
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 addCustomGrid = () => {
|
|
|
if (customGrid.trim() && customGrid.length >= 2) {
|
|
|
const grid = customGrid.toUpperCase().substring(0, 2);
|
|
|
const current = filters?.grids || [];
|
|
|
if (!current.includes(grid)) {
|
|
|
onFilterChange({ ...filters, grids: [...current, grid] });
|
|
|
}
|
|
|
setCustomGrid('');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const getActiveFilterCount = () => {
|
|
|
let count = 0;
|
|
|
if (filters?.bands?.length) count += filters.bands.length;
|
|
|
if (filters?.grids?.length) count += filters.grids.length;
|
|
|
if (filters?.modes?.length) count += filters.modes.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 renderBandsTab = () => (
|
|
|
<div>
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
|
|
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
|
|
|
Filter by Band
|
|
|
</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 style={{ marginTop: '15px', fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
|
{filters?.bands?.length
|
|
|
? `Showing only: ${filters.bands.join(', ')}`
|
|
|
: 'Showing all bands (no filter)'}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
|
|
|
const renderGridsTab = () => (
|
|
|
<div>
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
|
|
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
|
|
|
Filter by Grid Square
|
|
|
</span>
|
|
|
<button
|
|
|
onClick={() => clearFilter('grids')}
|
|
|
style={{ background: 'none', border: 'none', color: 'var(--accent-red)', fontSize: '12px', cursor: 'pointer' }}
|
|
|
>
|
|
|
Clear All
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
{/* Custom grid input */}
|
|
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
|
|
|
<input
|
|
|
type="text"
|
|
|
placeholder="Add grid (e.g. FN)"
|
|
|
value={customGrid}
|
|
|
onChange={(e) => setCustomGrid(e.target.value.toUpperCase())}
|
|
|
maxLength={2}
|
|
|
onKeyPress={(e) => e.key === 'Enter' && addCustomGrid()}
|
|
|
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={addCustomGrid}
|
|
|
style={{
|
|
|
padding: '8px 16px',
|
|
|
background: 'var(--accent-cyan)',
|
|
|
border: 'none',
|
|
|
borderRadius: '4px',
|
|
|
color: '#000',
|
|
|
fontSize: '12px',
|
|
|
cursor: 'pointer',
|
|
|
fontWeight: '600'
|
|
|
}}
|
|
|
>
|
|
|
Add
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
{/* Selected grids */}
|
|
|
{filters?.grids?.length > 0 && (
|
|
|
<div style={{ marginBottom: '20px' }}>
|
|
|
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '8px' }}>
|
|
|
Active Grid Filters:
|
|
|
</div>
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
|
|
{filters.grids.map(grid => (
|
|
|
<button
|
|
|
key={grid}
|
|
|
onClick={() => toggleArrayItem('grids', grid)}
|
|
|
style={{
|
|
|
...chipStyle(true),
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
gap: '6px'
|
|
|
}}
|
|
|
>
|
|
|
{grid}
|
|
|
<span style={{ color: 'var(--accent-red)', fontWeight: '700' }}>×</span>
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
{/* Quick select by region */}
|
|
|
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '10px' }}>
|
|
|
Quick Select by Region:
|
|
|
</div>
|
|
|
{GRID_REGIONS.map(region => (
|
|
|
<div key={region.name} style={{ marginBottom: '12px' }}>
|
|
|
<div style={{ fontSize: '11px', color: 'var(--text-secondary)', marginBottom: '6px' }}>
|
|
|
{region.name}
|
|
|
</div>
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
|
|
|
{region.grids.map(grid => (
|
|
|
<button
|
|
|
key={grid}
|
|
|
onClick={() => toggleArrayItem('grids', grid)}
|
|
|
style={{
|
|
|
...chipStyle(filters?.grids?.includes(grid)),
|
|
|
padding: '4px 8px',
|
|
|
fontSize: '11px'
|
|
|
}}
|
|
|
>
|
|
|
{grid}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
);
|
|
|
|
|
|
const renderModesTab = () => (
|
|
|
<div>
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
|
|
|
<span style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)' }}>
|
|
|
Filter by Mode
|
|
|
</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 style={{ marginTop: '15px', fontSize: '11px', color: 'var(--text-muted)' }}>
|
|
|
{filters?.modes?.length
|
|
|
? `Showing only: ${filters.modes.join(', ')}`
|
|
|
: 'Showing all modes (no filter)'}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<div
|
|
|
style={{
|
|
|
position: 'fixed',
|
|
|
top: 0,
|
|
|
left: 0,
|
|
|
right: 0,
|
|
|
bottom: 0,
|
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
justifyContent: 'center',
|
|
|
zIndex: 2000
|
|
|
}}
|
|
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
|
>
|
|
|
<div style={{
|
|
|
background: 'var(--bg-primary)',
|
|
|
border: '1px solid var(--border-color)',
|
|
|
borderRadius: '8px',
|
|
|
width: '500px',
|
|
|
maxWidth: '95vw',
|
|
|
maxHeight: '85vh',
|
|
|
display: 'flex',
|
|
|
flexDirection: 'column',
|
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.5)'
|
|
|
}}>
|
|
|
{/* Header */}
|
|
|
<div style={{
|
|
|
display: 'flex',
|
|
|
justifyContent: 'space-between',
|
|
|
alignItems: 'center',
|
|
|
padding: '16px 20px',
|
|
|
borderBottom: '1px solid var(--border-color)'
|
|
|
}}>
|
|
|
<div>
|
|
|
<h3 style={{ margin: 0, fontSize: '16px', color: 'var(--text-primary)' }}>
|
|
|
📡 PSKReporter Filters
|
|
|
</h3>
|
|
|
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
|
|
|
{getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active
|
|
|
</span>
|
|
|
</div>
|
|
|
<button
|
|
|
onClick={onClose}
|
|
|
style={{
|
|
|
background: 'none',
|
|
|
border: 'none',
|
|
|
color: 'var(--text-muted)',
|
|
|
fontSize: '24px',
|
|
|
cursor: 'pointer',
|
|
|
lineHeight: 1
|
|
|
}}
|
|
|
>
|
|
|
×
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
{/* Tabs */}
|
|
|
<div style={{
|
|
|
display: 'flex',
|
|
|
borderBottom: '1px solid var(--border-color)',
|
|
|
background: 'var(--bg-secondary)'
|
|
|
}}>
|
|
|
<button onClick={() => setActiveTab('bands')} style={tabStyle(activeTab === 'bands')}>
|
|
|
Bands {filters?.bands?.length ? `(${filters.bands.length})` : ''}
|
|
|
</button>
|
|
|
<button onClick={() => setActiveTab('grids')} style={tabStyle(activeTab === 'grids')}>
|
|
|
Grids {filters?.grids?.length ? `(${filters.grids.length})` : ''}
|
|
|
</button>
|
|
|
<button onClick={() => setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>
|
|
|
Modes {filters?.modes?.length ? `(${filters.modes.length})` : ''}
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
{/* Tab Content */}
|
|
|
<div style={{
|
|
|
flex: 1,
|
|
|
overflow: 'auto',
|
|
|
padding: '20px'
|
|
|
}}>
|
|
|
{activeTab === 'bands' && renderBandsTab()}
|
|
|
{activeTab === 'grids' && renderGridsTab()}
|
|
|
{activeTab === 'modes' && renderModesTab()}
|
|
|
</div>
|
|
|
|
|
|
{/* Footer */}
|
|
|
<div style={{
|
|
|
display: 'flex',
|
|
|
justifyContent: 'space-between',
|
|
|
padding: '16px 20px',
|
|
|
borderTop: '1px solid var(--border-color)',
|
|
|
background: 'var(--bg-secondary)'
|
|
|
}}>
|
|
|
<button
|
|
|
onClick={clearAllFilters}
|
|
|
style={{
|
|
|
padding: '8px 16px',
|
|
|
background: 'transparent',
|
|
|
border: '1px solid var(--accent-red)',
|
|
|
borderRadius: '4px',
|
|
|
color: 'var(--accent-red)',
|
|
|
fontSize: '13px',
|
|
|
cursor: 'pointer'
|
|
|
}}
|
|
|
>
|
|
|
Clear All Filters
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={onClose}
|
|
|
style={{
|
|
|
padding: '8px 24px',
|
|
|
background: 'var(--accent-cyan)',
|
|
|
border: 'none',
|
|
|
borderRadius: '4px',
|
|
|
color: '#000',
|
|
|
fontSize: '13px',
|
|
|
cursor: 'pointer',
|
|
|
fontWeight: '600'
|
|
|
}}
|
|
|
>
|
|
|
Done
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default PSKFilterManager;
|