|
|
|
|
@ -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>
|
|
|
|
|
);
|
|
|
|
|
|