diff --git a/attached_assets/image_1772316486233.png b/attached_assets/image_1772316486233.png new file mode 100644 index 0000000..ae5bc56 Binary files /dev/null and b/attached_assets/image_1772316486233.png differ diff --git a/client/src/components/breaking-news-widget.tsx b/client/src/components/breaking-news-widget.tsx new file mode 100644 index 0000000..b2b01d7 --- /dev/null +++ b/client/src/components/breaking-news-widget.tsx @@ -0,0 +1,95 @@ +import { useQuery } from "@tanstack/react-query"; +import { useState, useEffect, useCallback } from "react"; +import { Globe, ExternalLink } from "lucide-react"; + +interface NewsItem { + title: string; + link: string; + source: string; + pubDate: string; +} + +const VISIBLE_COUNT = 5; + +export function BreakingNewsWidget() { + const { data: news, isLoading } = useQuery({ + queryKey: ["/api/breaking-news"], + }); + + const [offset, setOffset] = useState(0); + const [paused, setPaused] = useState(false); + + const items = news || []; + const total = items.length; + + const advance = useCallback(() => { + if (total <= VISIBLE_COUNT) return; + setOffset((o) => (o + 1) % total); + }, [total]); + + useEffect(() => { + if (paused || total <= VISIBLE_COUNT) return; + const timer = setInterval(advance, 5000); + return () => clearInterval(timer); + }, [paused, advance, total]); + + const visible: NewsItem[] = []; + for (let i = 0; i < Math.min(VISIBLE_COUNT, total); i++) { + visible.push(items[(offset + i) % total]); + } + + return ( +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + data-testid="widget-breaking-news" + > +
+ +

Aktuelle Nachrichten

+
+
+ {visible.map((item, i) => ( + +
+
+

+ {item.title} +

+
+ {item.source} + {item.pubDate} +
+
+ +
+ {i < VISIBLE_COUNT - 1 &&
} + + ))} + {isLoading && ( +
+ {Array.from({ length: VISIBLE_COUNT }).map((_, i) => ( +
+
+
+
+ {i < VISIBLE_COUNT - 1 &&
} +
+ ))} +
+ )} + {!isLoading && total === 0 && ( +

Keine Nachrichten verfügbar

+ )} +
+
+ ); +} diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 7749963..42bb1fb 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -13,6 +13,7 @@ import { HoroscopeWidget } from "@/components/horoscope-widget"; import { RecipeWidget } from "@/components/recipe-widget"; import { NewsWidget } from "@/components/news-widget"; import { WeatherWidget } from "@/components/weather-widget"; +import { BreakingNewsWidget } from "@/components/breaking-news-widget"; import { useState, useEffect, useCallback, useRef, useMemo } from "react"; function useFocalPoints() { @@ -428,6 +429,7 @@ export default function Home() { { id: "horoscope-weather", el:
}, { id: "gallery", el: }, { id: "news", el:
}, + { id: "breaking", el:
}, ]; for (let i = w.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); @@ -440,16 +442,15 @@ export default function Home() { const items: { type: "article" | "widget"; article?: Article; widget?: typeof widgets[0] }[] = []; let ai = 0; let wi = 0; + const widgetRows = Math.ceil(widgets.length / 2); - items.push({ type: "article", article: shuffled[ai++] }); - items.push({ type: "article", article: shuffled[ai++] }); - if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); - if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); - - items.push({ type: "article", article: shuffled[ai++] }); - items.push({ type: "article", article: shuffled[ai++] }); - if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); - if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); + for (let r = 0; r < widgetRows; r++) { + if (ai < shuffled.length) items.push({ type: "article", article: shuffled[ai++] }); + if (ai < shuffled.length) items.push({ type: "article", article: shuffled[ai++] }); + if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); + if (wi < widgets.length) items.push({ type: "widget", widget: widgets[wi++] }); + else if (ai < shuffled.length) items.push({ type: "article", article: shuffled[ai++] }); + } while (ai < shuffled.length) { items.push({ type: "article", article: shuffled[ai++] }); diff --git a/server/routes.ts b/server/routes.ts index 00de594..f145864 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -293,5 +293,52 @@ export async function registerRoutes( } }); + app.get("/api/breaking-news", async (_req, res) => { + try { + const topics = ["Nachrichten+Deutschland", "Nachrichten+Oesterreich", "Nachrichten+Europa", "Wirtschaft+aktuell", "Sport+aktuell"]; + const topic = topics[Math.floor(Date.now() / 3600000) % topics.length]; + const rssUrl = `https://news.google.com/rss/search?q=${topic}&hl=de&gl=DE&ceid=DE:de`; + + const response = await new Promise((resolve, reject) => { + https.get(rssUrl, (resp) => { + let data = ""; + resp.on("data", (chunk: Buffer) => (data += chunk.toString())); + resp.on("end", () => resolve(data)); + resp.on("error", reject); + }).on("error", reject); + }); + + const items: { title: string; link: string; source: string; pubDate: string }[] = []; + const itemRegex = /([\s\S]*?)<\/item>/g; + let match; + while ((match = itemRegex.exec(response)) !== null && items.length < 10) { + const block = match[1]; + const title = block.match(/(.*?)<\/title>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || ""; + const link = block.match(/<link>(.*?)<\/link>/)?.[1] || ""; + const source = block.match(/<source[^>]*>(.*?)<\/source>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || ""; + const pubDateRaw = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1] || ""; + + let pubDate = ""; + try { + const d = new Date(pubDateRaw); + const diffH = Math.floor((Date.now() - d.getTime()) / 3600000); + if (diffH < 1) pubDate = "Gerade eben"; + else if (diffH < 24) pubDate = `vor ${diffH} Std.`; + else pubDate = `vor ${Math.floor(diffH / 24)} T.`; + } catch { + pubDate = ""; + } + + if (title && link) { + items.push({ title, link, source, pubDate }); + } + } + + res.json(items); + } catch (err: any) { + res.json([]); + } + }); + return httpServer; }