diff --git a/src/App.jsx b/src/App.jsx index 7d98379..0895bb9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -330,34 +330,42 @@ const App = () => { {/* RIGHT SIDEBAR */} -
- {/* DX Cluster */} - setShowDXFilters(true)} - onHoverSpot={setHoveredSpot} - hoveredSpot={hoveredSpot} - showOnMap={mapLayers.showDXPaths} - onToggleMap={toggleDXPaths} - /> +
+ {/* DX Cluster - takes most space */} +
+ setShowDXFilters(true)} + onHoverSpot={setHoveredSpot} + hoveredSpot={hoveredSpot} + showOnMap={mapLayers.showDXPaths} + onToggleMap={toggleDXPaths} + /> +
- {/* DXpeditions */} - + {/* DXpeditions - smaller */} +
+ +
- {/* POTA */} - + {/* POTA - smaller */} +
+ +
- {/* Contests */} - + {/* Contests - smaller */} +
+ +
diff --git a/src/components/ContestPanel.jsx b/src/components/ContestPanel.jsx index cfd4f91..815ed49 100644 --- a/src/components/ContestPanel.jsx +++ b/src/components/ContestPanel.jsx @@ -1,68 +1,72 @@ /** * 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 ( -
-
🏆 CONTESTS
- {loading ? ( -
-
-
- ) : data.length === 0 ? ( -
- No upcoming contests -
- ) : ( -
- {data.slice(0, 5).map((contest, i) => ( -
-
- {contest.name} -
-
- {contest.startDate} - +
+ 🏆 CONTESTS +
+ +
+ {loading ? ( +
+
+
+ ) : data && data.length > 0 ? ( +
+ {data.slice(0, 5).map((contest, i) => ( +
+
- {contest.isActive ? '● ACTIVE' : contest.timeUntil || ''} - + {contest.name} +
+
+ {contest.mode} + {formatDate(contest.start)} +
-
- ))} -
- )} + ))} +
+ ) : ( +
+ No upcoming contests +
+ )} +
); }; diff --git a/src/components/DXClusterPanel.jsx b/src/components/DXClusterPanel.jsx index 78137ce..3cd50c7 100644 --- a/src/components/DXClusterPanel.jsx +++ b/src/components/DXClusterPanel.jsx @@ -32,15 +32,15 @@ export const DXClusterPanel = ({ }; const filterCount = getActiveFilterCount(); + const spots = data || []; return (
{/* Header */}
🌐 DX CLUSTER ● LIVE
- {data?.length || 0}/{totalSpots || 0} + {spots.length}/{totalSpots || spots.length}
- {loading ? ( -
-
-
- ) : data && data.length > 0 ? ( -
- {data.slice(0, 5).map((spot, i) => ( -
- - {spot.call} - - - {spot.ref} - - - {spot.freq} - -
- ))} -
- ) : ( -
- No active POTA spots -
- )} +
+ {loading ? ( +
+
+
+ ) : data && data.length > 0 ? ( +
+ {data.slice(0, 5).map((spot, i) => ( +
+ + {spot.call} + + + {spot.ref} + + + {spot.freq} + +
+ ))} +
+ ) : ( +
+ No POTA spots +
+ )} +
); }; diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 4069ebc..26baf20 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -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 - }; - onSave(newConfig); - onClose(); + // 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); + } + } }; - 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) { 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 (
+ }}>
e.stopPropagation()}> - {/* Header */} -
+

-

⚙ Settings

- + />
- {/* Form */} -
- {/* Callsign */} -
- + {/* Grid Square */} +
+ + 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' + }} + /> +
+ + {/* Lat/Lon */} +
+
+ + 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' + }} + /> +
+
+ setFormData(prev => ({ ...prev, callsign: e.target.value }))} - style={inputStyle} - placeholder="W1ABC" + 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' + }} />
+
+ + {/* Use My Location button */} + - {/* Location */} -
-
- + {/* Theme */} +
+ +
+ {['dark', 'light', 'legacy'].map((t) => ( -
-
-
- setFormData(prev => ({ ...prev, lat: e.target.value }))} - style={inputStyle} - placeholder="Latitude" - /> -
-
- setFormData(prev => ({ ...prev, lon: e.target.value }))} - style={inputStyle} - placeholder="Longitude" - /> -
-
+ ))} +
+
+ {themeDescriptions[theme]}
+
- {/* Theme */} -
- -
- {['dark', 'light', 'legacy'].map(theme => ( - - ))} -
+ {/* Layout */} +
+ +
+ {['modern', 'classic'].map((l) => ( + + ))}
+
+ {layoutDescriptions[layout]} +
+
- {/* Layout */} -
- -
- {['modern', 'legacy'].map(layout => ( - - ))} -
+ {/* DX Cluster Source */} +
+ + +
+ → Real-time DX Spider feed via our dedicated proxy service
+
- {/* Submit */} + {/* Buttons */} +
+ - +
+ +
+ Settings are saved in your browser +
);