fixing layout and missing freqs

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

@ -330,8 +330,9 @@ const App = () => {
</div>
{/* RIGHT SIDEBAR */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', overflow: 'hidden' }}>
{/* DX Cluster */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', overflow: 'hidden' }}>
{/* DX Cluster - takes most space */}
<div style={{ flex: '2 1 0', minHeight: '250px', overflow: 'hidden' }}>
<DXClusterPanel
data={dxCluster.data}
loading={dxCluster.loading}
@ -344,22 +345,29 @@ const App = () => {
showOnMap={mapLayers.showDXPaths}
onToggleMap={toggleDXPaths}
/>
</div>
{/* DXpeditions */}
{/* DXpeditions - smaller */}
<div style={{ flex: '0 0 auto', maxHeight: '140px', overflow: 'hidden' }}>
<DXpeditionPanel data={dxpeditions.data} loading={dxpeditions.loading} />
</div>
{/* POTA */}
{/* POTA - smaller */}
<div style={{ flex: '0 0 auto', maxHeight: '120px', overflow: 'hidden' }}>
<POTAPanel
data={potaSpots.data}
loading={potaSpots.loading}
showOnMap={mapLayers.showPOTA}
onToggleMap={togglePOTA}
/>
</div>
{/* Contests */}
{/* Contests - smaller */}
<div style={{ flex: '0 0 auto', maxHeight: '150px', overflow: 'hidden' }}>
<ContestPanel data={contests.data} loading={contests.loading} />
</div>
</div>
</div>
{/* Modals */}
<SettingsPanel

@ -1,69 +1,73 @@
/**
* ContestPanel Component
* Displays upcoming amateur radio contests
* Displays upcoming contests (compact version)
*/
import React from 'react';
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 (
<div className="panel" style={{ padding: '12px' }}>
<div className="panel-header">🏆 CONTESTS</div>
<div className="panel" style={{ padding: '8px', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="panel-header" style={{
marginBottom: '6px',
fontSize: '11px'
}}>
🏆 CONTESTS
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'center', padding: '10px' }}>
<div className="loading-spinner" />
</div>
) : data.length === 0 ? (
<div style={{
textAlign: 'center',
padding: '20px',
color: 'var(--text-muted)',
fontSize: '12px'
}}>
No upcoming contests
</div>
) : (
<div style={{
fontSize: '11px',
fontFamily: 'JetBrains Mono, monospace'
}}>
) : data && data.length > 0 ? (
<div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono, monospace' }}>
{data.slice(0, 5).map((contest, i) => (
<div
key={`${contest.name}-${i}`}
style={{
padding: '8px',
borderRadius: '4px',
marginBottom: '4px',
background: i % 2 === 0 ? 'rgba(255,255,255,0.02)' : 'transparent'
padding: '4px 0',
borderBottom: i < Math.min(data.length, 5) - 1 ? '1px solid var(--border-color)' : 'none'
}}
>
<div style={{
color: 'var(--accent-cyan)',
color: 'var(--text-primary)',
fontWeight: '600',
marginBottom: '4px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
textOverflow: 'ellipsis'
}}>
{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 || ''}
</span>
<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 style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '10px', fontSize: '11px' }}>
No upcoming contests
</div>
)}
</div>
</div>
);
};

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

@ -1,6 +1,6 @@
/**
* DXpeditionPanel Component
* Shows active and upcoming DXpeditions
* Shows active and upcoming DXpeditions (compact version)
*/
import React from 'react';
@ -16,68 +16,58 @@ export const DXpeditionPanel = ({ data, loading }) => {
};
return (
<div className="panel" style={{ padding: '12px' }}>
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<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: '6px',
fontSize: '11px'
}}>
<span>🌍 DXPEDITIONS</span>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{loading && <div className="loading-spinner" />}
{data && (
<span style={{ fontSize: '10px', 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 && data.upcoming > 0 && ' • '}
{data.upcoming > 0 && <span style={{ color: 'var(--accent-cyan)' }}>{data.upcoming} upcoming</span>}
</span>
)}
</div>
</div>
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{data?.dxpeditions?.length > 0 ? (
data.dxpeditions.slice(0, 15).map((exp, idx) => {
<div style={{ flex: 1, overflowY: 'auto' }}>
{loading ? (
<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);
return (
<div key={idx} style={{
padding: '6px 8px',
marginBottom: '4px',
padding: '4px 6px',
marginBottom: '3px',
background: style.bg,
borderLeft: `3px solid ${style.border}`,
borderRadius: '4px',
fontSize: '12px',
borderLeft: `2px solid ${style.border}`,
borderRadius: '3px',
fontSize: '11px',
fontFamily: 'JetBrains Mono, monospace'
}}>
<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: style.color, fontSize: '10px' }}>
{exp.isActive ? '● NOW' : exp.isUpcoming ? 'UPCOMING' : 'PAST'}
<span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{exp.callsign}</span>
<span style={{ color: style.color, fontSize: '9px' }}>
{exp.isActive ? '● NOW' : 'SOON'}
</span>
</div>
<div style={{ color: 'var(--text-secondary)', marginTop: '2px' }}>
<div style={{ color: 'var(--text-muted)', fontSize: '10px' }}>
{exp.entity}
</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 style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '20px' }}>
{loading ? 'Loading DXpeditions...' : 'No DXpedition data available'}
<div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '10px', fontSize: '11px' }}>
No DXpeditions
</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>
);
};

@ -1,13 +1,19 @@
/**
* 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';
export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
return (
<div className="panel" style={{ padding: '12px' }}>
<div className="panel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<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: '6px',
fontSize: '11px'
}}>
<span>🏕 POTA ACTIVATORS</span>
<button
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)',
border: `1px solid ${showOnMap ? '#aa66ff' : '#666'}`,
color: showOnMap ? '#aa66ff' : '#888',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '10px',
padding: '1px 6px',
borderRadius: '3px',
fontSize: '9px',
fontFamily: 'JetBrains Mono',
cursor: 'pointer'
}}
@ -26,21 +32,22 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'center', padding: '10px' }}>
<div className="loading-spinner" />
</div>
) : data && data.length > 0 ? (
<div style={{ fontSize: '11px', fontFamily: 'JetBrains Mono, monospace' }}>
<div style={{ fontSize: '10px', fontFamily: 'JetBrains Mono, monospace' }}>
{data.slice(0, 5).map((spot, i) => (
<div
key={`${spot.call}-${i}`}
style={{
display: 'grid',
gridTemplateColumns: '70px 70px 1fr',
gap: '8px',
padding: '4px 0',
borderBottom: i < data.length - 1 ? '1px solid var(--border-color)' : 'none'
gridTemplateColumns: '60px 60px 1fr',
gap: '6px',
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' }}>
@ -56,11 +63,12 @@ export const POTAPanel = ({ data, loading, showOnMap, onToggleMap }) => {
))}
</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' }}>
No POTA spots
</div>
)}
</div>
</div>
);
};

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

Loading…
Cancel
Save

Powered by TurnKey Linux.