fixing layout and missing freqs

pull/6/head
accius 3 days ago
parent 6adc7e9fdf
commit 6afa55c7bd

@ -330,34 +330,42 @@ const App = () => {
</div> </div>
{/* RIGHT SIDEBAR */} {/* RIGHT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', overflow: 'hidden' }}>
{/* DX Cluster */} {/* DX Cluster - takes most space */}
<DXClusterPanel <div style={{ flex: '2 1 0', minHeight: '250px', overflow: 'hidden' }}>
data={dxCluster.data} <DXClusterPanel
loading={dxCluster.loading} data={dxCluster.data}
totalSpots={dxCluster.totalSpots} loading={dxCluster.loading}
filters={dxFilters} totalSpots={dxCluster.totalSpots}
onFilterChange={setDxFilters} filters={dxFilters}
onOpenFilters={() => setShowDXFilters(true)} onFilterChange={setDxFilters}
onHoverSpot={setHoveredSpot} onOpenFilters={() => setShowDXFilters(true)}
hoveredSpot={hoveredSpot} onHoverSpot={setHoveredSpot}
showOnMap={mapLayers.showDXPaths} hoveredSpot={hoveredSpot}
onToggleMap={toggleDXPaths} showOnMap={mapLayers.showDXPaths}
/> onToggleMap={toggleDXPaths}
/>
</div>
{/* DXpeditions */} {/* DXpeditions - smaller */}
<DXpeditionPanel data={dxpeditions.data} loading={dxpeditions.loading} /> <div style={{ flex: '0 0 auto', maxHeight: '140px', overflow: 'hidden' }}>
<DXpeditionPanel data={dxpeditions.data} loading={dxpeditions.loading} />
</div>
{/* POTA */} {/* POTA - smaller */}
<POTAPanel <div style={{ flex: '0 0 auto', maxHeight: '120px', overflow: 'hidden' }}>
data={potaSpots.data} <POTAPanel
loading={potaSpots.loading} data={potaSpots.data}
showOnMap={mapLayers.showPOTA} loading={potaSpots.loading}
onToggleMap={togglePOTA} showOnMap={mapLayers.showPOTA}
/> onToggleMap={togglePOTA}
/>
</div>
{/* Contests */} {/* Contests - smaller */}
<ContestPanel data={contests.data} loading={contests.loading} /> <div style={{ flex: '0 0 auto', maxHeight: '150px', overflow: 'hidden' }}>
<ContestPanel data={contests.data} loading={contests.loading} />
</div>
</div> </div>
</div> </div>

@ -1,68 +1,72 @@
/** /**
* ContestPanel Component * ContestPanel Component
* Displays upcoming amateur radio contests * Displays upcoming contests (compact version)
*/ */
import React from 'react'; import React from 'react';
export const ContestPanel = ({ data, loading }) => { export const ContestPanel = ({ data, loading }) => {
const getModeColor = (mode) => {
switch(mode) {
case 'CW': return 'var(--accent-cyan)';
case 'SSB': return 'var(--accent-amber)';
case 'RTTY': return 'var(--accent-purple)';
case 'FT8': case 'FT4': return 'var(--accent-green)';
default: return 'var(--text-secondary)';
}
};
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '8px', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="panel-header">🏆 CONTESTS</div> <div className="panel-header" style={{
{loading ? ( marginBottom: '6px',
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> fontSize: '11px'
<div className="loading-spinner" /> }}>
</div> 🏆 CONTESTS
) : data.length === 0 ? ( </div>
<div style={{
textAlign: 'center', <div style={{ flex: 1, overflowY: 'auto' }}>
padding: '20px', {loading ? (
color: 'var(--text-muted)', <div style={{ display: 'flex', justifyContent: 'center', padding: '10px' }}>
fontSize: '12px' <div className="loading-spinner" />
}}> </div>
No upcoming contests ) : data && data.length > 0 ? (
</div> <div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono, monospace' }}>
) : ( {data.slice(0, 5).map((contest, i) => (
<div style={{ <div
fontSize: '11px', key={`${contest.name}-${i}`}
fontFamily: 'JetBrains Mono, monospace' style={{
}}> padding: '4px 0',
{data.slice(0, 5).map((contest, i) => ( borderBottom: i < Math.min(data.length, 5) - 1 ? '1px solid var(--border-color)' : 'none'
<div }}
key={`${contest.name}-${i}`} >
style={{ <div style={{
padding: '8px', color: 'var(--text-primary)',
borderRadius: '4px', fontWeight: '600',
marginBottom: '4px', whiteSpace: 'nowrap',
background: i % 2 === 0 ? 'rgba(255,255,255,0.02)' : 'transparent' overflow: 'hidden',
}} textOverflow: 'ellipsis'
>
<div style={{
color: 'var(--accent-cyan)',
fontWeight: '600',
marginBottom: '4px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{contest.name}
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
color: 'var(--text-muted)',
fontSize: '10px'
}}>
<span>{contest.startDate}</span>
<span style={{
color: contest.isActive ? 'var(--accent-green)' : 'var(--text-muted)'
}}> }}>
{contest.isActive ? '● ACTIVE' : contest.timeUntil || ''} {contest.name}
</span> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2px' }}>
<span style={{ color: getModeColor(contest.mode) }}>{contest.mode}</span>
<span style={{ color: 'var(--text-muted)' }}>{formatDate(contest.start)}</span>
</div>
</div> </div>
</div> ))}
))} </div>
</div> ) : (
)} <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '10px', fontSize: '11px' }}>
No upcoming contests
</div>
)}
</div>
</div> </div>
); );
}; };

@ -32,15 +32,15 @@ export const DXClusterPanel = ({
}; };
const filterCount = getActiveFilterCount(); const filterCount = getActiveFilterCount();
const spots = data || [];
return ( return (
<div className="panel" style={{ <div className="panel" style={{
padding: '10px', padding: '10px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: '1 1 auto', height: '100%',
overflow: 'hidden', overflow: 'hidden'
minHeight: 0
}}> }}>
{/* Header */} {/* Header */}
<div style={{ <div style={{
@ -54,7 +54,7 @@ export const DXClusterPanel = ({
}}> }}>
<span>🌐 DX CLUSTER <span style={{ color: 'var(--accent-green)', fontSize: '10px' }}> LIVE</span></span> <span>🌐 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: '8px', color: 'var(--text-muted)' }}>{data?.length || 0}/{totalSpots || 0}</span> <span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>{spots.length}/{totalSpots || spots.length}</span>
<button <button
onClick={onOpenFilters} onClick={onOpenFilters}
style={{ style={{
@ -97,7 +97,7 @@ export const DXClusterPanel = ({
onChange={(e) => onFilterChange?.({ ...filters, callsign: e.target.value || undefined })} onChange={(e) => onFilterChange?.({ ...filters, callsign: e.target.value || undefined })}
style={{ style={{
flex: 1, flex: 1,
padding: '3px 6px', padding: '4px 8px',
background: 'var(--bg-secondary)', background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
borderRadius: '3px', borderRadius: '3px',
@ -113,7 +113,7 @@ export const DXClusterPanel = ({
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<div className="loading-spinner" /> <div className="loading-spinner" />
</div> </div>
) : !data || data.length === 0 ? ( ) : spots.length === 0 ? (
<div style={{ <div style={{
textAlign: 'center', textAlign: 'center',
padding: '20px', padding: '20px',
@ -126,14 +126,29 @@ export const DXClusterPanel = ({
<div style={{ <div style={{
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
fontSize: '11px', fontSize: '12px',
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}}> }}>
{data.slice(0, 20).map((spot, i) => { {spots.slice(0, 25).map((spot, i) => {
const freq = parseFloat(spot.freq); // Frequency can be in MHz (string like "14.070") or kHz (number like 14070)
const color = getBandColor(freq / 1000); let freqDisplay = '?';
const isHovered = hoveredSpot?.call === spot.call && let freqMHz = 0;
Math.abs(parseFloat(hoveredSpot?.freq) - freq) < 1;
if (spot.freq) {
const freqVal = parseFloat(spot.freq);
if (freqVal > 1000) {
// It's in kHz, convert to MHz
freqMHz = freqVal / 1000;
freqDisplay = freqMHz.toFixed(3);
} else {
// Already in MHz
freqMHz = freqVal;
freqDisplay = freqVal.toFixed(3);
}
}
const color = getBandColor(freqMHz);
const isHovered = hoveredSpot?.call === spot.call;
return ( return (
<div <div
@ -142,22 +157,23 @@ export const DXClusterPanel = ({
onMouseLeave={() => onHoverSpot?.(null)} onMouseLeave={() => onHoverSpot?.(null)}
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '55px 1fr auto', gridTemplateColumns: '60px 1fr auto',
gap: '6px', gap: '8px',
padding: '4px 6px', padding: '5px 6px',
borderRadius: '3px', borderRadius: '3px',
marginBottom: '1px', marginBottom: '2px',
background: isHovered ? 'rgba(68, 136, 255, 0.2)' : (i % 2 === 0 ? 'rgba(255,255,255,0.02)' : 'transparent'), background: isHovered ? 'rgba(68, 136, 255, 0.25)' : (i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent'),
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.15s' transition: 'background 0.15s',
borderLeft: isHovered ? '2px solid #4488ff' : '2px solid transparent'
}} }}
> >
<div style={{ color, fontWeight: '600' }}> <div style={{ color, fontWeight: '600' }}>
{(freq / 1000).toFixed(3)} {freqDisplay}
</div> </div>
<div style={{ <div style={{
color: 'var(--text-primary)', color: 'var(--text-primary)',
fontWeight: '600', fontWeight: '700',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'

@ -1,6 +1,6 @@
/** /**
* DXpeditionPanel Component * DXpeditionPanel Component
* Shows active and upcoming DXpeditions * Shows active and upcoming DXpeditions (compact version)
*/ */
import React from 'react'; import React from 'react';
@ -16,68 +16,58 @@ export const DXpeditionPanel = ({ data, loading }) => {
}; };
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '8px', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}> <div className="panel-header" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '6px',
fontSize: '11px'
}}>
<span>🌍 DXPEDITIONS</span> <span>🌍 DXPEDITIONS</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> {data && (
{loading && <div className="loading-spinner" />} <span style={{ fontSize: '9px', color: 'var(--text-muted)' }}>
{data && ( {data.active > 0 && <span style={{ color: 'var(--accent-green)' }}>{data.active} active</span>}
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}> </span>
{data.active > 0 && <span style={{ color: 'var(--accent-green)' }}>{data.active} active</span>} )}
{data.active > 0 && data.upcoming > 0 && ' • '}
{data.upcoming > 0 && <span style={{ color: 'var(--accent-cyan)' }}>{data.upcoming} upcoming</span>}
</span>
)}
</div>
</div> </div>
<div style={{ maxHeight: '200px', overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
{data?.dxpeditions?.length > 0 ? ( {loading ? (
data.dxpeditions.slice(0, 15).map((exp, idx) => { <div style={{ display: 'flex', justifyContent: 'center', padding: '10px' }}>
<div className="loading-spinner" />
</div>
) : data?.dxpeditions?.length > 0 ? (
data.dxpeditions.slice(0, 4).map((exp, idx) => {
const style = getStatusStyle(exp); const style = getStatusStyle(exp);
return ( return (
<div key={idx} style={{ <div key={idx} style={{
padding: '6px 8px', padding: '4px 6px',
marginBottom: '4px', marginBottom: '3px',
background: style.bg, background: style.bg,
borderLeft: `3px solid ${style.border}`, borderLeft: `2px solid ${style.border}`,
borderRadius: '4px', borderRadius: '3px',
fontSize: '12px', fontSize: '11px',
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: 'var(--accent-amber)', fontWeight: '700', fontSize: '13px' }}>{exp.callsign}</span> <span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{exp.callsign}</span>
<span style={{ color: style.color, fontSize: '10px' }}> <span style={{ color: style.color, fontSize: '9px' }}>
{exp.isActive ? '● NOW' : exp.isUpcoming ? 'UPCOMING' : 'PAST'} {exp.isActive ? '● NOW' : 'SOON'}
</span> </span>
</div> </div>
<div style={{ color: 'var(--text-secondary)', marginTop: '2px' }}> <div style={{ color: 'var(--text-muted)', fontSize: '10px' }}>
{exp.entity} {exp.entity}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '3px' }}>
<span style={{ color: 'var(--text-muted)', fontSize: '11px' }}>{exp.dates}</span>
<div style={{ display: 'flex', gap: '6px', fontSize: '10px' }}>
{exp.bands && <span style={{ color: 'var(--accent-purple)' }}>{exp.bands.split(' ').slice(0, 3).join(' ')}</span>}
{exp.modes && <span style={{ color: 'var(--accent-cyan)' }}>{exp.modes.split(' ').slice(0, 2).join(' ')}</span>}
</div>
</div>
</div> </div>
); );
}) })
) : ( ) : (
<div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '20px' }}> <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '10px', fontSize: '11px' }}>
{loading ? 'Loading DXpeditions...' : 'No DXpedition data available'} No DXpeditions
</div> </div>
)} )}
</div> </div>
{data && (
<div style={{ marginTop: '6px', textAlign: 'right', fontSize: '9px' }}>
<a href="https://www.ng3k.com/misc/adxo.html" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--text-muted)', textDecoration: 'none' }}>
NG3K ADXO Calendar
</a>
</div>
)}
</div> </div>
); );
}; };

@ -1,13 +1,19 @@
/** /**
* POTAPanel Component * POTAPanel Component
* Displays Parks on the Air activations with ON/OFF toggle * Displays Parks on the Air activations with ON/OFF toggle (compact version)
*/ */
import React from 'react'; import React from 'react';
export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => { export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
return ( return (
<div className="panel" style={{ padding: '12px' }}> <div className="panel" style={{ padding: '8px', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}> <div className="panel-header" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '6px',
fontSize: '11px'
}}>
<span>🏕 POTA ACTIVATORS</span> <span>🏕 POTA ACTIVATORS</span>
<button <button
onClick={onToggleMap} onClick={onToggleMap}
@ -15,9 +21,9 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
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'}`,
color: showOnMap ? '#aa66ff' : '#888', color: showOnMap ? '#aa66ff' : '#888',
padding: '2px 8px', padding: '1px 6px',
borderRadius: '4px', borderRadius: '3px',
fontSize: '10px', fontSize: '9px',
fontFamily: 'JetBrains Mono', fontFamily: 'JetBrains Mono',
cursor: 'pointer' cursor: 'pointer'
}} }}
@ -26,40 +32,42 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
</button> </button>
</div> </div>
{loading ? ( <div style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}> {loading ? (
<div className="loading-spinner" /> <div style={{ display: 'flex', justifyContent: 'center', padding: '10px' }}>
</div> <div className="loading-spinner" />
) : data && data.length > 0 ? ( </div>
<div style={{ fontSize: '11px', fontFamily: 'JetBrains Mono, monospace' }}> ) : data && data.length > 0 ? (
{data.slice(0, 5).map((spot, i) => ( <div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono, monospace' }}>
<div {data.slice(0, 5).map((spot, i) => (
key={`${spot.call}-${i}`} <div
style={{ key={`${spot.call}-${i}`}
display: 'grid', style={{
gridTemplateColumns: '70px 70px 1fr', display: 'grid',
gap: '8px', gridTemplateColumns: '60px 60px 1fr',
padding: '4px 0', gap: '6px',
borderBottom: i < data.length - 1 ? '1px solid var(--border-color)' : 'none' padding: '3px 0',
}} borderBottom: i < Math.min(data.length, 5) - 1 ? '1px solid var(--border-color)' : 'none'
> }}
<span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}> >
{spot.call} <span style={{ color: 'var(--accent-purple)', fontWeight: '600' }}>
</span> {spot.call}
<span style={{ color: 'var(--text-muted)' }}> </span>
{spot.ref} <span style={{ color: 'var(--text-muted)' }}>
</span> {spot.ref}
<span style={{ color: 'var(--accent-cyan)', textAlign: 'right' }}> </span>
{spot.freq} <span style={{ color: 'var(--accent-cyan)', textAlign: 'right' }}>
</span> {spot.freq}
</div> </span>
))} </div>
</div> ))}
) : ( </div>
<div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '20px' }}> ) : (
No active POTA spots <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '10px', fontSize: '11px' }}>
</div> No POTA spots
)} </div>
)}
</div>
</div> </div>
); );
}; };

@ -1,55 +1,83 @@
/** /**
* SettingsPanel Component * SettingsPanel Component
* Modal for app configuration (callsign, location, theme, layout) * Full settings modal matching production version
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { calculateGridSquare } from '../utils/geo.js';
export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [formData, setFormData] = useState({ const [callsign, setCallsign] = useState(config?.callsign || '');
callsign: '', const [gridSquare, setGridSquare] = useState('');
lat: '', const [lat, setLat] = useState(config?.location?.lat || 0);
lon: '', const [lon, setLon] = useState(config?.location?.lon || 0);
theme: 'dark', const [theme, setTheme] = useState(config?.theme || 'dark');
layout: 'modern' const [layout, setLayout] = useState(config?.layout || 'modern');
}); const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy');
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setFormData({ setCallsign(config.callsign || '');
callsign: config.callsign || 'N0CALL', setLat(config.location?.lat || 0);
lat: config.location?.lat?.toString() || '40.0150', setLon(config.location?.lon || 0);
lon: config.location?.lon?.toString() || '-105.2705', setTheme(config.theme || 'dark');
theme: config.theme || 'dark', setLayout(config.layout || 'modern');
layout: config.layout || 'modern' setDxClusterSource(config.dxClusterSource || 'dxspider-proxy');
}); // Calculate grid from coordinates
if (config.location?.lat && config.location?.lon) {
setGridSquare(calculateGridSquare(config.location.lat, config.location.lon));
}
} }
}, [config, isOpen]); }, [config, isOpen]);
const handleSubmit = (e) => { // Update lat/lon when grid square changes
e.preventDefault(); const handleGridChange = (grid) => {
const newConfig = { setGridSquare(grid.toUpperCase());
...config, // Parse grid square to lat/lon if valid (6 char)
callsign: formData.callsign.toUpperCase().trim() || 'N0CALL', if (grid.length >= 4) {
location: { const parsed = parseGridSquare(grid);
lat: parseFloat(formData.lat) || 40.0150, if (parsed) {
lon: parseFloat(formData.lon) || -105.2705 setLat(parsed.lat);
}, setLon(parsed.lon);
theme: formData.theme, }
layout: formData.layout }
};
onSave(newConfig);
onClose();
}; };
const handleGeolocate = () => { // Parse grid square to coordinates
const parseGridSquare = (grid) => {
grid = grid.toUpperCase();
if (grid.length < 4) return null;
const lon1 = (grid.charCodeAt(0) - 65) * 20 - 180;
const lat1 = (grid.charCodeAt(1) - 65) * 10 - 90;
const lon2 = parseInt(grid[2]) * 2;
const lat2 = parseInt(grid[3]) * 1;
let lon = lon1 + lon2 + 1;
let lat = lat1 + lat2 + 0.5;
if (grid.length >= 6) {
const lon3 = (grid.charCodeAt(4) - 65) * (2/24);
const lat3 = (grid.charCodeAt(5) - 65) * (1/24);
lon = lon1 + lon2 + lon3 + (1/24);
lat = lat1 + lat2 + lat3 + (1/48);
}
return { lat, lon };
};
// Update grid when lat/lon changes
useEffect(() => {
if (lat && lon) {
setGridSquare(calculateGridSquare(lat, lon));
}
}, [lat, lon]);
const handleUseLocation = () => {
if (navigator.geolocation) { if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(position) => { (position) => {
setFormData(prev => ({ setLat(position.coords.latitude);
...prev, setLon(position.coords.longitude);
lat: position.coords.latitude.toFixed(4),
lon: position.coords.longitude.toFixed(4)
}));
}, },
(error) => { (error) => {
console.error('Geolocation error:', error); console.error('Geolocation error:', error);
@ -57,193 +85,295 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
} }
); );
} else { } else {
alert('Geolocation is not supported by your browser.'); alert('Geolocation not supported by your browser.');
} }
}; };
const handleSave = () => {
onSave({
...config,
callsign: callsign.toUpperCase(),
location: { lat: parseFloat(lat), lon: parseFloat(lon) },
theme,
layout,
dxClusterSource
});
onClose();
};
if (!isOpen) return null; if (!isOpen) return null;
const inputStyle = { const themeDescriptions = {
width: '100%', dark: '→ Modern dark theme (default)',
padding: '10px 12px', light: '→ Light theme for daytime use',
background: 'var(--bg-tertiary)', legacy: '→ Classic green terminal style'
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '14px',
fontFamily: 'JetBrains Mono, monospace'
}; };
const labelStyle = { const layoutDescriptions = {
display: 'block', modern: '→ Modern responsive grid layout',
fontSize: '12px', classic: '→ Classic HamClock-style layout'
color: 'var(--text-secondary)',
marginBottom: '6px',
fontWeight: '500'
}; };
return ( return (
<div style={{ <div style={{
position: 'fixed', position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0, top: 0,
background: 'rgba(0,0,0,0.8)', left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
zIndex: 10000 zIndex: 10000
}} onClick={onClose}> }}>
<div style={{ <div style={{
background: 'var(--bg-primary)', background: 'var(--bg-secondary)',
border: '1px solid var(--border-color)', border: '2px solid var(--accent-amber)',
borderRadius: '12px', borderRadius: '12px',
width: '90%', padding: '24px',
maxWidth: '500px', width: '420px',
maxHeight: '90vh', maxHeight: '90vh',
overflow: 'auto' overflowY: 'auto'
}} onClick={e => e.stopPropagation()}> }}>
{/* Header */} <h2 style={{
<div style={{ color: 'var(--accent-cyan)',
padding: '20px', marginTop: 0,
borderBottom: '1px solid var(--border-color)', marginBottom: '24px',
display: 'flex', textAlign: 'center',
justifyContent: 'space-between', fontFamily: 'Orbitron, monospace',
alignItems: 'center' fontSize: '20px'
}}> }}>
<h2 style={{ margin: 0, fontSize: '20px', color: 'var(--accent-cyan)' }}> Settings</h2> Station Settings
<button </h2>
onClick={onClose}
{/* Callsign */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
Your Callsign
</label>
<input
type="text"
value={callsign}
onChange={(e) => setCallsign(e.target.value.toUpperCase())}
style={{ style={{
background: 'transparent', width: '100%',
border: 'none', padding: '12px',
color: 'var(--text-muted)', background: 'var(--bg-tertiary)',
fontSize: '24px', border: '1px solid var(--border-color)',
cursor: 'pointer', borderRadius: '6px',
padding: '4px 8px' color: 'var(--accent-amber)',
fontSize: '18px',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: '700',
boxSizing: 'border-box'
}} }}
> />
×
</button>
</div> </div>
{/* Form */} {/* Grid Square */}
<form onSubmit={handleSubmit} style={{ padding: '20px' }}> <div style={{ marginBottom: '20px' }}>
{/* Callsign */} <label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
<div style={{ marginBottom: '20px' }}> Grid Square (or enter Lat/Lon below)
<label style={labelStyle}>Callsign</label> </label>
<input
type="text"
value={gridSquare}
onChange={(e) => handleGridChange(e.target.value)}
placeholder="FN20nc"
maxLength={6}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--accent-amber)',
fontSize: '18px',
fontFamily: 'JetBrains Mono, monospace',
fontWeight: '700',
boxSizing: 'border-box'
}}
/>
</div>
{/* Lat/Lon */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
Latitude
</label>
<input
type="number"
step="0.000001"
value={lat}
onChange={(e) => setLat(parseFloat(e.target.value))}
style={{
width: '100%',
padding: '10px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '14px',
fontFamily: 'JetBrains Mono, monospace',
boxSizing: 'border-box'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
Longitude
</label>
<input <input
type="text" type="number"
value={formData.callsign} step="0.000001"
onChange={e => setFormData(prev => ({ ...prev, callsign: e.target.value }))} value={lon}
style={inputStyle} onChange={(e) => setLon(parseFloat(e.target.value))}
placeholder="W1ABC" style={{
width: '100%',
padding: '10px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '14px',
fontFamily: 'JetBrains Mono, monospace',
boxSizing: 'border-box'
}}
/> />
</div> </div>
</div>
{/* Use My Location button */}
<button
onClick={handleUseLocation}
style={{
width: '100%',
padding: '10px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
marginBottom: '20px'
}}
>
📍 Use My Current Location
</button>
{/* Location */} {/* Theme */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '8px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px' }}> <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
<label style={{ ...labelStyle, margin: 0 }}>Location</label> Theme
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
{['dark', 'light', 'legacy'].map((t) => (
<button <button
type="button" key={t}
onClick={handleGeolocate} onClick={() => setTheme(t)}
style={{ style={{
background: 'var(--bg-tertiary)', padding: '10px',
border: '1px solid var(--border-color)', background: theme === t ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
color: 'var(--accent-cyan)', border: `1px solid ${theme === t ? 'var(--accent-amber)' : 'var(--border-color)'}`,
padding: '4px 12px', borderRadius: '6px',
borderRadius: '4px', color: theme === t ? '#000' : 'var(--text-secondary)',
fontSize: '11px', fontSize: '13px',
cursor: 'pointer' cursor: 'pointer',
fontWeight: theme === t ? '600' : '400'
}} }}
> >
📍 Use My Location {t === 'dark' ? '🌙' : t === 'light' ? '☀️' : '💻'} {t.charAt(0).toUpperCase() + t.slice(1)}
</button> </button>
</div> ))}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}> </div>
<div> <div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
<input {themeDescriptions[theme]}
type="text"
value={formData.lat}
onChange={e => setFormData(prev => ({ ...prev, lat: e.target.value }))}
style={inputStyle}
placeholder="Latitude"
/>
</div>
<div>
<input
type="text"
value={formData.lon}
onChange={e => setFormData(prev => ({ ...prev, lon: e.target.value }))}
style={inputStyle}
placeholder="Longitude"
/>
</div>
</div>
</div> </div>
</div>
{/* Theme */} {/* Layout */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '8px' }}>
<label style={labelStyle}>Theme</label> <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
<div style={{ display: 'flex', gap: '8px' }}> Layout
{['dark', 'light', 'legacy'].map(theme => ( </label>
<button <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
key={theme} {['modern', 'classic'].map((l) => (
type="button" <button
onClick={() => setFormData(prev => ({ ...prev, theme }))} key={l}
style={{ onClick={() => setLayout(l)}
flex: 1, style={{
padding: '10px', padding: '10px',
background: formData.theme === theme ? 'var(--accent-amber)' : 'var(--bg-tertiary)', background: layout === l ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
border: `1px solid ${formData.theme === theme ? 'var(--accent-amber)' : 'var(--border-color)'}`, border: `1px solid ${layout === l ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '6px', borderRadius: '6px',
color: formData.theme === theme ? '#000' : 'var(--text-secondary)', color: layout === l ? '#000' : 'var(--text-secondary)',
fontSize: '13px', fontSize: '13px',
fontWeight: formData.theme === theme ? '600' : '400', cursor: 'pointer',
cursor: 'pointer', fontWeight: layout === l ? '600' : '400'
textTransform: 'capitalize' }}
}} >
> {l === 'modern' ? '🖥️' : '📺'} {l.charAt(0).toUpperCase() + l.slice(1)}
{theme === 'legacy' ? '🖥 Legacy' : theme === 'dark' ? '🌙 Dark' : '☀ Light'} </button>
</button> ))}
))}
</div>
</div> </div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
{layoutDescriptions[layout]}
</div>
</div>
{/* Layout */} {/* DX Cluster Source */}
<div style={{ marginBottom: '24px' }}> <div style={{ marginBottom: '20px' }}>
<label style={labelStyle}>Layout</label> <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
<div style={{ display: 'flex', gap: '8px' }}> DX Cluster Source
{['modern', 'legacy'].map(layout => ( </label>
<button <select
key={layout} value={dxClusterSource}
type="button" onChange={(e) => setDxClusterSource(e.target.value)}
onClick={() => setFormData(prev => ({ ...prev, layout }))} style={{
style={{ width: '100%',
flex: 1, padding: '12px',
padding: '10px', background: 'var(--bg-tertiary)',
background: formData.layout === layout ? 'var(--accent-cyan)' : 'var(--bg-tertiary)', border: '1px solid var(--border-color)',
border: `1px solid ${formData.layout === layout ? 'var(--accent-cyan)' : 'var(--border-color)'}`, borderRadius: '6px',
borderRadius: '6px', color: 'var(--accent-green)',
color: formData.layout === layout ? '#000' : 'var(--text-secondary)', fontSize: '14px',
fontSize: '13px', fontFamily: 'JetBrains Mono, monospace',
fontWeight: formData.layout === layout ? '600' : '400', cursor: 'pointer'
cursor: 'pointer', }}
textTransform: 'capitalize' >
}} <option value="dxspider-proxy"> DX Spider Proxy (Recommended)</option>
> <option value="hamqth">HamQTH Cluster</option>
{layout === 'modern' ? '✨ Modern' : '📺 Classic'} <option value="dxwatch">DXWatch</option>
</button> <option value="auto">Auto (try all sources)</option>
))} </select>
</div> <div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
Real-time DX Spider feed via our dedicated proxy service
</div> </div>
</div>
{/* Submit */} {/* Buttons */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginTop: '24px' }}>
<button <button
type="submit" onClick={onClose}
style={{
padding: '14px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: 'var(--text-secondary)',
fontSize: '14px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
onClick={handleSave}
style={{ style={{
width: '100%',
padding: '14px', padding: '14px',
background: 'var(--accent-green)', background: 'linear-gradient(135deg, #00ff88 0%, #00ddff 100%)',
border: 'none', border: 'none',
borderRadius: '6px', borderRadius: '6px',
color: '#000', color: '#000',
@ -254,7 +384,11 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
> >
Save Settings Save Settings
</button> </button>
</form> </div>
<div style={{ textAlign: 'center', marginTop: '16px', fontSize: '11px', color: 'var(--text-muted)' }}>
Settings are saved in your browser
</div>
</div> </div>
</div> </div>
); );

Loading…
Cancel
Save

Powered by TurnKey Linux.