/** * PSKReporter Panel * Shows where your digital mode signals are being received * Uses MQTT WebSocket for real-time data */ import React, { useState } from 'react'; import { usePSKReporter } from '../hooks/usePSKReporter.js'; import { getBandColor } from '../utils/callsign.js'; const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) => { const [timeWindow] = useState(15); // Keep spots for 15 minutes const [activeTab, setActiveTab] = useState('tx'); // Default to 'tx' (Being Heard) const { txReports, txCount, rxReports, rxCount, loading, error, connected, source, refresh } = usePSKReporter(callsign, { minutes: timeWindow, enabled: callsign && callsign !== 'N0CALL' }); const reports = activeTab === 'tx' ? txReports : rxReports; // 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 - matches DX Cluster style */}
πŸ“‘ PSKReporter {getStatusIndicator()}
{onToggleMap && ( )}
{/* Tabs - compact style */}
{/* Reports list - matches DX Cluster style */} {error && !connected ? (
⚠️ Connection failed - click πŸ”„ to retry
) : loading && reports.length === 0 ? (
Connecting to MQTT...
) : !connected && reports.length === 0 ? (
Waiting for connection...
) : reports.length === 0 ? (
{activeTab === 'tx' ? 'Waiting for spots... (TX to see reports)' : 'No stations heard yet'}
) : (
{reports.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 };