diff --git a/src/App.jsx b/src/App.jsx index 184c04d..0fa88c0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -191,6 +191,7 @@ const App = () => { const mySpots = useMySpots(config.callsign); const satellites = useSatellites(config.location); const localWeather = useLocalWeather(config.location); + const pskReporter = usePSKReporter(config.callsign, { minutes: 15, enabled: config.callsign !== 'N0CALL' }); // Computed values const deGrid = useMemo(() => calculateGridSquare(config.location.lat, config.location.lon), [config.location]); @@ -460,11 +461,13 @@ const App = () => { dxPaths={dxPaths.data} dxFilters={dxFilters} satellites={satellites.data} + pskReporterSpots={[...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])]} showDXPaths={mapLayers.showDXPaths} showDXLabels={mapLayers.showDXLabels} onToggleDXLabels={toggleDXLabels} showPOTA={mapLayers.showPOTA} showSatellites={mapLayers.showSatellites} + showPSKReporter={mapLayers.showPSKReporter} onToggleSatellites={toggleSatellites} hoveredSpot={hoveredSpot} /> @@ -596,11 +599,13 @@ const App = () => { dxPaths={dxPaths.data} dxFilters={dxFilters} satellites={satellites.data} + pskReporterSpots={[...(pskReporter.txReports || []), ...(pskReporter.rxReports || [])]} showDXPaths={mapLayers.showDXPaths} showDXLabels={mapLayers.showDXLabels} onToggleDXLabels={toggleDXLabels} showPOTA={mapLayers.showPOTA} showSatellites={mapLayers.showSatellites} + showPSKReporter={mapLayers.showPSKReporter} onToggleSatellites={toggleSatellites} hoveredSpot={hoveredSpot} /> @@ -620,9 +625,9 @@ const App = () => { {/* RIGHT SIDEBAR */} -
- {/* DX Cluster - reduced size to make room for PSKReporter */} -
+
+ {/* DX Cluster - primary panel */} +
{ />
- {/* PSKReporter - where your digital signals are heard */} -
+ {/* PSKReporter - digital mode spots */} +
{ if (report.lat && report.lon) { setDxLocation({ lat: report.lat, lon: report.lon, call: report.receiver || report.sender }); @@ -649,13 +656,13 @@ const App = () => { />
- {/* DXpeditions - smaller */} -
+ {/* DXpeditions */} +
- {/* POTA - smaller */} -
+ {/* POTA */} +
{ />
- {/* Contests - smaller */} -
+ {/* Contests */} +
diff --git a/src/components/PSKReporterPanel.jsx b/src/components/PSKReporterPanel.jsx index b0c11f0..651f1e7 100644 --- a/src/components/PSKReporterPanel.jsx +++ b/src/components/PSKReporterPanel.jsx @@ -1,129 +1,152 @@ /** * PSKReporter Panel * Shows where your digital mode signals are being received + * Styled to match DXClusterPanel */ import React, { useState } from 'react'; import { usePSKReporter } from '../hooks/usePSKReporter.js'; +import { getBandColor } from '../utils/callsign.js'; -const PSKReporterPanel = ({ callsign, onShowOnMap }) => { - const [timeWindow, setTimeWindow] = useState(15); // minutes - const [activeTab, setActiveTab] = useState('tx'); // 'tx' or 'rx' +const PSKReporterPanel = ({ callsign, onShowOnMap, showOnMap, onToggleMap }) => { + const [timeWindow, setTimeWindow] = useState(15); + const [activeTab, setActiveTab] = useState('rx'); // Default to 'rx' (Hearing) - more useful const { txReports, txCount, rxReports, rxCount, - stats, loading, error, - lastUpdate, refresh } = usePSKReporter(callsign, { minutes: timeWindow, enabled: callsign && callsign !== 'N0CALL' }); - const formatTime = (timestamp) => { - const date = new Date(timestamp); - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: false - }) + 'z'; + const reports = activeTab === 'tx' ? txReports : rxReports; + const count = activeTab === 'tx' ? txCount : rxCount; + + // 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 === 1) return '1m ago'; - return `${minutes}m ago`; + if (minutes < 60) return `${minutes}m`; + return `${Math.floor(minutes/60)}h`; }; - const getSnrColor = (snr) => { - if (snr === null || snr === undefined) return 'var(--text-muted)'; - if (snr >= 0) return '#4ade80'; // Green - excellent - if (snr >= -10) return '#fbbf24'; // Yellow - good - if (snr >= -15) return '#f97316'; // Orange - fair - return '#ef4444'; // Red - weak - }; - - const reports = activeTab === 'tx' ? txReports : rxReports; - const count = activeTab === 'tx' ? txCount : rxCount; - if (!callsign || callsign === 'N0CALL') { return ( -
-
- πŸ“‘ -

PSKReporter

+
+
+ πŸ“‘ PSKReporter
-
-

- Set your callsign in Settings to see PSKReporter data -

+
+ Set callsign in Settings
); } return ( -
-
- πŸ“‘ -

PSKReporter

-
+
+ {/* Header - matches DX Cluster style */} +
+ πŸ“‘ PSKReporter +
- + {onToggleMap && ( + + )}
- {/* Tabs */} + {/* Tabs - compact style */}
-
- {error ? ( -
-
⚠️ PSKReporter temporarily unavailable
-
Will retry automatically
-
- ) : loading && reports.length === 0 ? ( -
- Loading... -
- ) : reports.length === 0 ? ( -
- No {activeTab === 'tx' ? 'reception reports' : 'stations heard'} in the last {timeWindow} minutes -
- (Make sure you're transmitting digital modes like FT8) -
-
- ) : ( - <> - {/* Summary stats for TX */} - {activeTab === 'tx' && txCount > 0 && ( -
-
- - {txCount} stations hearing you - - {stats.txBands.length > 0 && ( - - Bands: {stats.txBands.join(', ')} - - )} - {stats.txModes.length > 0 && ( - - Modes: {stats.txModes.slice(0, 3).join(', ')} - - )} + {/* Reports list - matches DX Cluster style */} + {error ? ( +
+ ⚠️ Temporarily unavailable +
+ ) : loading && reports.length === 0 ? ( +
+
+
+ ) : reports.length === 0 ? ( +
+ No {activeTab === 'tx' ? 'reception reports' : 'stations heard'} +
+ ) : ( +
+ {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}
-
- )} - - {/* Reports list */} -
- {reports.slice(0, 25).map((report, idx) => ( -
onShowOnMap && report.lat && report.lon && onShowOnMap(report)} - style={{ - display: 'grid', - gridTemplateColumns: '1fr auto auto auto', - gap: '8px', - padding: '6px 8px', - background: 'var(--bg-tertiary)', - borderRadius: '4px', - fontSize: '0.75rem', - cursor: report.lat && report.lon ? 'pointer' : 'default', - alignItems: 'center' - }} - > -
- - {activeTab === 'tx' ? report.receiver : report.sender} - - {(activeTab === 'tx' ? report.receiverGrid : report.senderGrid) && ( - - {activeTab === 'tx' ? report.receiverGrid : report.senderGrid} - - )} -
- -
- {report.freqMHz} {report.band} -
- -
- {report.mode} -
- -
- {report.snr !== null && ( - - {report.snr > 0 ? '+' : ''}{report.snr}dB - - )} +
+ {displayCall} + {grid && {grid}} +
+
+ {report.mode} + {report.snr !== null && report.snr !== undefined && ( = 0 ? '#4ade80' : report.snr >= -10 ? '#fbbf24' : '#f97316', + fontWeight: '600' }}> - {formatAge(report.age)} + {report.snr > 0 ? '+' : ''}{report.snr} -
+ )} + + {formatAge(report.age)} +
- ))} -
- - {reports.length > 25 && ( -
- Showing 25 of {reports.length} reports
- )} - - )} -
- - {/* Footer with last update */} - {lastUpdate && ( -
- Updated: {lastUpdate.toLocaleTimeString()} + ); + })}
)}
diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 6ad138d..7a349e8 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -21,11 +21,13 @@ export const WorldMap = ({ dxPaths, dxFilters, satellites, + pskReporterSpots, showDXPaths, showDXLabels, onToggleDXLabels, showPOTA, showSatellites, + showPSKReporter, onToggleSatellites, hoveredSpot }) => { @@ -44,6 +46,7 @@ export const WorldMap = ({ const dxPathsMarkersRef = useRef([]); const satMarkersRef = useRef([]); const satTracksRef = useRef([]); + const pskMarkersRef = useRef([]); // Load map style from localStorage const getStoredMapSettings = () => { @@ -416,6 +419,55 @@ export const WorldMap = ({ } }, [satellites, showSatellites]); + // Update PSKReporter markers + useEffect(() => { + if (!mapInstanceRef.current) return; + const map = mapInstanceRef.current; + + pskMarkersRef.current.forEach(m => map.removeLayer(m)); + pskMarkersRef.current = []; + + if (showPSKReporter && pskReporterSpots && pskReporterSpots.length > 0 && deLocation) { + pskReporterSpots.forEach(spot => { + if (spot.lat && spot.lon) { + const displayCall = spot.receiver || spot.sender; + const freqMHz = spot.freqMHz || (spot.freq ? (spot.freq / 1000000).toFixed(3) : '?'); + const bandColor = getBandColor(parseFloat(freqMHz)); + + // Draw line from DE to spot location + const points = getGreatCirclePoints( + [deLocation.lat, deLocation.lon], + [spot.lat, spot.lon], + 50 + ); + + const line = L.polyline(points, { + color: bandColor, + weight: 1.5, + opacity: 0.5, + dashArray: '4, 4' + }).addTo(map); + pskMarkersRef.current.push(line); + + // Add small dot marker at spot location + const circle = L.circleMarker([spot.lat, spot.lon], { + radius: 4, + fillColor: bandColor, + color: '#fff', + weight: 1, + opacity: 0.9, + fillOpacity: 0.8 + }).bindPopup(` + ${displayCall}
+ ${spot.mode} @ ${freqMHz} MHz
+ ${spot.snr !== null ? `SNR: ${spot.snr > 0 ? '+' : ''}${spot.snr} dB` : ''} + `).addTo(map); + pskMarkersRef.current.push(circle); + } + }); + } + }, [pskReporterSpots, showPSKReporter, deLocation]); + return (