new icons and tips

pull/59/head
accius 2 days ago
parent 4403ddd657
commit e4a4e6f0ac

@ -20,7 +20,7 @@ export const BandConditionsPanel = ({ data, loading }) => {
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '12px' }}>
<div className="panel-header">📡 BAND CONDITIONS</div> <div className="panel-header"> BAND CONDITIONS</div>
{loading ? ( {loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<div className="loading-spinner" /> <div className="loading-spinner" />

@ -102,7 +102,7 @@ export const ContestPanel = ({ data, loading }) => {
color: 'var(--accent-primary)', color: 'var(--accent-primary)',
fontWeight: '700' fontWeight: '700'
}}> }}>
<span>🏆 CONTESTS</span> <span> CONTESTS</span>
{liveCount > 0 && ( {liveCount > 0 && (
<span style={{ <span style={{
background: 'rgba(239, 68, 68, 0.3)', background: 'rgba(239, 68, 68, 0.3)',

@ -4,6 +4,7 @@
*/ */
import React from 'react'; import React from 'react';
import { getBandColor } from '../utils/callsign.js'; import { getBandColor } from '../utils/callsign.js';
import { IconSearch, IconMap, IconGlobe } from './Icons.jsx';
export const DXClusterPanel = ({ export const DXClusterPanel = ({
data, data,
@ -52,11 +53,12 @@ export const DXClusterPanel = ({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}}> }}>
<span>🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '10px' }}> LIVE</span></span> <span><IconGlobe size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '10px' }}> LIVE</span></span>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>{spots.length}/{totalSpots || spots.length}</span> <span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>{spots.length}/{totalSpots || spots.length}</span>
<button <button
onClick={onOpenFilters} onClick={onOpenFilters}
title="Filter DX spots by band, mode, or continent"
style={{ style={{
background: filterCount > 0 ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.3)', background: filterCount > 0 ? 'rgba(255, 170, 0, 0.3)' : 'rgba(100, 100, 100, 0.3)',
border: `1px solid ${filterCount > 0 ? '#ffaa00' : '#666'}`, border: `1px solid ${filterCount > 0 ? '#ffaa00' : '#666'}`,
@ -68,10 +70,11 @@ export const DXClusterPanel = ({
cursor: 'pointer' cursor: 'pointer'
}} }}
> >
🔍 Filters <IconSearch size={10} style={{ verticalAlign: 'middle', marginRight: '3px' }} />Filters
</button> </button>
<button <button
onClick={onToggleMap} onClick={onToggleMap}
title={showOnMap ? 'Hide DX spots on map' : 'Show DX spots on map'}
style={{ style={{
background: showOnMap ? 'rgba(68, 136, 255, 0.3)' : 'rgba(100, 100, 100, 0.3)', background: showOnMap ? 'rgba(68, 136, 255, 0.3)' : 'rgba(100, 100, 100, 0.3)',
border: `1px solid ${showOnMap ? '#4488ff' : '#666'}`, border: `1px solid ${showOnMap ? '#4488ff' : '#666'}`,
@ -83,7 +86,7 @@ export const DXClusterPanel = ({
cursor: 'pointer' cursor: 'pointer'
}} }}
> >
🗺 {showOnMap ? 'ON' : 'OFF'} <IconMap size={10} style={{ verticalAlign: 'middle', marginRight: '3px' }} />{showOnMap ? 'ON' : 'OFF'}
</button> </button>
</div> </div>
</div> </div>

@ -416,7 +416,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
}}> }}>
<div> <div>
<div style={{ fontSize: '18px', fontWeight: '700', color: 'var(--accent-cyan)' }}> <div style={{ fontSize: '18px', fontWeight: '700', color: 'var(--accent-cyan)' }}>
🔍 DX Cluster Filters DX Cluster Filters
</div> </div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}> <div style={{ fontSize: '12px', color: 'var(--text-muted)', marginTop: '2px' }}>
{getActiveFilterCount()} filters active {getActiveFilterCount()} filters active
@ -462,7 +462,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
<button onClick={() => setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>Modes</button> <button onClick={() => setActiveTab('modes')} style={tabStyle(activeTab === 'modes')}>Modes</button>
<button onClick={() => setActiveTab('watchlist')} style={tabStyle(activeTab === 'watchlist')}>Watchlist</button> <button onClick={() => setActiveTab('watchlist')} style={tabStyle(activeTab === 'watchlist')}>Watchlist</button>
<button onClick={() => setActiveTab('exclude')} style={tabStyle(activeTab === 'exclude')}>Exclude</button> <button onClick={() => setActiveTab('exclude')} style={tabStyle(activeTab === 'exclude')}>Exclude</button>
<button onClick={() => setActiveTab('settings')} style={tabStyle(activeTab === 'settings')}> Settings</button> <button onClick={() => setActiveTab('settings')} style={tabStyle(activeTab === 'settings')}> Settings</button>
</div> </div>
{/* Tab Content */} {/* Tab Content */}

@ -24,7 +24,7 @@ export const DXpeditionPanel = ({ data, loading }) => {
marginBottom: '6px', marginBottom: '6px',
fontSize: '11px' fontSize: '11px'
}}> }}>
<span>🌍 DXPEDITIONS</span> <span> DXPEDITIONS</span>
{data && ( {data && (
<span style={{ fontSize: '9px', color: 'var(--text-muted)' }}> <span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>
{data.active > 0 && <span style={{ color: 'var(--accent-green)' }}>{data.active} active</span>} {data.active > 0 && <span style={{ color: 'var(--accent-green)' }}>{data.active} active</span>}

@ -3,7 +3,7 @@
* Top bar with callsign, clocks, weather, and controls * Top bar with callsign, clocks, weather, and controls
*/ */
import React from 'react'; import React from 'react';
import { IconGear, IconExpand, IconShrink } from './Icons.jsx';
export const Header = ({ export const Header = ({
config, config,
utcTime, utcTime,
@ -146,7 +146,7 @@ export const Header = ({
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
> >
Settings <IconGear size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />Settings
</button> </button>
<button <button
onClick={onFullscreenToggle} onClick={onFullscreenToggle}
@ -162,7 +162,10 @@ export const Header = ({
}} }}
title={isFullscreen ? "Exit Fullscreen (Esc)" : "Enter Fullscreen"} title={isFullscreen ? "Exit Fullscreen (Esc)" : "Enter Fullscreen"}
> >
{isFullscreen ? '⛶ Exit' : '⛶ Full'} {isFullscreen
? <><IconShrink size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />Exit</>
: <><IconExpand size={12} style={{ verticalAlign: 'middle', marginRight: '4px' }} />Full</>
}
</button> </button>
</div> </div>
</div> </div>

@ -0,0 +1,170 @@
/**
* SVG Icons for OpenHamClock
*
* Cross-platform icons that render identically on all browsers and operating systems.
* Replaces emoji which render as tofu/boxes on Linux Chromium without emoji fonts.
*
* All icons accept: size (default 14), color (default 'currentColor'), style, className
*/
import React from 'react';
const defaults = { size: 14, color: 'currentColor' };
// Magnifying glass / Search / Filter
export const IconSearch = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" {...props}>
<circle cx="6.5" cy="6.5" r="4.5" />
<line x1="10" y1="10" x2="14" y2="14" />
</svg>
);
// Refresh / Reload
export const IconRefresh = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M13.5 2.5v4h-4" />
<path d="M2.5 13.5v-4h4" />
<path d="M3.5 5.5a5.5 5.5 0 0 1 9.1-1l.9.9" />
<path d="M12.5 10.5a5.5 5.5 0 0 1-9.1 1l-.9-.9" />
</svg>
);
// Map
export const IconMap = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M1 3.5l4.5-2 5 2.5 4.5-2v11l-4.5 2-5-2.5L1 14.5z" />
<line x1="5.5" y1="1.5" x2="5.5" y2="12" />
<line x1="10.5" y1="4" x2="10.5" y2="14.5" />
</svg>
);
// Gear / Settings
export const IconGear = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="8" cy="8" r="2.2" />
<path d="M8 1.5l.7 1.8a4.5 4.5 0 0 1 1.6.9l1.9-.4 1 1.7-1.2 1.4c.1.4.1.7 0 1.1l1.2 1.4-1 1.7-1.9-.4a4.5 4.5 0 0 1-1.6.9L8 14.5l-.7-1.8a4.5 4.5 0 0 1-1.6-.9l-1.9.4-1-1.7 1.2-1.4c-.1-.4-.1-.7 0-1.1L3.8 6.6l1-1.7 1.9.4a4.5 4.5 0 0 1 1.6-.9z" />
</svg>
);
// Globe / World
export const IconGlobe = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" {...props}>
<circle cx="8" cy="8" r="6.5" />
<ellipse cx="8" cy="8" rx="2.8" ry="6.5" />
<line x1="1.5" y1="8" x2="14.5" y2="8" />
</svg>
);
// Satellite
export const IconSatellite = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<rect x="5" y="5" width="6" height="6" rx="1" transform="rotate(45 8 8)" />
<line x1="2" y1="2" x2="4.5" y2="4.5" />
<line x1="11.5" y1="11.5" x2="14" y2="14" />
<path d="M3.5 6.5 A4 4 0 0 0 6.5 3.5" />
</svg>
);
// Antenna / Radio
export const IconAntenna = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<line x1="8" y1="6" x2="8" y2="15" />
<line x1="5" y1="15" x2="11" y2="15" />
<path d="M4 4a5.5 5.5 0 0 1 8 0" />
<path d="M2 2a9 9 0 0 1 12 0" />
<circle cx="8" cy="6" r="1" fill={color} stroke="none" />
</svg>
);
// Sun
export const IconSun = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" {...props}>
<circle cx="8" cy="8" r="3" />
<line x1="8" y1="1" x2="8" y2="3" />
<line x1="8" y1="13" x2="8" y2="15" />
<line x1="1" y1="8" x2="3" y2="8" />
<line x1="13" y1="8" x2="15" y2="8" />
<line x1="3.05" y1="3.05" x2="4.46" y2="4.46" />
<line x1="11.54" y1="11.54" x2="12.95" y2="12.95" />
<line x1="3.05" y1="12.95" x2="4.46" y2="11.54" />
<line x1="11.54" y1="4.46" x2="12.95" y2="3.05" />
</svg>
);
// Moon
export const IconMoon = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M13.5 8.5a6 6 0 1 1-6-6 4.5 4.5 0 0 0 6 6z" />
</svg>
);
// Trophy / Contest
export const IconTrophy = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M5 2h6v5a3 3 0 0 1-6 0z" />
<path d="M5 4H3a1.5 1.5 0 0 0 0 3h2" />
<path d="M11 4h2a1.5 1.5 0 0 1 0 3h-2" />
<line x1="8" y1="10" x2="8" y2="12" />
<line x1="5.5" y1="14" x2="10.5" y2="14" />
<line x1="6" y1="12" x2="10" y2="12" />
</svg>
);
// Tent / POTA / Camping
export const IconTent = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M8 2L1.5 14h13z" />
<path d="M8 2v12" />
<path d="M6 14l2-5 2 5" />
</svg>
);
// Earth / DXpedition
export const IconEarth = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" {...props}>
<circle cx="8" cy="8" r="6.5" />
<path d="M1.5 6h13M1.5 10h13" />
<ellipse cx="8" cy="8" rx="3" ry="6.5" />
</svg>
);
// Pin / Location
export const IconPin = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M8 1.5A4.5 4.5 0 0 0 3.5 6c0 3.5 4.5 8.5 4.5 8.5s4.5-5 4.5-8.5A4.5 4.5 0 0 0 8 1.5z" />
<circle cx="8" cy="6" r="1.5" />
</svg>
);
// Tag / Label
export const IconTag = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M1.5 1.5h6l7 7-6 6-7-7z" />
<circle cx="5" cy="5" r="1" fill={color} stroke="none" />
</svg>
);
// Fullscreen expand
export const IconExpand = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...props}>
<polyline points="10,1 15,1 15,6" />
<polyline points="6,15 1,15 1,10" />
<line x1="15" y1="1" x2="9.5" y2="6.5" />
<line x1="1" y1="15" x2="6.5" y2="9.5" />
</svg>
);
// Fullscreen shrink
export const IconShrink = ({ size = defaults.size, color = defaults.color, ...props }) => (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...props}>
<polyline points="5,10 0,15" />
<polyline points="11,6 16,1" />
<polyline points="6,11 6,15 2,15" />
<polyline points="10,5 10,1 14,1" />
</svg>
);
export default {
IconSearch, IconRefresh, IconMap, IconGear, IconGlobe, IconSatellite,
IconAntenna, IconSun, IconMoon, IconTrophy, IconTent, IconEarth,
IconPin, IconTag, IconExpand, IconShrink,
};

@ -21,7 +21,7 @@ export const LocationPanel = ({
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '12px' }}>
<div className="panel-header">📍 LOCATIONS</div> <div className="panel-header"> LOCATIONS</div>
{/* DE Location */} {/* DE Location */}
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>

@ -14,9 +14,10 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
marginBottom: '6px', marginBottom: '6px',
fontSize: '11px' fontSize: '11px'
}}> }}>
<span>🏕 POTA ACTIVATORS</span> <span> POTA ACTIVATORS</span>
<button <button
onClick={onToggleMap} onClick={onToggleMap}
title={showOnMap ? 'Hide POTA activators on map' : 'Show POTA activators on map'}
style={{ style={{
background: showOnMap ? 'rgba(170, 102, 255, 0.3)' : 'rgba(100, 100, 100, 0.3)', background: showOnMap ? 'rgba(170, 102, 255, 0.3)' : 'rgba(100, 100, 100, 0.3)',
border: `1px solid ${showOnMap ? '#aa66ff' : '#666'}`, border: `1px solid ${showOnMap ? '#aa66ff' : '#666'}`,
@ -28,7 +29,7 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
cursor: 'pointer' cursor: 'pointer'
}} }}
> >
🗺 {showOnMap ? 'ON' : 'OFF'} Map {showOnMap ? 'ON' : 'OFF'}
</button> </button>
</div> </div>

@ -310,7 +310,7 @@ export const PSKFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =
}}> }}>
<div> <div>
<h3 style={{ margin: 0, fontSize: '16px', color: 'var(--text-primary)' }}> <h3 style={{ margin: 0, fontSize: '16px', color: 'var(--text-primary)' }}>
📡 PSKReporter Filters PSKReporter Filters
</h3> </h3>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}> <span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
{getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active {getActiveFilterCount()} filter{getActiveFilterCount() !== 1 ? 's' : ''} active

@ -10,6 +10,7 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { usePSKReporter } from '../hooks/usePSKReporter.js'; import { usePSKReporter } from '../hooks/usePSKReporter.js';
import { getBandColor } from '../utils/callsign.js'; import { getBandColor } from '../utils/callsign.js';
import { IconSearch, IconRefresh, IconMap } from './Icons.jsx';
const PSKReporterPanel = ({ const PSKReporterPanel = ({
callsign, callsign,
@ -182,10 +183,10 @@ const PSKReporterPanel = ({
}}> }}>
{/* Mode toggle */} {/* Mode toggle */}
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<button onClick={() => setPanelModePersist('psk')} style={segBtn(panelMode === 'psk', 'var(--accent-primary)')}> <button onClick={() => setPanelModePersist('psk')} style={segBtn(panelMode === 'psk', 'var(--accent-primary)')} title="Internet-based reception reports via PSKReporter.info">
PSKReporter PSKReporter
</button> </button>
<button onClick={() => setPanelModePersist('wsjtx')} style={segBtn(panelMode === 'wsjtx', '#a78bfa')}> <button onClick={() => setPanelModePersist('wsjtx')} style={segBtn(panelMode === 'wsjtx', '#a78bfa')} title="Local WSJT-X decodes via UDP relay">
WSJT-X WSJT-X
</button> </button>
</div> </div>
@ -198,14 +199,14 @@ const PSKReporterPanel = ({
{statusDot && ( {statusDot && (
<span style={{ color: statusDot.color, fontSize: '10px', lineHeight: 1 }}>{statusDot.char}</span> <span style={{ color: statusDot.color, fontSize: '10px', lineHeight: 1 }}>{statusDot.char}</span>
)} )}
<button onClick={onOpenFilters} style={iconBtn(pskFilterCount > 0, '#ffaa00')}> <button onClick={onOpenFilters} style={iconBtn(pskFilterCount > 0, '#ffaa00')} title="Filter spots by band, mode, or grid">
{pskFilterCount > 0 ? `🔍${pskFilterCount}` : '🔍'} <IconSearch size={11} style={{ verticalAlign: 'middle' }} />{pskFilterCount > 0 ? pskFilterCount : ''}
</button> </button>
<button onClick={refresh} disabled={loading} style={{ <button onClick={refresh} disabled={loading} style={{
...iconBtn(false), ...iconBtn(false),
opacity: loading ? 0.4 : 1, opacity: loading ? 0.4 : 1,
cursor: loading ? 'not-allowed' : 'pointer', cursor: loading ? 'not-allowed' : 'pointer',
}}>🔄</button> }} title="Reconnect to PSKReporter"><IconRefresh size={11} style={{ verticalAlign: 'middle' }} /></button>
</> </>
)} )}
@ -241,8 +242,8 @@ const PSKReporterPanel = ({
{/* Map toggle (always visible) */} {/* Map toggle (always visible) */}
{handleMapToggle && ( {handleMapToggle && (
<button onClick={handleMapToggle} style={iconBtn(isMapOn, panelMode === 'psk' ? '#4488ff' : '#a78bfa')}> <button onClick={handleMapToggle} style={iconBtn(isMapOn, panelMode === 'psk' ? '#4488ff' : '#a78bfa')} title={isMapOn ? 'Hide spots on map' : 'Show spots on map'}>
🗺 <IconMap size={11} style={{ verticalAlign: 'middle' }} />
</button> </button>
)} )}
</div> </div>
@ -252,19 +253,19 @@ const PSKReporterPanel = ({
<div style={{ display: 'flex', gap: '4px', marginBottom: '5px', flexShrink: 0 }}> <div style={{ display: 'flex', gap: '4px', marginBottom: '5px', flexShrink: 0 }}>
{panelMode === 'psk' ? ( {panelMode === 'psk' ? (
<> <>
<button onClick={() => setActiveTabPersist('tx')} style={subTabBtn(activeTab === 'tx', '#4ade80')}> <button onClick={() => setActiveTabPersist('tx')} style={subTabBtn(activeTab === 'tx', '#4ade80')} title="Stations hearing your signal">
Heard ({pskFilterCount > 0 ? filteredTx.length : txCount}) Heard ({pskFilterCount > 0 ? filteredTx.length : txCount})
</button> </button>
<button onClick={() => setActiveTabPersist('rx')} style={subTabBtn(activeTab === 'rx', '#60a5fa')}> <button onClick={() => setActiveTabPersist('rx')} style={subTabBtn(activeTab === 'rx', '#60a5fa')} title="Stations you are hearing">
Hearing ({pskFilterCount > 0 ? filteredRx.length : rxCount}) Hearing ({pskFilterCount > 0 ? filteredRx.length : rxCount})
</button> </button>
</> </>
) : ( ) : (
<> <>
<button onClick={() => setWsjtxTab('decodes')} style={subTabBtn(wsjtxTab === 'decodes', '#a78bfa')}> <button onClick={() => setWsjtxTab('decodes')} style={subTabBtn(wsjtxTab === 'decodes', '#a78bfa')} title="Live WSJT-X decodes">
Decodes ({filteredDecodes.length}) Decodes ({filteredDecodes.length})
</button> </button>
<button onClick={() => setWsjtxTab('qsos')} style={subTabBtn(wsjtxTab === 'qsos', '#a78bfa')}> <button onClick={() => setWsjtxTab('qsos')} style={subTabBtn(wsjtxTab === 'qsos', '#a78bfa')} title="Logged QSOs from WSJT-X">
QSOs ({wsjtxQsos.length}) QSOs ({wsjtxQsos.length})
</button> </button>
</> </>
@ -283,7 +284,7 @@ const PSKReporterPanel = ({
</div> </div>
) : error && !connected ? ( ) : error && !connected ? (
<div style={{ textAlign: 'center', padding: '12px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '12px', color: 'var(--text-muted)', fontSize: '11px' }}>
Connection failed tap 🔄 Connection failed tap refresh
</div> </div>
) : loading && filteredReports.length === 0 && pskFilterCount === 0 ? ( ) : loading && filteredReports.length === 0 && pskFilterCount === 0 ? (
<div style={{ textAlign: 'center', padding: '16px', color: 'var(--text-muted)', fontSize: '11px' }}> <div style={{ textAlign: 'center', padding: '16px', color: 'var(--text-muted)', fontSize: '11px' }}>

@ -34,7 +34,7 @@ export const PropagationPanel = ({ propagation, loading, bandConditions }) => {
if (loading || !propagation) { if (loading || !propagation) {
return ( return (
<div className="panel"> <div className="panel">
<div className="panel-header">📡 VOACAP</div> <div className="panel-header"> VOACAP</div>
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}> <div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}>
Loading predictions... Loading predictions...
</div> </div>
@ -81,7 +81,7 @@ export const PropagationPanel = ({ propagation, loading, bandConditions }) => {
<div className="panel" style={{ cursor: 'pointer' }} onClick={cycleViewMode}> <div className="panel" style={{ cursor: 'pointer' }} onClick={cycleViewMode}>
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span> <span>
{viewMode === 'bands' ? '📊 BAND CONDITIONS' : '📡 VOACAP'} {viewMode === 'bands' ? '◫ BAND CONDITIONS' : '⌇ VOACAP'}
{hasRealData && viewMode !== 'bands' && <span style={{ color: '#00ff88', fontSize: '10px', marginLeft: '4px' }}></span>} {hasRealData && viewMode !== 'bands' && <span style={{ color: '#00ff88', fontSize: '10px', marginLeft: '4px' }}></span>}
</span> </span>
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}> <span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>
@ -143,7 +143,7 @@ export const PropagationPanel = ({ propagation, loading, bandConditions }) => {
</div> </div>
<span style={{ color: hasRealData ? '#00ff88' : 'var(--text-muted)', fontSize: '10px' }}> <span style={{ color: hasRealData ? '#00ff88' : 'var(--text-muted)', fontSize: '10px' }}>
{hasRealData {hasRealData
? `📡 ${ionospheric?.source || 'ionosonde'}${ionospheric?.distance ? ` (${ionospheric.distance}km)` : ''}` ? ` ${ionospheric?.source || 'ionosonde'}${ionospheric?.distance ? ` (${ionospheric.distance}km)` : ''}`
: '⚡ estimated' : '⚡ estimated'
} }
</span> </span>

@ -232,7 +232,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}} }}
> >
📡 Station Station
</button> </button>
<button <button
onClick={() => setActiveTab('layers')} onClick={() => setActiveTab('layers')}
@ -249,7 +249,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}} }}
> >
🗺 Map Layers Map Layers
</button> </button>
</div> </div>
@ -592,7 +592,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{/* Language */} {/* Language */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}> <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
🌐 {t('station.settings.language')} {t('station.settings.language')}
</label> </label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}>
{LANGUAGES.map((lang) => ( {LANGUAGES.map((lang) => (

@ -7,7 +7,7 @@ import { getMoonPhase } from '../utils/geo.js';
const MODES = ['image', 'indices', 'xray', 'lunar']; const MODES = ['image', 'indices', 'xray', 'lunar'];
const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX', lunar: 'LUNAR' }; const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX', lunar: 'LUNAR' };
const MODE_ICONS = { image: '📊', indices: '📈', xray: '🌙', lunar: '☀️' }; const MODE_ICONS = { image: '◫', indices: '⊞', xray: '☽', lunar: '☼' };
const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show lunar phase', lunar: 'Show solar image' }; const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show lunar phase', lunar: 'Show solar image' };
// Flare class from flux value (W/m²) // Flare class from flux value (W/m²)
@ -389,13 +389,13 @@ export const SolarPanel = ({ solarIndices }) => {
<div style={{ <div style={{
background: 'var(--bg-tertiary)', borderRadius: '4px', padding: '4px 8px', textAlign: 'center', background: 'var(--bg-tertiary)', borderRadius: '4px', padding: '4px 8px', textAlign: 'center',
}}> }}>
<div style={{ color: 'var(--text-muted)' }}>🌑 New</div> <div style={{ color: 'var(--text-muted)' }}> New</div>
<div style={{ color: 'var(--text-secondary)', fontWeight: '600' }}>{nextNew}</div> <div style={{ color: 'var(--text-secondary)', fontWeight: '600' }}>{nextNew}</div>
</div> </div>
<div style={{ <div style={{
background: 'var(--bg-tertiary)', borderRadius: '4px', padding: '4px 8px', textAlign: 'center', background: 'var(--bg-tertiary)', borderRadius: '4px', padding: '4px 8px', textAlign: 'center',
}}> }}>
<div style={{ color: 'var(--text-muted)' }}>🌕 Full</div> <div style={{ color: 'var(--text-muted)' }}> Full</div>
<div style={{ color: 'var(--text-secondary)', fontWeight: '600' }}>{nextFull}</div> <div style={{ color: 'var(--text-secondary)', fontWeight: '600' }}>{nextFull}</div>
</div> </div>
</div> </div>

@ -15,7 +15,7 @@ export const SpaceWeatherPanel = ({ data, loading }) => {
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '12px' }}>
<div className="panel-header"> SPACE WEATHER</div> <div className="panel-header"> SPACE WEATHER</div>
{loading ? ( {loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<div className="loading-spinner" /> <div className="loading-spinner" />

@ -13,6 +13,7 @@ import {
import { filterDXPaths, getBandColor } from '../utils/callsign.js'; import { filterDXPaths, getBandColor } from '../utils/callsign.js';
import { getAllLayers } from '../plugins/layerRegistry.js'; import { getAllLayers } from '../plugins/layerRegistry.js';
import { IconSatellite, IconTag, IconSun, IconMoon } from './Icons.jsx';
import PluginLayer from './PluginLayer.jsx'; import PluginLayer from './PluginLayer.jsx';
import { DXNewsTicker } from './DXNewsTicker.jsx'; import { DXNewsTicker } from './DXNewsTicker.jsx';
@ -299,24 +300,24 @@ export const WorldMap = ({
const sunPos = getSunPosition(new Date()); const sunPos = getSunPosition(new Date());
const sunIcon = L.divIcon({ const sunIcon = L.divIcon({
className: 'custom-marker sun-marker', className: 'custom-marker sun-marker',
html: '', html: '',
iconSize: [24, 24], iconSize: [24, 24],
iconAnchor: [12, 12] iconAnchor: [12, 12]
}); });
sunMarkerRef.current = L.marker([sunPos.lat, sunPos.lon], { icon: sunIcon }) sunMarkerRef.current = L.marker([sunPos.lat, sunPos.lon], { icon: sunIcon })
.bindPopup(`<b> Subsolar Point</b><br>${sunPos.lat.toFixed(2)}°, ${sunPos.lon.toFixed(2)}°`) .bindPopup(`<b> Subsolar Point</b><br>${sunPos.lat.toFixed(2)}°, ${sunPos.lon.toFixed(2)}°`)
.addTo(map); .addTo(map);
// Moon marker // Moon marker
const moonPos = getMoonPosition(new Date()); const moonPos = getMoonPosition(new Date());
const moonIcon = L.divIcon({ const moonIcon = L.divIcon({
className: 'custom-marker moon-marker', className: 'custom-marker moon-marker',
html: '🌙', html: '',
iconSize: [24, 24], iconSize: [24, 24],
iconAnchor: [12, 12] iconAnchor: [12, 12]
}); });
moonMarkerRef.current = L.marker([moonPos.lat, moonPos.lon], { icon: moonIcon }) moonMarkerRef.current = L.marker([moonPos.lat, moonPos.lon], { icon: moonIcon })
.bindPopup(`<b>🌙 Sublunar Point</b><br>${moonPos.lat.toFixed(2)}°, ${moonPos.lon.toFixed(2)}°`) .bindPopup(`<b> Sublunar Point</b><br>${moonPos.lat.toFixed(2)}°, ${moonPos.lon.toFixed(2)}°`)
.addTo(map); .addTo(map);
}, [deLocation, dxLocation]); }, [deLocation, dxLocation]);
@ -480,14 +481,14 @@ export const WorldMap = ({
// Add satellite marker icon // Add satellite marker icon
const icon = L.divIcon({ const icon = L.divIcon({
className: '', className: '',
html: `<span style="display:inline-block;background:${sat.visible ? satColor : satColorDark};color:${sat.visible ? '#000' : '#fff'};padding:4px 8px;border-radius:4px;font-size:11px;font-family:'JetBrains Mono',monospace;white-space:nowrap;border:2px solid ${sat.visible ? '#fff' : '#666'};font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.4);">🛰 ${sat.name}</span>`, html: `<span style="display:inline-block;background:${sat.visible ? satColor : satColorDark};color:${sat.visible ? '#000' : '#fff'};padding:4px 8px;border-radius:4px;font-size:11px;font-family:'JetBrains Mono',monospace;white-space:nowrap;border:2px solid ${sat.visible ? '#fff' : '#666'};font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.4);"> ${sat.name}</span>`,
iconSize: null, iconSize: null,
iconAnchor: [0, 0] iconAnchor: [0, 0]
}); });
const marker = L.marker([sat.lat, sat.lon], { icon }) const marker = L.marker([sat.lat, sat.lon], { icon })
.bindPopup(` .bindPopup(`
<b>🛰 ${sat.name}</b><br> <b> ${sat.name}</b><br>
<table style="font-size: 11px;"> <table style="font-size: 11px;">
<tr><td>Mode:</td><td><b>${sat.mode || 'Unknown'}</b></td></tr> <tr><td>Mode:</td><td><b>${sat.mode || 'Unknown'}</b></td></tr>
<tr><td>Alt:</td><td>${sat.alt} km</td></tr> <tr><td>Alt:</td><td>${sat.alt} km</td></tr>
@ -777,6 +778,7 @@ export const WorldMap = ({
{onToggleSatellites && ( {onToggleSatellites && (
<button <button
onClick={onToggleSatellites} onClick={onToggleSatellites}
title={showSatellites ? 'Hide satellite tracks' : 'Show satellite tracks'}
style={{ style={{
position: 'absolute', position: 'absolute',
top: '10px', top: '10px',
@ -792,7 +794,7 @@ export const WorldMap = ({
zIndex: 1000 zIndex: 1000
}} }}
> >
🛰 SAT {showSatellites ? 'ON' : 'OFF'} SAT {showSatellites ? 'ON' : 'OFF'}
</button> </button>
)} )}
@ -800,6 +802,7 @@ export const WorldMap = ({
{onToggleDXLabels && showDXPaths && ( {onToggleDXLabels && showDXPaths && (
<button <button
onClick={onToggleDXLabels} onClick={onToggleDXLabels}
title={showDXLabels ? 'Hide callsign labels on map' : 'Show callsign labels on map'}
style={{ style={{
position: 'absolute', position: 'absolute',
top: '10px', top: '10px',
@ -815,7 +818,7 @@ export const WorldMap = ({
zIndex: 1000 zIndex: 1000
}} }}
> >
🏷 CALLS {showDXLabels ? 'ON' : 'OFF'} CALLS {showDXLabels ? 'ON' : 'OFF'}
</button> </button>
)} )}
@ -865,8 +868,8 @@ export const WorldMap = ({
</div> </div>
)} )}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<span style={{ color: '#ffcc00' }}> Sun</span> <span style={{ color: '#ffcc00' }}> Sun</span>
<span style={{ color: '#aaaaaa' }}>🌙 Moon</span> <span style={{ color: '#aaaaaa' }}> Moon</span>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save

Powered by TurnKey Linux.