From 4ef5d84bbf02eda71df1aa1a5895a194316d3672 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 18:10:21 -0500 Subject: [PATCH 1/2] PR Merge backwards --- package.json | 2 +- src/components/SettingsPanel.jsx | 162 +++++++++++++++++++++++++++++++ src/lang/de.json | 48 +++++++++ src/lang/en.json | 48 +++++++++ src/lang/es.json | 48 +++++++++ src/lang/fr.json | 48 +++++++++ src/lang/i18n.js | 48 +++++++++ src/lang/it.json | 48 +++++++++ src/lang/ja.json | 48 +++++++++ src/lang/pt.json | 48 +++++++++ 10 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 src/lang/de.json create mode 100644 src/lang/en.json create mode 100644 src/lang/es.json create mode 100644 src/lang/fr.json create mode 100644 src/lang/i18n.js create mode 100644 src/lang/it.json create mode 100644 src/lang/ja.json create mode 100644 src/lang/pt.json diff --git a/package.json b/package.json index dfcde40..122c622 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "prestart": "node -e \"const fs=require('fs'); if(!fs.existsSync('dist/index.html')){console.log('Building frontend...'); require('child_process').execSync('npm run build',{stdio:'inherit'})}\"", + "prestart": "npm run build", "start": "node server.js", "server": "node server.js", "test": "echo \"Tests passing\" && exit 0" diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 3eb5f52..73ef1b9 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -4,6 +4,11 @@ */ import React, { useState, useEffect } from 'react'; import { calculateGridSquare } from '../utils/geo.js'; +<<<<<<< Updated upstream +======= +import { useTranslation, Trans } from 'react-i18next'; +import { LANGUAGES } from '../lang/i18n.js'; +>>>>>>> Stashed changes export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [callsign, setCallsign] = useState(config?.callsign || ''); @@ -13,6 +18,14 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [theme, setTheme] = useState(config?.theme || 'dark'); const [layout, setLayout] = useState(config?.layout || 'modern'); const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy'); +<<<<<<< Updated upstream +======= + const { t, i18n } = useTranslation(); + + // Layer controls + const [layers, setLayers] = useState([]); + const [activeTab, setActiveTab] = useState('station'); +>>>>>>> Stashed changes useEffect(() => { if (config) { @@ -311,9 +324,158 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { fontWeight: theme === t ? '600' : '400' }} > +<<<<<<< Updated upstream {t === 'dark' ? '🌙' : t === 'light' ? '☀️' : t === 'legacy' ? '💻' : '🪟'} {t.charAt(0).toUpperCase() + t.slice(1)} ))} +======= + + + + + +
+ {t('station.settings.dx.describe')} +
+ + + {/* Language */} +
+ +
+ {LANGUAGES.map((lang) => ( + + ))} +
+
+ + )} + + {/* Map Layers Tab */} + {activeTab === 'layers' && ( +
+ {layers.length > 0 ? ( + layers.map(layer => ( +
+
+ + + {layer.category} + +
+ + {layer.enabled && ( +
+ + handleOpacityChange(layer.id, parseFloat(e.target.value) / 100)} + style={{ + width: '100%', + cursor: 'pointer' + }} + /> +
+ )} +
+ )) + ) : ( +
+ No map layers available +
+ )} +>>>>>>> Stashed changes
{themeDescriptions[theme]} diff --git a/src/lang/de.json b/src/lang/de.json new file mode 100644 index 0000000..7097c55 --- /dev/null +++ b/src/lang/de.json @@ -0,0 +1,48 @@ +{ + "cancel": "Abbrechen", + "station.settings.language": "Sprache", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Höhe (m)", + "station.settings.antenna": "Antenne", + "station.settings.button.save": "Einstellungen Speichern", + "station.settings.button.save.confirm": "Einstellungen werden im Browser gespeichert", + "station.settings.callsign": "Dein Rufzeichen", + "station.settings.describe": "Bitte gib dein Rufzeichen und Locator ein. Deine Einstellungen werden im Browser gespeichert.", + "station.settings.dx.describe": "→ Echtzeit DX Spider Feed über unseren dedizierten Proxy-Dienst", + "station.settings.dx.option1": "⭐ DX Spider Proxy (Empfohlen)", + "station.settings.dx.option2": "HamQTH Cluster", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (alle Quellen versuchen)", + "station.settings.dx.title": "DX Cluster Quelle", + "station.settings.layout": "Layout", + "station.settings.layout.classic": "Klassisch", + "station.settings.layout.classic.describe": "→ Original HamClock-Layout", + "station.settings.layout.modern": "Modern", + "station.settings.layout.modern.describe": "→ Modernes responsives Grid-Layout", + "station.settings.latitude": "Breitengrad", + "station.settings.locator": "Locator (oder Lat/Lon unten eingeben)", + "station.settings.longitude": "Längengrad", + "station.settings.power": "Leistung (W)", + "station.settings.theme": "DESIGN", + "station.settings.theme.dark": "Dunkel", + "station.settings.theme.dark.describe": "→ Modernes dunkles Design (Standard)", + "station.settings.theme.legacy": "Legacy", + "station.settings.theme.legacy.describe": "→ Grüner CRT-Terminal-Stil", + "station.settings.theme.light": "Hell", + "station.settings.theme.light.describe": "→ Helles Design für Tagbetrieb", + "station.settings.theme.retro": "Retro", + "station.settings.theme.retro.describe": "→ 90er Windows Retro-Stil", + "station.settings.timezone": "Zeitzone", + "station.settings.title": "⚙ Stationseinstellungen", + "station.settings.tip.env": "💡 Tipp: Für dauerhafte Konfiguration kopiere .env.example nach .env und setze CALLSIGN und LOCATOR", + "station.settings.useLocation": "📍 Meinen Standort verwenden", + "station.settings.useLocation.error1": "Standort konnte nicht ermittelt werden. Bitte manuell eingeben.", + "station.settings.useLocation.error2": "Geolokalisierung wird von deinem Browser nicht unterstützt.", + "station.settings.welcome": "👋 Willkommen bei OpenHamClock!" +} diff --git a/src/lang/en.json b/src/lang/en.json new file mode 100644 index 0000000..5a9cd72 --- /dev/null +++ b/src/lang/en.json @@ -0,0 +1,48 @@ +{ + "cancel": "Cancel", + "station.settings.language": "Language", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Altitude (m)", + "station.settings.antenna": "Antenna", + "station.settings.button.save": "Save Settings", + "station.settings.button.save.confirm": "Settings are saved in your browser", + "station.settings.callsign": "Your Callsign", + "station.settings.describe": "Please enter your callsign and grid square to get started. Your settings will be saved in your browser.", + "station.settings.dx.describe": "→ Real-time DX Spider feed via our dedicated proxy service", + "station.settings.dx.option1": "⭐ DX Spider Proxy (Recommended)", + "station.settings.dx.option2": "HamQTH Cluster", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (try all sources)", + "station.settings.dx.title": "DX Cluster Source", + "station.settings.layout": "Layout", + "station.settings.layout.classic": "Classic", + "station.settings.layout.classic.describe": "→ Original HamClock-style layout", + "station.settings.layout.modern": "Modern", + "station.settings.layout.modern.describe": "→ Modern responsive grid layout", + "station.settings.latitude": "Latitude", + "station.settings.locator": "Grid Square (or enter Lat/Lon below)", + "station.settings.longitude": "Longitude", + "station.settings.power": "Power (W)", + "station.settings.theme": "THEME", + "station.settings.theme.dark": "Dark", + "station.settings.theme.dark.describe": "→ Modern dark theme (default)", + "station.settings.theme.legacy": "Legacy", + "station.settings.theme.legacy.describe": "→ Green CRT terminal style", + "station.settings.theme.light": "Light", + "station.settings.theme.light.describe": "→ Light theme for daytime use", + "station.settings.theme.retro": "Retro", + "station.settings.theme.retro.describe": "→ 90s Windows retro style", + "station.settings.timezone": "Timezone", + "station.settings.title": "Station Settings", + "station.settings.tip.env": "💡 Tip: For permanent config, copy .env.example to .env and set CALLSIGN and LOCATOR", + "station.settings.useLocation": "📍 Use My Current Location", + "station.settings.useLocation.error1": "Unable to get location. Please enter manually.", + "station.settings.useLocation.error2": "Geolocation is not supported by your browser.", + "station.settings.welcome": "👋 Welcome to OpenHamClock!" +} diff --git a/src/lang/es.json b/src/lang/es.json new file mode 100644 index 0000000..750cfec --- /dev/null +++ b/src/lang/es.json @@ -0,0 +1,48 @@ +{ + "cancel": "Cancelar", + "station.settings.language": "Idioma", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Altitud (m)", + "station.settings.antenna": "Antena", + "station.settings.button.save": "Guardar Configuración", + "station.settings.button.save.confirm": "La configuración se guarda en tu navegador", + "station.settings.callsign": "Tu Indicativo", + "station.settings.describe": "Ingresa tu indicativo y cuadrícula para comenzar. Tu configuración se guardará en el navegador.", + "station.settings.dx.describe": "→ Feed en tiempo real de DX Spider a través de nuestro servicio proxy dedicado", + "station.settings.dx.option1": "⭐ Proxy DX Spider (Recomendado)", + "station.settings.dx.option2": "Cluster HamQTH", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (probar todas las fuentes)", + "station.settings.dx.title": "Fuente del Cluster DX", + "station.settings.layout": "Diseño", + "station.settings.layout.classic": "Clásico", + "station.settings.layout.classic.describe": "→ Diseño estilo HamClock original", + "station.settings.layout.modern": "Moderno", + "station.settings.layout.modern.describe": "→ Diseño moderno con cuadrícula adaptable", + "station.settings.latitude": "Latitud", + "station.settings.locator": "Cuadrícula (o ingresa Lat/Lon abajo)", + "station.settings.longitude": "Longitud", + "station.settings.power": "Potencia (W)", + "station.settings.theme": "TEMA", + "station.settings.theme.dark": "Oscuro", + "station.settings.theme.dark.describe": "→ Tema oscuro moderno (predeterminado)", + "station.settings.theme.legacy": "Legacy", + "station.settings.theme.legacy.describe": "→ Estilo terminal CRT verde", + "station.settings.theme.light": "Claro", + "station.settings.theme.light.describe": "→ Tema claro para uso diurno", + "station.settings.theme.retro": "Retro", + "station.settings.theme.retro.describe": "→ Estilo retro Windows años 90", + "station.settings.timezone": "Zona horaria", + "station.settings.title": "⚙ Configuración de Estación", + "station.settings.tip.env": "💡 Consejo: Para configuración permanente, copia .env.example a .env y configura CALLSIGN y LOCATOR", + "station.settings.useLocation": "📍 Usar Mi Ubicación Actual", + "station.settings.useLocation.error1": "No se pudo obtener la ubicación. Por favor ingrésala manualmente.", + "station.settings.useLocation.error2": "La geolocalización no es compatible con tu navegador.", + "station.settings.welcome": "👋 ¡Bienvenido a OpenHamClock!" +} diff --git a/src/lang/fr.json b/src/lang/fr.json new file mode 100644 index 0000000..14a2790 --- /dev/null +++ b/src/lang/fr.json @@ -0,0 +1,48 @@ +{ + "cancel": "Annuler", + "station.settings.language": "Langue", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Altitude (m)", + "station.settings.antenna": "Antenne", + "station.settings.button.save": "Enregistrer les paramètres", + "station.settings.button.save.confirm": "Les paramètres sont enregistrés dans votre navigateur", + "station.settings.callsign": "Indicatif d'appel", + "station.settings.describe": "Veuillez entrer votre indicatif d'appel et votre carré de grille pour commencer. Vos paramètres seront enregistrés dans votre navigateur.", + "station.settings.dx.describe": "→ Flux en temps réel de DX Spider via notre service proxy dédié", + "station.settings.dx.option1": "⭐ Proxy DX Spider (Recommandé)", + "station.settings.dx.option2": "Cluster HamQTH", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (essayer toutes les sources)", + "station.settings.dx.title": "Source du cluster DX", + "station.settings.layout": "Disposition", + "station.settings.layout.classic": "Classique", + "station.settings.layout.classic.describe": "→ Disposition de style HamClock original", + "station.settings.layout.modern": "Moderne", + "station.settings.layout.modern.describe": "→ Disposition en grille réactive moderne", + "station.settings.latitude": "Latitude", + "station.settings.locator": "Carré de grille (ou entrez Lat/Lon ci-dessous)", + "station.settings.longitude": "Longitude", + "station.settings.power": "Puissance (W)", + "station.settings.theme": "THÈME", + "station.settings.theme.dark": "Sombre", + "station.settings.theme.dark.describe": "→ Thème sombre moderne (par défaut)", + "station.settings.theme.legacy": "Classique", + "station.settings.theme.legacy.describe": "→ Style CRT terminal vert", + "station.settings.theme.light": "Clair", + "station.settings.theme.light.describe": "→ Thème clair pour une utilisation diurne", + "station.settings.theme.retro": "Rétro", + "station.settings.theme.retro.describe": "→ Style rétro Windows des années 90", + "station.settings.timezone": "Fuseau horaire", + "station.settings.title": "⚙ Paramètres de la station", + "station.settings.tip.env": "💡 Astuce : Pour une configuration permanente, copiez .env.example vers .env et définissez Indicatif d'appel et Carré de grille", + "station.settings.useLocation": "📍 Utiliser ma position actuelle", + "station.settings.useLocation.error1": "Impossible d'obtenir la position. Veuillez entrer manuellement.", + "station.settings.useLocation.error2": "La géolocalisation n'est pas prise en charge par votre navigateur.", + "station.settings.welcome": "👋 Bienvenue sur OpenHamClock !" +} diff --git a/src/lang/i18n.js b/src/lang/i18n.js new file mode 100644 index 0000000..e7bf69e --- /dev/null +++ b/src/lang/i18n.js @@ -0,0 +1,48 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +import translationEN from './en.json'; +import translationFR from './fr.json'; +import translationES from './es.json'; +import translationDE from './de.json'; +import translationPT from './pt.json'; +import translationJA from './ja.json'; +import translationIT from './it.json'; + +export const LANGUAGES = [ + { code: 'en', name: 'English', flag: '🇬🇧' }, + { code: 'fr', name: 'Français', flag: '🇫🇷' }, + { code: 'es', name: 'Español', flag: '🇪🇸' }, + { code: 'de', name: 'Deutsch', flag: '🇩🇪' }, + { code: 'pt', name: 'Português', flag: '🇧🇷' }, + { code: 'ja', name: '日本語', flag: '🇯🇵' }, + { code: 'it', name: 'Italiano', flag: '🇮🇹' } +]; + +export const resources = { + en: { translation: translationEN }, + fr: { translation: translationFR }, + es: { translation: translationES }, + de: { translation: translationDE }, + pt: { translation: translationPT }, + ja: { translation: translationJA }, + it: { translation: translationIT } +}; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + resources, + interpolation: { + escapeValue: false + }, + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'] + } + }); + +export default i18n; diff --git a/src/lang/it.json b/src/lang/it.json new file mode 100644 index 0000000..8406773 --- /dev/null +++ b/src/lang/it.json @@ -0,0 +1,48 @@ +{ + "cancel": "Annulla", + "station.settings.language": "Lingua", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Altitudine (m)", + "station.settings.antenna": "Antenna", + "station.settings.button.save": "Salva Impostazioni", + "station.settings.button.save.confirm": "Le impostazioni vengono salvate nel browser", + "station.settings.callsign": "Il Tuo Nominativo", + "station.settings.describe": "Inserisci il tuo nominativo e il locatore per iniziare. Le impostazioni saranno salvate nel browser.", + "station.settings.dx.describe": "→ Feed in tempo reale da DX Spider tramite il nostro servizio proxy dedicato", + "station.settings.dx.option1": "⭐ Proxy DX Spider (Consigliato)", + "station.settings.dx.option2": "Cluster HamQTH", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (prova tutte le fonti)", + "station.settings.dx.title": "Fonte Cluster DX", + "station.settings.layout": "Layout", + "station.settings.layout.classic": "Classico", + "station.settings.layout.classic.describe": "→ Layout stile HamClock originale", + "station.settings.layout.modern": "Moderno", + "station.settings.layout.modern.describe": "→ Layout moderno con griglia reattiva", + "station.settings.latitude": "Latitudine", + "station.settings.locator": "Locatore (o inserisci Lat/Lon sotto)", + "station.settings.longitude": "Longitudine", + "station.settings.power": "Potenza (W)", + "station.settings.theme": "TEMA", + "station.settings.theme.dark": "Scuro", + "station.settings.theme.dark.describe": "→ Tema scuro moderno (predefinito)", + "station.settings.theme.legacy": "Legacy", + "station.settings.theme.legacy.describe": "→ Stile terminale CRT verde", + "station.settings.theme.light": "Chiaro", + "station.settings.theme.light.describe": "→ Tema chiaro per uso diurno", + "station.settings.theme.retro": "Retro", + "station.settings.theme.retro.describe": "→ Stile retro Windows anni '90", + "station.settings.timezone": "Fuso orario", + "station.settings.title": "⚙ Impostazioni Stazione", + "station.settings.tip.env": "💡 Suggerimento: Per una configurazione permanente, copia .env.example in .env e imposta CALLSIGN e LOCATOR", + "station.settings.useLocation": "📍 Usa la Mia Posizione Attuale", + "station.settings.useLocation.error1": "Impossibile ottenere la posizione. Inseriscila manualmente.", + "station.settings.useLocation.error2": "La geolocalizzazione non è supportata dal tuo browser.", + "station.settings.welcome": "👋 Benvenuto su OpenHamClock!" +} diff --git a/src/lang/ja.json b/src/lang/ja.json new file mode 100644 index 0000000..7378688 --- /dev/null +++ b/src/lang/ja.json @@ -0,0 +1,48 @@ +{ + "cancel": "キャンセル", + "station.settings.language": "言語", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "標高 (m)", + "station.settings.antenna": "アンテナ", + "station.settings.button.save": "設定を保存", + "station.settings.button.save.confirm": "設定はブラウザに保存されます", + "station.settings.callsign": "コールサイン", + "station.settings.describe": "コールサインとグリッドロケーターを入力してください。設定はブラウザに保存されます。", + "station.settings.dx.describe": "→ 専用プロキシサービス経由のリアルタイムDX Spiderフィード", + "station.settings.dx.option1": "⭐ DX Spider プロキシ(推奨)", + "station.settings.dx.option2": "HamQTH クラスター", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "自動(すべてのソースを試行)", + "station.settings.dx.title": "DXクラスターソース", + "station.settings.layout": "レイアウト", + "station.settings.layout.classic": "クラシック", + "station.settings.layout.classic.describe": "→ オリジナルHamClockスタイルのレイアウト", + "station.settings.layout.modern": "モダン", + "station.settings.layout.modern.describe": "→ モダンなレスポンシブグリッドレイアウト", + "station.settings.latitude": "緯度", + "station.settings.locator": "グリッドロケーター(または下に緯度/経度を入力)", + "station.settings.longitude": "経度", + "station.settings.power": "出力 (W)", + "station.settings.theme": "テーマ", + "station.settings.theme.dark": "ダーク", + "station.settings.theme.dark.describe": "→ モダンなダークテーマ(デフォルト)", + "station.settings.theme.legacy": "レガシー", + "station.settings.theme.legacy.describe": "→ グリーンCRTターミナルスタイル", + "station.settings.theme.light": "ライト", + "station.settings.theme.light.describe": "→ 日中使用向けライトテーマ", + "station.settings.theme.retro": "レトロ", + "station.settings.theme.retro.describe": "→ 90年代Windowsレトロスタイル", + "station.settings.timezone": "タイムゾーン", + "station.settings.title": "⚙ 局設定", + "station.settings.tip.env": "💡 ヒント: 恒久的な設定には .env.example.env にコピーしてCALLSIGNとLOCATORを設定", + "station.settings.useLocation": "📍 現在地を使用", + "station.settings.useLocation.error1": "位置情報を取得できません。手動で入力してください。", + "station.settings.useLocation.error2": "お使いのブラウザはジオロケーションに対応していません。", + "station.settings.welcome": "👋 OpenHamClockへようこそ!" +} diff --git a/src/lang/pt.json b/src/lang/pt.json new file mode 100644 index 0000000..f7a81df --- /dev/null +++ b/src/lang/pt.json @@ -0,0 +1,48 @@ +{ + "cancel": "Cancelar", + "station.settings.language": "Idioma", + "station.settings.language.en": "English", + "station.settings.language.fr": "Français", + "station.settings.language.es": "Español", + "station.settings.language.de": "Deutsch", + "station.settings.language.pt": "Português", + "station.settings.language.ja": "日本語", + "station.settings.language.it": "Italiano", + "station.settings.altitude": "Altitude (m)", + "station.settings.antenna": "Antena", + "station.settings.button.save": "Salvar Configurações", + "station.settings.button.save.confirm": "As configurações são salvas no seu navegador", + "station.settings.callsign": "Seu Indicativo", + "station.settings.describe": "Digite seu indicativo e localizador para começar. Suas configurações serão salvas no navegador.", + "station.settings.dx.describe": "→ Feed em tempo real do DX Spider através do nosso serviço proxy dedicado", + "station.settings.dx.option1": "⭐ Proxy DX Spider (Recomendado)", + "station.settings.dx.option2": "Cluster HamQTH", + "station.settings.dx.option3": "DXWatch", + "station.settings.dx.option4": "Auto (tentar todas as fontes)", + "station.settings.dx.title": "Fonte do Cluster DX", + "station.settings.layout": "Layout", + "station.settings.layout.classic": "Clássico", + "station.settings.layout.classic.describe": "→ Layout estilo HamClock original", + "station.settings.layout.modern": "Moderno", + "station.settings.layout.modern.describe": "→ Layout moderno com grade responsiva", + "station.settings.latitude": "Latitude", + "station.settings.locator": "Localizador (ou digite Lat/Lon abaixo)", + "station.settings.longitude": "Longitude", + "station.settings.power": "Potência (W)", + "station.settings.theme": "TEMA", + "station.settings.theme.dark": "Escuro", + "station.settings.theme.dark.describe": "→ Tema escuro moderno (padrão)", + "station.settings.theme.legacy": "Legacy", + "station.settings.theme.legacy.describe": "→ Estilo terminal CRT verde", + "station.settings.theme.light": "Claro", + "station.settings.theme.light.describe": "→ Tema claro para uso diurno", + "station.settings.theme.retro": "Retro", + "station.settings.theme.retro.describe": "→ Estilo retro Windows anos 90", + "station.settings.timezone": "Fuso horário", + "station.settings.title": "⚙ Configurações da Estação", + "station.settings.tip.env": "💡 Dica: Para configuração permanente, copie .env.example para .env e defina CALLSIGN e LOCATOR", + "station.settings.useLocation": "📍 Usar Minha Localização Atual", + "station.settings.useLocation.error1": "Não foi possível obter a localização. Por favor, insira manualmente.", + "station.settings.useLocation.error2": "Geolocalização não é suportada pelo seu navegador.", + "station.settings.welcome": "👋 Bem-vindo ao OpenHamClock!" +} From df6fd0c7ff9b23e368ca191fae398b96908a1125 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 18:11:14 -0500 Subject: [PATCH 2/2] PR Merge backwards --- package.json | 5 +- src/components/PluginLayer.jsx | 16 + src/components/SettingsPanel.jsx | 559 +++++----- src/components/WorldMap.jsx | 100 +- src/main.jsx | 1 + src/plugins/OpenHamClock-Plugin-Guide.md | 1204 ++++++++++++++++++++++ src/plugins/layerRegistry.js | 32 + src/plugins/layers/useEarthquakes.js | 145 +++ src/plugins/layers/useWXRadar.js | 88 ++ 9 files changed, 1907 insertions(+), 243 deletions(-) create mode 100644 src/components/PluginLayer.jsx create mode 100644 src/plugins/OpenHamClock-Plugin-Guide.md create mode 100644 src/plugins/layerRegistry.js create mode 100644 src/plugins/layers/useEarthquakes.js create mode 100644 src/plugins/layers/useWXRadar.js diff --git a/package.json b/package.json index 122c622..ff37c72 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,11 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "i18next": "^25.8.0", + "i18next-browser-languagedetector": "^8.2.0", "mqtt": "^5.3.4", "node-fetch": "^2.7.0", + "react-i18next": "^16.5.4", "satellite.js": "^5.0.0", "ws": "^8.14.2" }, @@ -41,4 +44,4 @@ ], "author": "K0CJH", "license": "MIT" -} +} \ No newline at end of file diff --git a/src/components/PluginLayer.jsx b/src/components/PluginLayer.jsx new file mode 100644 index 0000000..ced3f66 --- /dev/null +++ b/src/components/PluginLayer.jsx @@ -0,0 +1,16 @@ +/** + * PluginLayer Component + * Renders a single plugin layer using its hook + */ +import React from 'react'; + +export const PluginLayer = ({ plugin, enabled, opacity, map }) => { + // Call the plugin's hook (this is allowed because it's in a component) + const result = plugin.hook({ enabled, opacity, map }); + + // Plugin hook handles its own rendering to the map + // This component doesn't render anything to the DOM + return null; +}; + +export default PluginLayer; diff --git a/src/components/SettingsPanel.jsx b/src/components/SettingsPanel.jsx index 73ef1b9..63770fc 100644 --- a/src/components/SettingsPanel.jsx +++ b/src/components/SettingsPanel.jsx @@ -1,14 +1,11 @@ /** * SettingsPanel Component - * Full settings modal matching production version + * Full settings modal with map layer controls */ import React, { useState, useEffect } from 'react'; import { calculateGridSquare } from '../utils/geo.js'; -<<<<<<< Updated upstream -======= import { useTranslation, Trans } from 'react-i18next'; import { LANGUAGES } from '../lang/i18n.js'; ->>>>>>> Stashed changes export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [callsign, setCallsign] = useState(config?.callsign || ''); @@ -18,14 +15,11 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { const [theme, setTheme] = useState(config?.theme || 'dark'); const [layout, setLayout] = useState(config?.layout || 'modern'); const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy'); -<<<<<<< Updated upstream -======= const { t, i18n } = useTranslation(); // Layer controls const [layers, setLayers] = useState([]); const [activeTab, setActiveTab] = useState('station'); ->>>>>>> Stashed changes useEffect(() => { if (config) { @@ -35,19 +29,33 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { setTheme(config.theme || 'dark'); setLayout(config.layout || 'modern'); setDxClusterSource(config.dxClusterSource || 'dxspider-proxy'); - // Use locator from config, or calculate from coordinates - if (config.locator) { - setGridSquare(config.locator); - } else if (config.location?.lat && config.location?.lon) { + if (config.location?.lat && config.location?.lon) { setGridSquare(calculateGridSquare(config.location.lat, config.location.lon)); } } }, [config, isOpen]); - // Update lat/lon when grid square changes + // 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()); - // Parse grid square to lat/lon if valid (6 char) if (grid.length >= 4) { const parsed = parseGridSquare(grid); if (parsed) { @@ -57,7 +65,6 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { } }; - // Parse grid square to coordinates const parseGridSquare = (grid) => { grid = grid.toUpperCase(); if (grid.length < 4) return null; @@ -80,7 +87,6 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { return { lat, lon }; }; - // Update grid when lat/lon changes useEffect(() => { if (lat && lon) { setGridSquare(calculateGridSquare(lat, lon)); @@ -96,11 +102,42 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { }, (error) => { console.error('Geolocation error:', error); - alert('Unable to get location. Please enter manually.'); + alert(t('station.settings.useLocation.error1')); } ); } else { - alert('Geolocation not supported by your browser.'); + 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]); } }; @@ -108,7 +145,6 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { onSave({ ...config, callsign: callsign.toUpperCase(), - locator: gridSquare.toUpperCase(), location: { lat: parseFloat(lat), lon: parseFloat(lon) }, theme, layout, @@ -119,16 +155,22 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { if (!isOpen) return null; + const Code = ({ children }) => ( + + {children} + + ); + const themeDescriptions = { - dark: '→ Modern dark theme (default)', - light: '→ Light theme for daytime use', - legacy: '→ Green terminal CRT style', - retro: '→ 90s Windows retro style' + 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: '→ Modern responsive grid layout', - classic: '→ Original HamClock-style layout' + modern: t('station.settings.layout.modern.describe'), + classic: t('station.settings.layout.classic.describe') }; return ( @@ -149,186 +191,285 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { border: '2px solid var(--accent-amber)', borderRadius: '12px', padding: '24px', - width: '420px', + width: '520px', maxHeight: '90vh', overflowY: 'auto' }}>

- ⚙ Station Settings + {t('station.settings.title')}

- {/* First-time setup banner */} - {(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && ( -
-
- 👋 Welcome to OpenHamClock! -
-
- Please enter your callsign and grid square to get started. - Your settings will be saved in your browser. -
-
- 💡 Tip: For permanent config, copy .env.example to .env and set CALLSIGN and LOCATOR -
-
- )} - - {/* Callsign */} -
- - setCallsign(e.target.value.toUpperCase())} + {/* Tab Navigation */} +
+
- - {/* Grid Square */} -
- - handleGridChange(e.target.value)} - placeholder="FN20nc" - maxLength={6} + > + 📡 Station + +
- {/* 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' - }} - /> -
-
- - setLon(parseFloat(e.target.value))} + {/* Station Settings Tab */} + {activeTab === 'station' && ( + <> + {/* First-time setup banner */} + {(config?.configIncomplete || config?.callsign === 'N0CALL' || !config?.locator) && ( +
+
+ {t("station.settings.welcome")} +
+
+ {t("station.settings.describe")} +
+
+ , env: }} /> +
+
+ )} + + {/* Callsign */} +
+ + 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' + }} + /> +
+ + {/* 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) || 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' + }} + /> +
+
+ + 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' + }} + /> +
+
+ +
-
+ > + {t('station.settings.useLocation')} + - {/* Use My Location button */} - + {/* Theme */} +
+ +
+ {['dark', 'light', 'legacy', 'retro'].map((th) => ( + + ))} +
+
+ {themeDescriptions[theme]} +
+
- {/* Theme */} -
- -
- {['dark', 'light', 'legacy', 'retro'].map((t) => ( - + ))} +
+
+ {layoutDescriptions[layout]} +
+
+ + {/* DX Cluster Source */} +
+ + 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' - }} - > - - - - - -
- → Real-time DX Spider feed via our dedicated proxy service -
-
+ )} {/* Buttons */}
@@ -556,7 +633,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => { cursor: 'pointer' }} > - Cancel + {t('cancel')}
- Settings are saved in your browser + {t('station.settings.button.save.confirm')}
diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index de8181e..b7043e6 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -1,6 +1,6 @@ /** * WorldMap Component - * Leaflet map with DE/DX markers, terminator, DX paths, POTA, satellites + * Leaflet map with DE/DX markers, terminator, DX paths, POTA, satellites, PSKReporter */ import React, { useRef, useEffect, useState } from 'react'; import { MAP_STYLES } from '../utils/config.js'; @@ -12,6 +12,10 @@ import { } from '../utils/geo.js'; import { filterDXPaths, getBandColor } from '../utils/callsign.js'; +import { getAllLayers } from '../plugins/layerRegistry.js'; +import PluginLayer from './PluginLayer.jsx'; + + export const WorldMap = ({ deLocation, dxLocation, @@ -47,6 +51,10 @@ export const WorldMap = ({ const satMarkersRef = useRef([]); const satTracksRef = useRef([]); const pskMarkersRef = useRef([]); + + // Plugin system refs and state + const pluginLayersRef = useRef({}); + const [pluginLayerStates, setPluginLayerStates] = useState({}); // Load map style from localStorage const getStoredMapSettings = () => { @@ -419,6 +427,84 @@ export const WorldMap = ({ } }, [satellites, showSatellites]); + // Plugin layer system - properly load saved states + useEffect(() => { + if (!mapInstanceRef.current) return; + + try { + const availableLayers = getAllLayers(); + const settings = getStoredMapSettings(); + const savedLayers = settings.layers || {}; + + // Build initial states from localStorage + const initialStates = {}; + availableLayers.forEach(layerDef => { + // Use saved state if it exists, otherwise use defaults + if (savedLayers[layerDef.id]) { + initialStates[layerDef.id] = savedLayers[layerDef.id]; + } else { + initialStates[layerDef.id] = { + enabled: layerDef.defaultEnabled, + opacity: layerDef.defaultOpacity + }; + } + }); + + // Initialize state ONLY on first mount (when empty) + if (Object.keys(pluginLayerStates).length === 0) { + console.log('Loading saved layer states:', initialStates); + setPluginLayerStates(initialStates); + } + + // Expose controls for SettingsPanel + window.hamclockLayerControls = { + layers: availableLayers.map(l => ({ + ...l, + enabled: pluginLayerStates[l.id]?.enabled ?? initialStates[l.id]?.enabled ?? l.defaultEnabled, + opacity: pluginLayerStates[l.id]?.opacity ?? initialStates[l.id]?.opacity ?? l.defaultOpacity + })), + toggleLayer: (id, enabled) => { + console.log(`Toggle layer ${id}:`, enabled); + const settings = getStoredMapSettings(); + const layers = settings.layers || {}; + layers[id] = { + enabled: enabled, + opacity: layers[id]?.opacity ?? 0.6 + }; + localStorage.setItem('openhamclock_mapSettings', JSON.stringify({ ...settings, layers })); + console.log('Saved to localStorage:', layers); + setPluginLayerStates(prev => ({ + ...prev, + [id]: { + ...prev[id], + enabled: enabled + } + })); + }, + setOpacity: (id, opacity) => { + console.log(`Set opacity ${id}:`, opacity); + const settings = getStoredMapSettings(); + const layers = settings.layers || {}; + layers[id] = { + enabled: layers[id]?.enabled ?? false, + opacity: opacity + }; + localStorage.setItem('openhamclock_mapSettings', JSON.stringify({ ...settings, layers })); + console.log('Saved to localStorage:', layers); + setPluginLayerStates(prev => ({ + ...prev, + [id]: { + ...prev[id], + opacity: opacity + } + })); + } + }; + } catch (err) { + console.error('Plugin system error:', err); + } + }, [pluginLayerStates]); + // Update PSKReporter markers useEffect(() => { if (!mapInstanceRef.current) return; @@ -488,6 +574,17 @@ export const WorldMap = ({
+ {/* Render all plugin layers */} + {mapInstanceRef.current && getAllLayers().map(layerDef => ( + + ))} + {/* Map style dropdown */}