From bac64fc86bb8d3a413788695c45a3be22ea123c5 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 20:22:34 -0500 Subject: [PATCH 1/3] news scroller --- server.js | 68 ++++++++++++ src/components/DXNewsTicker.jsx | 176 ++++++++++++++++++++++++++++++++ src/components/WorldMap.jsx | 19 ++-- src/components/index.js | 1 + src/styles/main.css | 1 + 5 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/components/DXNewsTicker.jsx diff --git a/server.js b/server.js index 213b9d0..11c86c3 100644 --- a/server.js +++ b/server.js @@ -703,6 +703,74 @@ app.get('/api/noaa/aurora', async (req, res) => { } }); +// DX News from dxnews.com +let dxNewsCache = { data: null, timestamp: 0 }; +const DXNEWS_CACHE_TTL = 30 * 60 * 1000; // 30 minutes + +app.get('/api/dxnews', async (req, res) => { + try { + if (dxNewsCache.data && (Date.now() - dxNewsCache.timestamp) < DXNEWS_CACHE_TTL) { + return res.json(dxNewsCache.data); + } + + const response = await fetch('https://dxnews.com/', { + headers: { 'User-Agent': 'OpenHamClock/1.0 (amateur radio dashboard)' } + }); + const html = await response.text(); + + // Parse news items from HTML + const items = []; + // Match pattern:

TITLE

followed by date and description + const articleRegex = /]*>\s*]*>[^<]*<\/a>\s*<\/h3>\s*[\s\S]*?(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s*[\s\S]*?<\/li>\s*<\/ul>\s*([\s\S]*?)(?:)/g; + + // Simpler approach: split by article blocks + const blocks = html.split(/]*>\s*]+>/g, ' ') + .replace(/\s+/g, ' ') + .replace(/Views\d+.*$/, '') + .replace(/More\.\.\..*$/, '') + .trim() + .substring(0, 200); + } + + if (titleMatch && urlMatch) { + items.push({ + title: titleMatch[1], + url: 'https://dxnews.com/' + urlMatch[1], + date: dateMatch ? dateMatch[1] : null, + description: desc || titleMatch[1] + }); + } + } catch (e) { + // Skip malformed entries + } + } + + const result = { items, fetched: new Date().toISOString() }; + dxNewsCache = { data: result, timestamp: Date.now() }; + res.json(result); + } catch (error) { + console.error('DX News fetch error:', error.message); + if (dxNewsCache.data) return res.json(dxNewsCache.data); + res.status(500).json({ error: 'Failed to fetch DX news', items: [] }); + } +}); + // POTA Spots // POTA cache (2 minutes) let potaCache = { data: null, timestamp: 0 }; diff --git a/src/components/DXNewsTicker.jsx b/src/components/DXNewsTicker.jsx new file mode 100644 index 0000000..4e27c02 --- /dev/null +++ b/src/components/DXNewsTicker.jsx @@ -0,0 +1,176 @@ +/** + * DXNewsTicker Component + * Scrolling news banner showing latest DX news headlines from dxnews.com + */ +import React, { useState, useEffect, useRef } from 'react'; + +export const DXNewsTicker = () => { + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + const tickerRef = useRef(null); + const contentRef = useRef(null); + const [animDuration, setAnimDuration] = useState(120); + + // Fetch news + useEffect(() => { + const fetchNews = async () => { + try { + const res = await fetch('/api/dxnews'); + if (res.ok) { + const data = await res.json(); + if (data.items && data.items.length > 0) { + setNews(data.items); + } + } + } catch (err) { + console.error('DX News ticker fetch error:', err); + } finally { + setLoading(false); + } + }; + + fetchNews(); + // Refresh every 30 minutes + const interval = setInterval(fetchNews, 30 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + // Calculate animation duration based on content width + useEffect(() => { + if (contentRef.current && tickerRef.current) { + const contentWidth = contentRef.current.scrollWidth; + const containerWidth = tickerRef.current.offsetWidth; + // ~50px per second scroll speed + const duration = Math.max(30, (contentWidth + containerWidth) / 50); + setAnimDuration(duration); + } + }, [news]); + + if (loading || news.length === 0) return null; + + // Build ticker text: "TITLE — description ★ TITLE — description ★ ..." + const tickerItems = news.map(item => ({ + title: item.title, + desc: item.description + })); + + return ( +
+ {/* DX NEWS label */} +
+ 📰 DX NEWS +
+ + {/* Scrolling content */} +
+
+ {tickerItems.map((item, i) => ( + + + {item.title} + + + {item.desc} + + + ◆ + + + ))} + {/* Duplicate for seamless loop */} + {tickerItems.map((item, i) => ( + + + {item.title} + + + {item.desc} + + + ◆ + + + ))} +
+
+
+ ); +}; + +export default DXNewsTicker; diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 4bfb23f..8b1caa8 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -14,6 +14,7 @@ import { filterDXPaths, getBandColor } from '../utils/callsign.js'; import { getAllLayers } from '../plugins/layerRegistry.js'; import PluginLayer from './PluginLayer.jsx'; +import { DXNewsTicker } from './DXNewsTicker.jsx'; export const WorldMap = ({ @@ -732,22 +733,26 @@ export const WorldMap = ({ )} - {/* Legend */} + {/* DX News Ticker - left side of bottom bar */} + + + {/* Legend - right side */}
{showDXPaths && (
diff --git a/src/components/index.js b/src/components/index.js index 3093e72..e0294c4 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -18,3 +18,4 @@ export { SolarPanel } from './SolarPanel.jsx'; export { PropagationPanel } from './PropagationPanel.jsx'; export { DXpeditionPanel } from './DXpeditionPanel.jsx'; export { PSKReporterPanel } from './PSKReporterPanel.jsx'; +export { DXNewsTicker } from './DXNewsTicker.jsx'; diff --git a/src/styles/main.css b/src/styles/main.css index cec6b54..ef225f9 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -323,6 +323,7 @@ body::before { @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } +@keyframes dxnews-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } .loading-spinner { width: 14px; height: 14px; From 933b074461487068f2b7fcc69364946dd499c3eb Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 20:31:57 -0500 Subject: [PATCH 2/3] news scroller updates --- .dockerignore | 14 ++++++++++++++ server.js | 10 ++++++---- src/components/DXNewsTicker.jsx | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1ce90c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +node_modules +dist +.git +.env +.env.local +*.log +npm-debug.log* +.DS_Store +.vscode +.idea +*.zip +*.tar.gz +dxspider-proxy/node_modules +iturhfprop-service/node_modules diff --git a/server.js b/server.js index 11c86c3..008ca99 100644 --- a/server.js +++ b/server.js @@ -734,16 +734,18 @@ app.get('/api/dxnews', async (req, res) => { const titleMatch = block.match(/title="([^"]+)"/); // Extract date const dateMatch = block.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/); - // Extract description - text after the date, before "Views" or next element + // Extract description - text after the date, before stats const descParts = block.split(/\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/); let desc = ''; if (descParts[1]) { - // Get text content, strip HTML tags + // Get text content, strip HTML tags, then remove stats/junk desc = descParts[1] .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') - .replace(/Views\d+.*$/, '') - .replace(/More\.\.\..*$/, '') + .replace(/Views\s*\d+.*/i, '') + .replace(/Comments\s*\d+.*/i, '') + .replace(/\d+%/, '') + .replace(/More\.\.\..*/i, '') .trim() .substring(0, 200); } diff --git a/src/components/DXNewsTicker.jsx b/src/components/DXNewsTicker.jsx index 4e27c02..edf72db 100644 --- a/src/components/DXNewsTicker.jsx +++ b/src/components/DXNewsTicker.jsx @@ -40,8 +40,8 @@ export const DXNewsTicker = () => { if (contentRef.current && tickerRef.current) { const contentWidth = contentRef.current.scrollWidth; const containerWidth = tickerRef.current.offsetWidth; - // ~50px per second scroll speed - const duration = Math.max(30, (contentWidth + containerWidth) / 50); + // ~90px per second scroll speed + const duration = Math.max(20, (contentWidth + containerWidth) / 90); setAnimDuration(duration); } }, [news]); From 173e9085f79a5085820a19845442d3dbcda66c3e Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 2 Feb 2026 20:35:15 -0500 Subject: [PATCH 3/3] Update WorldMap.jsx --- src/components/WorldMap.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/WorldMap.jsx b/src/components/WorldMap.jsx index 8b1caa8..89ad624 100644 --- a/src/components/WorldMap.jsx +++ b/src/components/WorldMap.jsx @@ -778,11 +778,6 @@ export const WorldMap = ({ ● POTA
)} - {showSatellites && ( -
- 🛰 SAT -
- )}
☀ Sun 🌙 Moon