/** * 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 (
πŸ“‘ PSKReporter
Set callsign in Settings
); } 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 ? (
Connecting to MQTT...
) : !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 };