/**
* PSKReporter Panel
* Shows where your digital mode signals are being received
* Uses MQTT WebSocket for real-time data
*/
import React, { useState, useMemo } from 'react';
import { usePSKReporter } from '../hooks/usePSKReporter.js';
import { getBandColor } from '../utils/callsign.js';
const PSKReporterPanel = ({
callsign,
onShowOnMap,
showOnMap,
onToggleMap,
filters = {},
onOpenFilters
}) => {
const [activeTab, setActiveTab] = useState('tx'); // Default to 'tx' (Being Heard)
const {
txReports,
txCount,
rxReports,
rxCount,
loading,
error,
connected,
source,
refresh
} = usePSKReporter(callsign, {
minutes: 15,
enabled: callsign && callsign !== 'N0CALL'
});
// Filter reports by band, grid, and mode
const filterReports = (reports) => {
return reports.filter(r => {
// Band filter
if (filters?.bands?.length && !filters.bands.includes(r.band)) return false;
// Grid filter (prefix match)
if (filters?.grids?.length) {
const grid = activeTab === 'tx' ? r.receiverGrid : r.senderGrid;
if (!grid) return false;
const gridPrefix = grid.substring(0, 2).toUpperCase();
if (!filters.grids.includes(gridPrefix)) return false;
}
// Mode filter
if (filters?.modes?.length && !filters.modes.includes(r.mode)) return false;
return true;
});
};
const filteredTx = useMemo(() => filterReports(txReports), [txReports, filters, activeTab]);
const filteredRx = useMemo(() => filterReports(rxReports), [rxReports, filters, activeTab]);
const filteredReports = activeTab === 'tx' ? filteredTx : filteredRx;
// Count active filters
const getActiveFilterCount = () => {
let count = 0;
if (filters?.bands?.length) count++;
if (filters?.grids?.length) count++;
if (filters?.modes?.length) count++;
return count;
};
const filterCount = getActiveFilterCount();
// Get band color from frequency
const getFreqColor = (freqMHz) => {
if (!freqMHz) return 'var(--text-muted)';
const freq = parseFloat(freqMHz);
return getBandColor(freq);
};
// Format age
const formatAge = (minutes) => {
if (minutes < 1) return 'now';
if (minutes < 60) return `${minutes}m`;
return `${Math.floor(minutes/60)}h`;
};
// Get status indicator
const getStatusIndicator = () => {
if (connected) {
return β LIVE;
}
if (source === 'connecting' || source === 'reconnecting') {
return β {source};
}
if (error) {
return β offline;
}
return null;
};
if (!callsign || callsign === 'N0CALL') {
return (
{/* Header */}
π‘ PSKReporter {getStatusIndicator()}
{filteredReports.length}/{activeTab === 'tx' ? txCount : rxCount}
{onToggleMap && (
)}
{/* Tabs */}
{/* Reports list */}
{error && !connected ? (
β οΈ Connection failed - click π to retry
) : loading && filteredReports.length === 0 && filterCount === 0 ? (
) : !connected && filteredReports.length === 0 && filterCount === 0 ? (
Waiting for connection...
) : filteredReports.length === 0 ? (
{filterCount > 0
? 'No spots match filters'
: activeTab === 'tx'
? 'Waiting for spots... (TX to see reports)'
: 'No stations heard yet'}
) : (
{filteredReports.slice(0, 20).map((report, i) => {
const freqMHz = report.freqMHz || (report.freq ? (report.freq / 1000000).toFixed(3) : '?');
const color = getFreqColor(freqMHz);
const displayCall = activeTab === 'tx' ? report.receiver : report.sender;
const grid = activeTab === 'tx' ? report.receiverGrid : report.senderGrid;
return (
onShowOnMap && report.lat && report.lon && onShowOnMap(report)}
style={{
display: 'grid',
gridTemplateColumns: '55px 1fr auto',
gap: '6px',
padding: '4px 6px',
borderRadius: '3px',
marginBottom: '2px',
background: i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent',
cursor: report.lat && report.lon ? 'pointer' : 'default',
transition: 'background 0.15s',
borderLeft: '2px solid transparent'
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(68, 136, 255, 0.15)'}
onMouseLeave={(e) => e.currentTarget.style.background = i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent'}
>
{freqMHz}
{displayCall}
{grid && {grid}}
{report.mode}
{report.snr !== null && report.snr !== undefined && (
= 0 ? '#4ade80' : report.snr >= -10 ? '#fbbf24' : '#f97316',
fontWeight: '600'
}}>
{report.snr > 0 ? '+' : ''}{report.snr}
)}
{formatAge(report.age)}
);
})}
)}
);
};
export default PSKReporterPanel;
export { PSKReporterPanel };