You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openhamclock/src/components/SettingsPanel.jsx

805 lines
33 KiB

/**
* SettingsPanel Component
* Full settings modal with map layer controls
*/
import React, { useState, useEffect } from 'react';
import { calculateGridSquare } from '../utils/geo.js';
import { useTranslation, Trans } from 'react-i18next';
import { LANGUAGES } from '../lang/i18n.js';
export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [callsign, setCallsign] = useState(config?.callsign || '');
const [callsignSize, setCallsignSize] = useState(config?.callsignSize || 1.0);
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 [timezone, setTimezone] = useState(config?.timezone || '');
const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy');
const { t, i18n } = useTranslation();
// Layer controls
const [layers, setLayers] = useState([]);
const [activeTab, setActiveTab] = useState('station');
useEffect(() => {
if (config) {
setCallsign(config.callsign || '');
setCallsignSize(config.callsignSize || 1.0)
setLat(config.location?.lat || 0);
setLon(config.location?.lon || 0);
setTheme(config.theme || 'dark');
setLayout(config.layout || 'modern');
setTimezone(config.timezone || '');
setDxClusterSource(config.dxClusterSource || 'dxspider-proxy');
if (config.location?.lat && config.location?.lon) {
setGridSquare(calculateGridSquare(config.location.lat, config.location.lon));
}
}
}, [config, isOpen]);
// Load layers when panel opens
useEffect(() => {
if (isOpen && window.hamclockLayerControls) {
setLayers(window.hamclockLayerControls.layers || []);
}
}, [isOpen]);
// Refresh layers periodically
useEffect(() => {
if (isOpen && activeTab === 'layers') {
const interval = setInterval(() => {
if (window.hamclockLayerControls) {
setLayers([...window.hamclockLayerControls.layers]);
}
}, 200);
return () => clearInterval(interval);
}
}, [isOpen, activeTab]);
const handleGridChange = (grid) => {
setGridSquare(grid.toUpperCase());
if (grid.length >= 4) {
const parsed = parseGridSquare(grid);
if (parsed) {
setLat(parsed.lat);
setLon(parsed.lon);
}
}
};
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 };
};
useEffect(() => {
if (lat && lon) {
setGridSquare(calculateGridSquare(lat, lon));
}
}, [lat, lon]);
const handleUseLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setLat(position.coords.latitude);
setLon(position.coords.longitude);
},
(error) => {
console.error('Geolocation error:', error);
alert(t('station.settings.useLocation.error1'));
}
);
} else {
alert(t('station.settings.useLocation.error2'));
}
};
const handleToggleLayer = (layerId) => {
if (window.hamclockLayerControls) {
const layer = layers.find(l => l.id === layerId);
const newEnabledState = !layer.enabled;
// Update the control
window.hamclockLayerControls.toggleLayer(layerId, newEnabledState);
// Force immediate UI update
setLayers(prevLayers =>
prevLayers.map(l =>
l.id === layerId ? { ...l, enabled: newEnabledState } : l
)
);
// Refresh after a short delay to get the updated state
setTimeout(() => {
if (window.hamclockLayerControls) {
setLayers([...window.hamclockLayerControls.layers]);
}
}, 100);
}
};
const handleOpacityChange = (layerId, opacity) => {
if (window.hamclockLayerControls) {
window.hamclockLayerControls.setOpacity(layerId, opacity);
setLayers([...window.hamclockLayerControls.layers]);
}
};
const handleSave = () => {
onSave({
...config,
callsign: callsign.toUpperCase(),
callsignSize: callsignSize,
location: { lat: parseFloat(lat), lon: parseFloat(lon) },
theme,
layout,
timezone,
dxClusterSource
});
onClose();
};
if (!isOpen) return null;
const Code = ({ children }) => (
<code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>
{children}
</code>
);
const themeDescriptions = {
dark: t('station.settings.theme.dark.describe'),
light: t('station.settings.theme.light.describe'),
legacy: t('station.settings.theme.legacy.describe'),
retro: t('station.settings.theme.retro.describe')
};
const layoutDescriptions = {
modern: t('station.settings.layout.modern.describe'),
classic: t('station.settings.layout.classic.describe')
};
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10000
}}>
<div style={{
background: 'var(--bg-secondary)',
border: '2px solid var(--accent-amber)',
borderRadius: '12px',
padding: '24px',
width: '520px',
maxHeight: '90vh',
overflowY: 'auto'
}}>
<h2 style={{
color: 'var(--accent-cyan)',
marginTop: 0,
marginBottom: '24px',
textAlign: 'center',
fontFamily: 'Orbitron, monospace',
fontSize: '20px'
}}>
{t('station.settings.title')}
</h2>
{/* Tab Navigation */}
<div style={{
display: 'flex',
gap: '8px',
marginBottom: '24px',
borderBottom: '1px solid var(--border-color)',
paddingBottom: '12px'
}}>
<button
onClick={() => setActiveTab('station')}
style={{
flex: 1,
padding: '10px',
background: activeTab === 'station' ? 'var(--accent-amber)' : 'transparent',
border: 'none',
borderRadius: '6px 6px 0 0',
color: activeTab === 'station' ? '#000' : 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
fontWeight: activeTab === 'station' ? '700' : '400',
fontFamily: 'JetBrains Mono, monospace'
}}
>
Station
</button>
<button
onClick={() => setActiveTab('layers')}
style={{
flex: 1,
padding: '10px',
background: activeTab === 'layers' ? 'var(--accent-amber)' : 'transparent',
border: 'none',
borderRadius: '6px 6px 0 0',
color: activeTab === 'layers' ? '#000' : 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
fontWeight: activeTab === 'layers' ? '700' : '400',
fontFamily: 'JetBrains Mono, monospace'
}}
>
Map Layers
</button>
</div>
{/* Station Settings Tab */}
{activeTab === 'station' && (
<>
{/* First-time setup banner */}
{(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && (
<div style={{
background: 'rgba(255, 193, 7, 0.15)',
border: '1px solid var(--accent-amber)',
borderRadius: '8px',
padding: '12px 16px',
marginBottom: '20px',
fontSize: '13px'
}}>
<div style={{ color: 'var(--accent-amber)', fontWeight: '700', marginBottom: '6px' }}>
{t("station.settings.welcome")}
</div>
<div style={{ color: 'var(--text-secondary)', lineHeight: 1.5 }}>
{t("station.settings.describe")}
</div>
<div style={{ color: 'var(--text-muted)', fontSize: '11px', marginTop: '8px' }}>
<Trans i18nKey="station.settings.tip.env" components={{ envExample: <Code />, env: <Code /> }} />
</div>
</div>
)}
{/* Callsign */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.callsign')}
</label>
<input
type="text"
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>
{/* Callsign Size*/}
<div style={{ marginBottom: '20px'}}>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
{t('station.settings.callsignSize')}
</label>
<input
type="number"
step="0.1"
value={isNaN(lat) ? '' : callsignSize}
onChange={(e) => {
if (e.target.value >= 0.1 && e.target.value <= 2.0) {
setCallsignSize(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>
{/* Grid Square */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.locator')}
</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' }}>
{t('station.settings.latitude')}
</label>
<input
type="number"
step="0.000001"
value={isNaN(lat) ? '' : lat}
onChange={(e) => setLat(parseFloat(e.target.value) || 0)}
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' }}>
{t('station.settings.longitude')}
</label>
<input
type="number"
step="0.000001"
value={isNaN(lon) ? '' : lon}
onChange={(e) => setLon(parseFloat(e.target.value) || 0)}
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>
<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'
}}
>
{t('station.settings.useLocation')}
</button>
{/* Theme */}
<div style={{ marginBottom: '8px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.theme')}
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
{['dark', 'light', 'legacy', 'retro'].map((th) => (
<button
key={th}
onClick={() => setTheme(th)}
style={{
padding: '10px',
background: theme === th ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
border: `1px solid ${theme === th ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '6px',
color: theme === th ? '#000' : 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontWeight: theme === th ? '600' : '400'
}}
>
{th === 'dark' ? '🌙' : th === 'light' ? '☀️' : th === 'legacy' ? '💻' : '🪟'} {t('station.settings.theme.' + th)}
</button>
))}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
{themeDescriptions[theme]}
</div>
</div>
{/* Layout */}
<div style={{ marginBottom: '8px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.layout')}
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
{['modern', 'classic'].map((l) => (
<button
key={l}
onClick={() => setLayout(l)}
style={{
padding: '10px',
background: layout === l ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
border: `1px solid ${layout === l ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '6px',
color: layout === l ? '#000' : 'var(--text-secondary)',
fontSize: '13px',
cursor: 'pointer',
fontWeight: layout === l ? '600' : '400'
}}
>
{l === 'modern' ? '🖥️' : '📺'} {t('station.settings.layout.' + l)}
</button>
))}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
{layoutDescriptions[layout]}
</div>
</div>
{/* DX Cluster Source */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
🕐 Timezone
</label>
<select
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
color: timezone ? 'var(--accent-green)' : 'var(--text-muted)',
fontSize: '14px',
fontFamily: 'JetBrains Mono, monospace',
cursor: 'pointer'
}}
>
<option value="">Auto (browser default)</option>
<optgroup label="North America">
<option value="America/New_York">Eastern (New York)</option>
<option value="America/Chicago">Central (Chicago)</option>
<option value="America/Denver">Mountain (Denver)</option>
<option value="America/Los_Angeles">Pacific (Los Angeles)</option>
<option value="America/Anchorage">Alaska</option>
<option value="Pacific/Honolulu">Hawaii</option>
<option value="America/Phoenix">Arizona (no DST)</option>
<option value="America/Regina">Saskatchewan (no DST)</option>
<option value="America/Halifax">Atlantic (Halifax)</option>
<option value="America/St_Johns">Newfoundland</option>
<option value="America/Toronto">Ontario (Toronto)</option>
<option value="America/Winnipeg">Manitoba (Winnipeg)</option>
<option value="America/Edmonton">Alberta (Edmonton)</option>
<option value="America/Vancouver">BC (Vancouver)</option>
<option value="America/Mexico_City">Mexico City</option>
</optgroup>
<optgroup label="Europe">
<option value="Europe/London">UK (London)</option>
<option value="Europe/Dublin">Ireland (Dublin)</option>
<option value="Europe/Paris">Central Europe (Paris)</option>
<option value="Europe/Berlin">Germany (Berlin)</option>
<option value="Europe/Rome">Italy (Rome)</option>
<option value="Europe/Madrid">Spain (Madrid)</option>
<option value="Europe/Amsterdam">Netherlands (Amsterdam)</option>
<option value="Europe/Brussels">Belgium (Brussels)</option>
<option value="Europe/Stockholm">Sweden (Stockholm)</option>
<option value="Europe/Helsinki">Finland (Helsinki)</option>
<option value="Europe/Athens">Greece (Athens)</option>
<option value="Europe/Bucharest">Romania (Bucharest)</option>
<option value="Europe/Moscow">Russia (Moscow)</option>
<option value="Europe/Warsaw">Poland (Warsaw)</option>
<option value="Europe/Zurich">Switzerland (Zurich)</option>
<option value="Europe/Lisbon">Portugal (Lisbon)</option>
</optgroup>
<optgroup label="Asia & Pacific">
<option value="Asia/Tokyo">Japan (Tokyo)</option>
<option value="Asia/Seoul">Korea (Seoul)</option>
<option value="Asia/Shanghai">China (Shanghai)</option>
<option value="Asia/Hong_Kong">Hong Kong</option>
<option value="Asia/Taipei">Taiwan (Taipei)</option>
<option value="Asia/Singapore">Singapore</option>
<option value="Asia/Kolkata">India (Kolkata)</option>
<option value="Asia/Dubai">UAE (Dubai)</option>
<option value="Asia/Riyadh">Saudi Arabia (Riyadh)</option>
<option value="Asia/Tehran">Iran (Tehran)</option>
<option value="Asia/Bangkok">Thailand (Bangkok)</option>
<option value="Asia/Jakarta">Indonesia (Jakarta)</option>
<option value="Asia/Manila">Philippines (Manila)</option>
<option value="Australia/Sydney">Australia Eastern (Sydney)</option>
<option value="Australia/Adelaide">Australia Central (Adelaide)</option>
<option value="Australia/Perth">Australia Western (Perth)</option>
<option value="Pacific/Auckland">New Zealand (Auckland)</option>
<option value="Pacific/Fiji">Fiji</option>
</optgroup>
<optgroup label="South America">
<option value="America/Sao_Paulo">Brazil (São Paulo)</option>
<option value="America/Argentina/Buenos_Aires">Argentina (Buenos Aires)</option>
<option value="America/Santiago">Chile (Santiago)</option>
<option value="America/Bogota">Colombia (Bogotá)</option>
<option value="America/Lima">Peru (Lima)</option>
<option value="America/Caracas">Venezuela (Caracas)</option>
</optgroup>
<optgroup label="Africa">
<option value="Africa/Cairo">Egypt (Cairo)</option>
<option value="Africa/Johannesburg">South Africa (Johannesburg)</option>
<option value="Africa/Lagos">Nigeria (Lagos)</option>
<option value="Africa/Nairobi">Kenya (Nairobi)</option>
<option value="Africa/Casablanca">Morocco (Casablanca)</option>
</optgroup>
<optgroup label="Other">
<option value="UTC">UTC</option>
<option value="Atlantic/Reykjavik">Iceland (Reykjavik)</option>
<option value="Atlantic/Azores">Azores</option>
<option value="Indian/Maldives">Maldives</option>
<option value="Indian/Mauritius">Mauritius</option>
</optgroup>
</select>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
Set this if your local time shows incorrectly (e.g. same as UTC).
Privacy browsers like Librewolf may spoof your timezone.
{timezone ? '' : ' Currently using browser default.'}
</div>
</div>
{/* DX Cluster Source - original */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.dx.title')}
</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">{t('station.settings.dx.option1')}</option>
<option value="hamqth">{t('station.settings.dx.option2')}</option>
<option value="dxwatch">{t('station.settings.dx.option3')}</option>
<option value="auto">{t('station.settings.dx.option4')}</option>
</select>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
{t('station.settings.dx.describe')}
</div>
</div>
{/* Language */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
{t('station.settings.language')}
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6px' }}>
{LANGUAGES.map((lang) => (
<button
key={lang.code}
onClick={() => i18n.changeLanguage(lang.code)}
style={{
padding: '8px 6px',
background: i18n.language === lang.code || (i18n.language && i18n.language.startsWith(lang.code))
? 'rgba(0, 221, 255, 0.2)'
: 'var(--bg-tertiary)',
border: `1px solid ${i18n.language === lang.code || (i18n.language && i18n.language.startsWith(lang.code))
? 'var(--accent-cyan)'
: 'var(--border-color)'}`,
borderRadius: '6px',
color: i18n.language === lang.code || (i18n.language && i18n.language.startsWith(lang.code))
? 'var(--accent-cyan)'
: 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontWeight: i18n.language === lang.code || (i18n.language && i18n.language.startsWith(lang.code)) ? '600' : '400',
textAlign: 'center'
}}
>
{lang.flag} {lang.name}
</button>
))}
</div>
</div>
</>
)}
{/* Map Layers Tab */}
{activeTab === 'layers' && (
<div>
{layers.length > 0 ? (
layers.map(layer => (
<div key={layer.id} style={{
background: 'var(--bg-tertiary)',
border: `1px solid ${layer.enabled ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '8px',
padding: '14px',
marginBottom: '12px'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
cursor: 'pointer',
flex: 1
}}>
<input
type="checkbox"
checked={layer.enabled}
onChange={() => handleToggleLayer(layer.id)}
style={{
width: '18px',
height: '18px',
cursor: 'pointer'
}}
/>
<span style={{ fontSize: '18px' }}>{layer.icon}</span>
<div>
<div style={{
color: layer.enabled ? 'var(--accent-amber)' : 'var(--text-primary)',
fontSize: '14px',
fontWeight: '600',
fontFamily: 'JetBrains Mono, monospace'
}}>
{layer.name}
</div>
{layer.description && (
<div style={{
fontSize: '11px',
color: 'var(--text-muted)',
marginTop: '2px'
}}>
{layer.description}
</div>
)}
</div>
</label>
<span style={{
fontSize: '11px',
textTransform: 'uppercase',
color: 'var(--text-secondary)',
background: 'var(--bg-hover)',
padding: '2px 8px',
borderRadius: '3px'
}}>
{layer.category}
</span>
</div>
{layer.enabled && (
<div style={{ paddingLeft: '38px', marginTop: '12px' }}>
<label style={{
display: 'block',
fontSize: '11px',
color: 'var(--text-muted)',
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Opacity: {Math.round(layer.opacity * 100)}%
</label>
<input
type="range"
min="0"
max="100"
value={layer.opacity * 100}
onChange={(e) => handleOpacityChange(layer.id, parseFloat(e.target.value) / 100)}
style={{
width: '100%',
cursor: 'pointer'
}}
/>
</div>
)}
</div>
))
) : (
<div style={{
textAlign: 'center',
padding: '40px 20px',
color: 'var(--text-muted)',
fontSize: '13px'
}}>
No map layers available
</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'
}}
>
{t('cancel')}
</button>
<button
onClick={handleSave}
style={{
padding: '14px',
background: 'linear-gradient(135deg, #00ff88 0%, #00ddff 100%)',
border: 'none',
borderRadius: '6px',
color: '#000',
fontSize: '14px',
fontWeight: '700',
cursor: 'pointer'
}}
>
{t('station.settings.button.save')}
</button>
</div>
<div style={{ textAlign: 'center', marginTop: '16px', fontSize: '11px', color: 'var(--text-muted)' }}>
{t('station.settings.button.save.confirm')}
</div>
</div>
</div>
);
};
export default SettingsPanel;

Powered by TurnKey Linux.