trending-news-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # trending-news-api
2
+
3
+ React trending news widget: daily top Wikipedia pages (via the Wikimedia Pageviews API) matched
4
+ against headlines from [The News API](https://www.thenewsapi.com/), served through a bundled
5
+ Cloudflare Worker so the News API key never reaches the browser.
6
+
7
+ ## Features
8
+
9
+ - Daily trending topics, ranked by Wikipedia pageviews.
10
+ - Matching headlines per topic from The News API.
11
+ - Single-topic headline lookup.
12
+ - Compact card row or full article-list layouts.
13
+ - `localStorage` response caching (10 minutes).
14
+ - TypeScript + tsup library scaffold.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install trending-news-api
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```tsx
25
+ import { TrendingNews } from 'trending-news-api';
26
+
27
+ export default function App() {
28
+ return (
29
+ <TrendingNews
30
+ compact
31
+ maxTopics={8}
32
+ apiEndpoint="https://trending-news-api.your-subdomain.workers.dev"
33
+ />
34
+ );
35
+ }
36
+ ```
37
+
38
+ Pass `topic` to render headlines for a single topic instead of the daily trending list:
39
+
40
+ ```tsx
41
+ <TrendingNews apiEndpoint="https://trending-news-api.your-subdomain.workers.dev" topic="Donald Trump" />
42
+ ```
43
+
44
+ The widget renders nothing when `apiEndpoint` is unset, still loading, or errored — safe to drop
45
+ into a layout unconditionally.
46
+
47
+ ## Direct API usage
48
+
49
+ ```ts
50
+ import { getTrendingNews, getTrendingNewsForTopic } from 'trending-news-api';
51
+
52
+ const trending = await getTrendingNews({
53
+ apiEndpoint: 'https://trending-news-api.your-subdomain.workers.dev',
54
+ });
55
+
56
+ const topicNews = await getTrendingNewsForTopic('Donald Trump', {
57
+ apiEndpoint: 'https://trending-news-api.your-subdomain.workers.dev',
58
+ });
59
+ ```
60
+
61
+ ## Build
62
+
63
+ ```bash
64
+ npm install
65
+ npm run build
66
+ ```
67
+
68
+ ## The worker backend
69
+
70
+ `worker/index.ts` is a Cloudflare Worker that:
71
+
72
+ - Fetches yesterday's top Wikipedia pages by pageviews (Wikimedia REST API), filtering out
73
+ non-article pages (`Main_Page`, `Special:`, `Wikipedia:`, etc).
74
+ - For each page, searches The News API for matching headlines.
75
+ - Exposes:
76
+ - `GET /` — trending topics with headline counts and articles.
77
+ - `GET /?topic=...` — headlines for a specific topic.
78
+
79
+ ### Deploying the worker
80
+
81
+ ```bash
82
+ cd packages/trending-news-api
83
+ npm run worker:deploy
84
+ npx wrangler secret put THENEWSAPI_API_KEY --config worker/wrangler.jsonc
85
+ ```
86
+
87
+ Use the resulting `*.workers.dev` URL (or a custom route) as `apiEndpoint`.
88
+
89
+ ## Caching
90
+
91
+ `getTrendingNews` / `getTrendingNewsForTopic` cache each response in `localStorage` for 10
92
+ minutes, keyed by the exact request URL. Call `clearTrendingNewsCache()` to evict everything
93
+ (e.g. in tests). The cache is a no-op in non-browser environments (SSR) or when `localStorage`
94
+ is unavailable/full.
95
+
96
+ ## Notes
97
+
98
+ - Wikimedia's Pageviews API powers the trending topic list.
99
+ - The News API (thenewsapi.com) powers the headlines — you'll need a free or paid API key.
package/dist/index.cjs ADDED
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ TrendingNews: () => TrendingNews,
24
+ clearTrendingNewsCache: () => clearTrendingNewsCache,
25
+ getTrendingNews: () => getTrendingNews,
26
+ getTrendingNewsForTopic: () => getTrendingNewsForTopic,
27
+ useTrendingNews: () => useTrendingNews
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/lib/cache.ts
32
+ var CACHE_PREFIX = "trending-news-cache:";
33
+ var CACHE_TTL_MS = 10 * 60 * 1e3;
34
+ function getStorage() {
35
+ if (typeof window === "undefined" || !window.localStorage) return null;
36
+ return window.localStorage;
37
+ }
38
+ function readCachedTrendingNews(key) {
39
+ const storage = getStorage();
40
+ if (!storage) return null;
41
+ try {
42
+ const raw = storage.getItem(CACHE_PREFIX + key);
43
+ if (!raw) return null;
44
+ const entry = JSON.parse(raw);
45
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
46
+ storage.removeItem(CACHE_PREFIX + key);
47
+ return null;
48
+ }
49
+ return entry.data;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeCachedTrendingNews(key, data) {
55
+ const storage = getStorage();
56
+ if (!storage) return;
57
+ try {
58
+ const entry = { timestamp: Date.now(), data };
59
+ storage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));
60
+ } catch {
61
+ }
62
+ }
63
+ function clearTrendingNewsCache() {
64
+ const storage = getStorage();
65
+ if (!storage) return;
66
+ for (let i = storage.length - 1; i >= 0; i--) {
67
+ const key = storage.key(i);
68
+ if (key?.startsWith(CACHE_PREFIX)) storage.removeItem(key);
69
+ }
70
+ }
71
+
72
+ // src/api/trending.ts
73
+ function mapArticles(articles = []) {
74
+ return articles.map((a) => ({
75
+ title: a.title ?? "",
76
+ url: a.url,
77
+ source: a.source,
78
+ publishedAt: a.published_at
79
+ }));
80
+ }
81
+ function buildUrl(apiEndpoint, options) {
82
+ const url = new URL(apiEndpoint);
83
+ if (options.topic) url.searchParams.set("topic", options.topic);
84
+ return url.toString();
85
+ }
86
+ async function getTrendingNews(options) {
87
+ if (!options.apiEndpoint) {
88
+ throw new Error("trending-news-api: apiEndpoint is required");
89
+ }
90
+ const url = buildUrl(options.apiEndpoint, options);
91
+ const cached = readCachedTrendingNews(url);
92
+ if (cached) return cached;
93
+ const response = await fetch(url, { headers: { "Content-Type": "application/json" } });
94
+ if (!response.ok) {
95
+ throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);
96
+ }
97
+ const data = await response.json();
98
+ if (data.error) throw new Error(data.error);
99
+ const limit = options.limit ?? 25;
100
+ const result = {
101
+ date: data.date,
102
+ topics: (data.topics ?? []).slice(0, limit).map((t) => ({
103
+ topic: t.topic,
104
+ wikiRank: t.wiki_rank,
105
+ wikiViews: t.wiki_views,
106
+ newsCount: t.news_count,
107
+ articles: mapArticles(t.articles)
108
+ }))
109
+ };
110
+ writeCachedTrendingNews(url, result);
111
+ return result;
112
+ }
113
+ async function getTrendingNewsForTopic(topic, options) {
114
+ if (!options.apiEndpoint) {
115
+ throw new Error("trending-news-api: apiEndpoint is required");
116
+ }
117
+ const url = buildUrl(options.apiEndpoint, { ...options, topic });
118
+ const cached = readCachedTrendingNews(url);
119
+ if (cached) return cached;
120
+ const response = await fetch(url, { headers: { "Content-Type": "application/json" } });
121
+ if (!response.ok) {
122
+ throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);
123
+ }
124
+ const data = await response.json();
125
+ if (data.error) throw new Error(data.error);
126
+ const result = {
127
+ topic: data.topic,
128
+ newsCount: data.news_count,
129
+ articles: mapArticles(data.articles)
130
+ };
131
+ writeCachedTrendingNews(url, result);
132
+ return result;
133
+ }
134
+
135
+ // src/hooks/useTrendingNews.ts
136
+ var import_react = require("react");
137
+ function useTrendingNews(options = {}) {
138
+ const [data, setData] = (0, import_react.useState)(null);
139
+ const [error, setError] = (0, import_react.useState)(null);
140
+ const [loading, setLoading] = (0, import_react.useState)(Boolean(options.apiEndpoint));
141
+ (0, import_react.useEffect)(() => {
142
+ if (!options.apiEndpoint) {
143
+ setLoading(false);
144
+ return;
145
+ }
146
+ let active = true;
147
+ setLoading(true);
148
+ setError(null);
149
+ getTrendingNews(options).then((result) => {
150
+ if (active) setData(result);
151
+ }).catch((err) => {
152
+ if (active) setError(err instanceof Error ? err : new Error("Unknown error"));
153
+ }).finally(() => {
154
+ if (active) setLoading(false);
155
+ });
156
+ return () => {
157
+ active = false;
158
+ };
159
+ }, [options.apiEndpoint, options.topic, options.limit]);
160
+ return { data, error, loading };
161
+ }
162
+
163
+ // src/components/TrendingNews.tsx
164
+ var import_jsx_runtime = require("react/jsx-runtime");
165
+ var styles = {
166
+ root: {
167
+ fontFamily: "system-ui, sans-serif",
168
+ border: "1px solid #e5e7eb",
169
+ borderRadius: 12,
170
+ padding: 16,
171
+ background: "#fff",
172
+ color: "#111827",
173
+ maxWidth: 640
174
+ },
175
+ compactRoot: {
176
+ fontFamily: "system-ui, sans-serif",
177
+ display: "flex",
178
+ flexDirection: "column",
179
+ gap: 6,
180
+ padding: "10px 14px",
181
+ borderRadius: 8
182
+ },
183
+ compactHeader: {
184
+ fontSize: 11,
185
+ fontWeight: 600,
186
+ opacity: 0.75,
187
+ letterSpacing: 0.3,
188
+ textTransform: "uppercase"
189
+ },
190
+ topicRow: { display: "flex", gap: 10, overflowX: "auto", paddingBottom: 2 },
191
+ topicCard: {
192
+ minWidth: 160,
193
+ maxWidth: 200,
194
+ padding: "8px 10px",
195
+ borderRadius: 8,
196
+ background: "rgba(255,255,255,0.06)",
197
+ display: "flex",
198
+ flexDirection: "column",
199
+ gap: 4,
200
+ textDecoration: "none",
201
+ color: "inherit"
202
+ },
203
+ topicName: {
204
+ fontSize: 12,
205
+ fontWeight: 600,
206
+ whiteSpace: "nowrap",
207
+ overflow: "hidden",
208
+ textOverflow: "ellipsis"
209
+ },
210
+ headline: {
211
+ fontSize: 11,
212
+ opacity: 0.8,
213
+ display: "-webkit-box",
214
+ WebkitLineClamp: 2,
215
+ WebkitBoxOrient: "vertical",
216
+ overflow: "hidden"
217
+ },
218
+ count: { fontSize: 10, opacity: 0.6 },
219
+ list: { display: "flex", flexDirection: "column", gap: 16 },
220
+ fullTopic: { display: "flex", flexDirection: "column", gap: 6 },
221
+ fullTopicHeader: { display: "flex", alignItems: "baseline", gap: 8 },
222
+ article: { fontSize: 13 },
223
+ articleLink: { color: "inherit", textDecoration: "none" }
224
+ };
225
+ function TrendingNews(props) {
226
+ const { className, style, compact, maxTopics = 8, ...options } = props;
227
+ const { data, loading, error } = useTrendingNews(options);
228
+ if (!options.apiEndpoint) return null;
229
+ if (loading && !data) return null;
230
+ if (error) return null;
231
+ if (!data || data.topics.length === 0) return null;
232
+ const topics = data.topics.slice(0, maxTopics);
233
+ if (compact) {
234
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: { ...styles.compactRoot, ...style }, children: [
235
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.compactHeader, children: "Trending" }),
236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.topicRow, children: topics.map((topic) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
237
+ "a",
238
+ {
239
+ href: topic.articles[0]?.url,
240
+ target: "_blank",
241
+ rel: "noreferrer",
242
+ style: styles.topicCard,
243
+ children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.topicName, children: topic.topic }),
245
+ topic.articles[0] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.headline, children: topic.articles[0].title }),
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.count, children: [
247
+ topic.newsCount,
248
+ " article",
249
+ topic.newsCount === 1 ? "" : "s"
250
+ ] })
251
+ ]
252
+ },
253
+ topic.topic
254
+ )) })
255
+ ] });
256
+ }
257
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: { ...styles.root, ...styles.list, ...style }, children: topics.map((topic) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.fullTopic, children: [
258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.fullTopicHeader, children: [
259
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: topic.topic }),
260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: styles.count, children: [
261
+ topic.newsCount,
262
+ " article",
263
+ topic.newsCount === 1 ? "" : "s"
264
+ ] })
265
+ ] }),
266
+ topic.articles.slice(0, 5).map((article, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.article, children: [
267
+ article.url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: article.url, target: "_blank", rel: "noreferrer", style: styles.articleLink, children: article.title }) : article.title,
268
+ article.source && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { opacity: 0.6 }, children: [
269
+ " \xB7 ",
270
+ article.source
271
+ ] })
272
+ ] }, article.url ?? i))
273
+ ] }, topic.topic)) });
274
+ }
275
+ // Annotate the CommonJS export names for ESM import in node:
276
+ 0 && (module.exports = {
277
+ TrendingNews,
278
+ clearTrendingNewsCache,
279
+ getTrendingNews,
280
+ getTrendingNewsForTopic,
281
+ useTrendingNews
282
+ });
283
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/lib/cache.ts","../src/api/trending.ts","../src/hooks/useTrendingNews.ts","../src/components/TrendingNews.tsx"],"sourcesContent":["export * from './types';\nexport { clearTrendingNewsCache } from './lib/cache';\nexport * from './api/trending';\nexport * from './hooks/useTrendingNews';\nexport * from './components/TrendingNews';\n","const CACHE_PREFIX = 'trending-news-cache:';\nconst CACHE_TTL_MS = 10 * 60 * 1000;\n\ntype CacheEntry<T> = {\n timestamp: number;\n data: T;\n};\n\nfunction getStorage(): Storage | null {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n return window.localStorage;\n}\n\n/** Returns the cached value for `key` if it was written within the last 10 minutes. */\nexport function readCachedTrendingNews<T>(key: string): T | null {\n const storage = getStorage();\n if (!storage) return null;\n\n try {\n const raw = storage.getItem(CACHE_PREFIX + key);\n if (!raw) return null;\n\n const entry = JSON.parse(raw) as CacheEntry<T>;\n if (Date.now() - entry.timestamp > CACHE_TTL_MS) {\n storage.removeItem(CACHE_PREFIX + key);\n return null;\n }\n\n return entry.data;\n } catch {\n return null;\n }\n}\n\nexport function writeCachedTrendingNews<T>(key: string, data: T): void {\n const storage = getStorage();\n if (!storage) return;\n\n try {\n const entry: CacheEntry<T> = { timestamp: Date.now(), data };\n storage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));\n } catch {\n // Storage full or unavailable (e.g. private browsing) — safe to ignore,\n // the next call will simply hit the network again.\n }\n}\n\n/** Clears all cached trending news responses. Mainly useful for tests/debugging. */\nexport function clearTrendingNewsCache(): void {\n const storage = getStorage();\n if (!storage) return;\n\n for (let i = storage.length - 1; i >= 0; i--) {\n const key = storage.key(i);\n if (key?.startsWith(CACHE_PREFIX)) storage.removeItem(key);\n }\n}\n","import type { NewsArticle, TrendingNewsData, TrendingNewsOptions, TrendingNewsTopicData } from '../types';\nimport { readCachedTrendingNews, writeCachedTrendingNews } from '../lib/cache';\n\ntype WorkerArticle = {\n title?: string;\n url?: string;\n source?: string;\n published_at?: string;\n};\n\ntype WorkerTopic = {\n topic: string;\n wiki_rank?: number;\n wiki_views?: number;\n news_count: number;\n articles: WorkerArticle[];\n};\n\ntype WorkerTopicsResponse = {\n date?: string;\n topics: WorkerTopic[];\n error?: string;\n};\n\ntype WorkerTopicResponse = {\n topic: string;\n news_count: number;\n articles: WorkerArticle[];\n error?: string;\n};\n\nfunction mapArticles(articles: WorkerArticle[] = []): NewsArticle[] {\n return articles.map((a) => ({\n title: a.title ?? '',\n url: a.url,\n source: a.source,\n publishedAt: a.published_at,\n }));\n}\n\nfunction buildUrl(apiEndpoint: string, options: TrendingNewsOptions) {\n const url = new URL(apiEndpoint);\n if (options.topic) url.searchParams.set('topic', options.topic);\n return url.toString();\n}\n\n/**\n * Fetches trending topics (or, when `options.topic` is set, news for a\n * single topic) from a deployed instance of `worker/index.ts`.\n */\nexport async function getTrendingNews(options: TrendingNewsOptions): Promise<TrendingNewsData> {\n if (!options.apiEndpoint) {\n throw new Error('trending-news-api: apiEndpoint is required');\n }\n\n const url = buildUrl(options.apiEndpoint, options);\n\n const cached = readCachedTrendingNews<TrendingNewsData>(url);\n if (cached) return cached;\n\n const response = await fetch(url, { headers: { 'Content-Type': 'application/json' } });\n if (!response.ok) {\n throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as WorkerTopicsResponse;\n if (data.error) throw new Error(data.error);\n\n const limit = options.limit ?? 25;\n const result: TrendingNewsData = {\n date: data.date,\n topics: (data.topics ?? []).slice(0, limit).map((t) => ({\n topic: t.topic,\n wikiRank: t.wiki_rank,\n wikiViews: t.wiki_views,\n newsCount: t.news_count,\n articles: mapArticles(t.articles),\n })),\n };\n\n writeCachedTrendingNews(url, result);\n return result;\n}\n\n/** Fetches news articles for a single topic. */\nexport async function getTrendingNewsForTopic(\n topic: string,\n options: Omit<TrendingNewsOptions, 'topic'>\n): Promise<TrendingNewsTopicData> {\n if (!options.apiEndpoint) {\n throw new Error('trending-news-api: apiEndpoint is required');\n }\n\n const url = buildUrl(options.apiEndpoint, { ...options, topic });\n\n const cached = readCachedTrendingNews<TrendingNewsTopicData>(url);\n if (cached) return cached;\n\n const response = await fetch(url, { headers: { 'Content-Type': 'application/json' } });\n if (!response.ok) {\n throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as WorkerTopicResponse;\n if (data.error) throw new Error(data.error);\n\n const result: TrendingNewsTopicData = {\n topic: data.topic,\n newsCount: data.news_count,\n articles: mapArticles(data.articles),\n };\n\n writeCachedTrendingNews(url, result);\n return result;\n}\n","import { useEffect, useState } from 'react';\nimport type { TrendingNewsData, TrendingNewsOptions } from '../types';\nimport { getTrendingNews } from '../api/trending';\n\nexport function useTrendingNews(options: TrendingNewsOptions = {}) {\n const [data, setData] = useState<TrendingNewsData | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(Boolean(options.apiEndpoint));\n\n useEffect(() => {\n if (!options.apiEndpoint) {\n setLoading(false);\n return;\n }\n\n let active = true;\n setLoading(true);\n setError(null);\n\n getTrendingNews(options)\n .then((result) => { if (active) setData(result); })\n .catch((err) => { if (active) setError(err instanceof Error ? err : new Error('Unknown error')); })\n .finally(() => { if (active) setLoading(false); });\n\n return () => { active = false; };\n }, [options.apiEndpoint, options.topic, options.limit]);\n\n return { data, error, loading };\n}\n","import React from 'react';\nimport { useTrendingNews } from '../hooks/useTrendingNews';\nimport type { TrendingNewsOptions } from '../types';\n\ntype Props = TrendingNewsOptions & {\n className?: string;\n style?: React.CSSProperties;\n compact?: boolean;\n /** Max trending topics to render (default 8). */\n maxTopics?: number;\n};\n\nconst styles = {\n root: {\n fontFamily: 'system-ui, sans-serif',\n border: '1px solid #e5e7eb',\n borderRadius: 12,\n padding: 16,\n background: '#fff',\n color: '#111827',\n maxWidth: 640,\n } as React.CSSProperties,\n compactRoot: {\n fontFamily: 'system-ui, sans-serif',\n display: 'flex',\n flexDirection: 'column',\n gap: 6,\n padding: '10px 14px',\n borderRadius: 8,\n } as React.CSSProperties,\n compactHeader: {\n fontSize: 11,\n fontWeight: 600,\n opacity: 0.75,\n letterSpacing: 0.3,\n textTransform: 'uppercase',\n } as React.CSSProperties,\n topicRow: { display: 'flex', gap: 10, overflowX: 'auto', paddingBottom: 2 } as React.CSSProperties,\n topicCard: {\n minWidth: 160,\n maxWidth: 200,\n padding: '8px 10px',\n borderRadius: 8,\n background: 'rgba(255,255,255,0.06)',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n textDecoration: 'none',\n color: 'inherit',\n } as React.CSSProperties,\n topicName: {\n fontSize: 12,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n } as React.CSSProperties,\n headline: {\n fontSize: 11,\n opacity: 0.8,\n display: '-webkit-box',\n WebkitLineClamp: 2,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden',\n } as React.CSSProperties,\n count: { fontSize: 10, opacity: 0.6 } as React.CSSProperties,\n list: { display: 'flex', flexDirection: 'column', gap: 16 } as React.CSSProperties,\n fullTopic: { display: 'flex', flexDirection: 'column', gap: 6 } as React.CSSProperties,\n fullTopicHeader: { display: 'flex', alignItems: 'baseline', gap: 8 } as React.CSSProperties,\n article: { fontSize: 13 } as React.CSSProperties,\n articleLink: { color: 'inherit', textDecoration: 'none' } as React.CSSProperties,\n};\n\n/**\n * Trending news widget. Requires `apiEndpoint` pointing at a deployed\n * instance of the bundled Cloudflare Worker (`worker/index.ts`) — renders\n * nothing when it's not configured, still loading, or errored, so it never\n * disrupts the layout it's dropped into.\n */\nexport function TrendingNews(props: Props) {\n const { className, style, compact, maxTopics = 8, ...options } = props;\n const { data, loading, error } = useTrendingNews(options);\n\n if (!options.apiEndpoint) return null;\n if (loading && !data) return null;\n if (error) return null;\n if (!data || data.topics.length === 0) return null;\n\n const topics = data.topics.slice(0, maxTopics);\n\n if (compact) {\n return (\n <div className={className} style={{ ...styles.compactRoot, ...style }}>\n <div style={styles.compactHeader}>Trending</div>\n <div style={styles.topicRow}>\n {topics.map((topic) => (\n <a\n key={topic.topic}\n href={topic.articles[0]?.url}\n target=\"_blank\"\n rel=\"noreferrer\"\n style={styles.topicCard}\n >\n <div style={styles.topicName}>{topic.topic}</div>\n {topic.articles[0] && <div style={styles.headline}>{topic.articles[0].title}</div>}\n <div style={styles.count}>\n {topic.newsCount} article{topic.newsCount === 1 ? '' : 's'}\n </div>\n </a>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className={className} style={{ ...styles.root, ...styles.list, ...style }}>\n {topics.map((topic) => (\n <div key={topic.topic} style={styles.fullTopic}>\n <div style={styles.fullTopicHeader}>\n <strong>{topic.topic}</strong>\n <span style={styles.count}>\n {topic.newsCount} article{topic.newsCount === 1 ? '' : 's'}\n </span>\n </div>\n {topic.articles.slice(0, 5).map((article, i) => (\n <div key={article.url ?? i} style={styles.article}>\n {article.url ? (\n <a href={article.url} target=\"_blank\" rel=\"noreferrer\" style={styles.articleLink}>\n {article.title}\n </a>\n ) : (\n article.title\n )}\n {article.source && <span style={{ opacity: 0.6 }}> &middot; {article.source}</span>}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+
3
+ type NewsArticle = {
4
+ title: string;
5
+ url?: string;
6
+ source?: string;
7
+ publishedAt?: string;
8
+ };
9
+ type TrendingTopic = {
10
+ topic: string;
11
+ wikiRank?: number;
12
+ wikiViews?: number;
13
+ newsCount: number;
14
+ articles: NewsArticle[];
15
+ };
16
+ type TrendingNewsData = {
17
+ date?: string;
18
+ topics: TrendingTopic[];
19
+ };
20
+ type TrendingNewsTopicData = {
21
+ topic: string;
22
+ newsCount: number;
23
+ articles: NewsArticle[];
24
+ };
25
+ type TrendingNewsOptions = {
26
+ /** URL of a deployed instance of the bundled Cloudflare Worker (`worker/index.ts`). */
27
+ apiEndpoint?: string;
28
+ /** When set, fetches news for this single topic instead of the daily trending list. */
29
+ topic?: string;
30
+ /** Max trending topics to request from the worker (default 25). */
31
+ limit?: number;
32
+ };
33
+
34
+ /** Clears all cached trending news responses. Mainly useful for tests/debugging. */
35
+ declare function clearTrendingNewsCache(): void;
36
+
37
+ /**
38
+ * Fetches trending topics (or, when `options.topic` is set, news for a
39
+ * single topic) from a deployed instance of `worker/index.ts`.
40
+ */
41
+ declare function getTrendingNews(options: TrendingNewsOptions): Promise<TrendingNewsData>;
42
+ /** Fetches news articles for a single topic. */
43
+ declare function getTrendingNewsForTopic(topic: string, options: Omit<TrendingNewsOptions, 'topic'>): Promise<TrendingNewsTopicData>;
44
+
45
+ declare function useTrendingNews(options?: TrendingNewsOptions): {
46
+ data: TrendingNewsData | null;
47
+ error: Error | null;
48
+ loading: boolean;
49
+ };
50
+
51
+ type Props = TrendingNewsOptions & {
52
+ className?: string;
53
+ style?: React.CSSProperties;
54
+ compact?: boolean;
55
+ /** Max trending topics to render (default 8). */
56
+ maxTopics?: number;
57
+ };
58
+ /**
59
+ * Trending news widget. Requires `apiEndpoint` pointing at a deployed
60
+ * instance of the bundled Cloudflare Worker (`worker/index.ts`) — renders
61
+ * nothing when it's not configured, still loading, or errored, so it never
62
+ * disrupts the layout it's dropped into.
63
+ */
64
+ declare function TrendingNews(props: Props): React.JSX.Element | null;
65
+
66
+ export { type NewsArticle, TrendingNews, type TrendingNewsData, type TrendingNewsOptions, type TrendingNewsTopicData, type TrendingTopic, clearTrendingNewsCache, getTrendingNews, getTrendingNewsForTopic, useTrendingNews };
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+
3
+ type NewsArticle = {
4
+ title: string;
5
+ url?: string;
6
+ source?: string;
7
+ publishedAt?: string;
8
+ };
9
+ type TrendingTopic = {
10
+ topic: string;
11
+ wikiRank?: number;
12
+ wikiViews?: number;
13
+ newsCount: number;
14
+ articles: NewsArticle[];
15
+ };
16
+ type TrendingNewsData = {
17
+ date?: string;
18
+ topics: TrendingTopic[];
19
+ };
20
+ type TrendingNewsTopicData = {
21
+ topic: string;
22
+ newsCount: number;
23
+ articles: NewsArticle[];
24
+ };
25
+ type TrendingNewsOptions = {
26
+ /** URL of a deployed instance of the bundled Cloudflare Worker (`worker/index.ts`). */
27
+ apiEndpoint?: string;
28
+ /** When set, fetches news for this single topic instead of the daily trending list. */
29
+ topic?: string;
30
+ /** Max trending topics to request from the worker (default 25). */
31
+ limit?: number;
32
+ };
33
+
34
+ /** Clears all cached trending news responses. Mainly useful for tests/debugging. */
35
+ declare function clearTrendingNewsCache(): void;
36
+
37
+ /**
38
+ * Fetches trending topics (or, when `options.topic` is set, news for a
39
+ * single topic) from a deployed instance of `worker/index.ts`.
40
+ */
41
+ declare function getTrendingNews(options: TrendingNewsOptions): Promise<TrendingNewsData>;
42
+ /** Fetches news articles for a single topic. */
43
+ declare function getTrendingNewsForTopic(topic: string, options: Omit<TrendingNewsOptions, 'topic'>): Promise<TrendingNewsTopicData>;
44
+
45
+ declare function useTrendingNews(options?: TrendingNewsOptions): {
46
+ data: TrendingNewsData | null;
47
+ error: Error | null;
48
+ loading: boolean;
49
+ };
50
+
51
+ type Props = TrendingNewsOptions & {
52
+ className?: string;
53
+ style?: React.CSSProperties;
54
+ compact?: boolean;
55
+ /** Max trending topics to render (default 8). */
56
+ maxTopics?: number;
57
+ };
58
+ /**
59
+ * Trending news widget. Requires `apiEndpoint` pointing at a deployed
60
+ * instance of the bundled Cloudflare Worker (`worker/index.ts`) — renders
61
+ * nothing when it's not configured, still loading, or errored, so it never
62
+ * disrupts the layout it's dropped into.
63
+ */
64
+ declare function TrendingNews(props: Props): React.JSX.Element | null;
65
+
66
+ export { type NewsArticle, TrendingNews, type TrendingNewsData, type TrendingNewsOptions, type TrendingNewsTopicData, type TrendingTopic, clearTrendingNewsCache, getTrendingNews, getTrendingNewsForTopic, useTrendingNews };
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ // src/lib/cache.ts
2
+ var CACHE_PREFIX = "trending-news-cache:";
3
+ var CACHE_TTL_MS = 10 * 60 * 1e3;
4
+ function getStorage() {
5
+ if (typeof window === "undefined" || !window.localStorage) return null;
6
+ return window.localStorage;
7
+ }
8
+ function readCachedTrendingNews(key) {
9
+ const storage = getStorage();
10
+ if (!storage) return null;
11
+ try {
12
+ const raw = storage.getItem(CACHE_PREFIX + key);
13
+ if (!raw) return null;
14
+ const entry = JSON.parse(raw);
15
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
16
+ storage.removeItem(CACHE_PREFIX + key);
17
+ return null;
18
+ }
19
+ return entry.data;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ function writeCachedTrendingNews(key, data) {
25
+ const storage = getStorage();
26
+ if (!storage) return;
27
+ try {
28
+ const entry = { timestamp: Date.now(), data };
29
+ storage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));
30
+ } catch {
31
+ }
32
+ }
33
+ function clearTrendingNewsCache() {
34
+ const storage = getStorage();
35
+ if (!storage) return;
36
+ for (let i = storage.length - 1; i >= 0; i--) {
37
+ const key = storage.key(i);
38
+ if (key?.startsWith(CACHE_PREFIX)) storage.removeItem(key);
39
+ }
40
+ }
41
+
42
+ // src/api/trending.ts
43
+ function mapArticles(articles = []) {
44
+ return articles.map((a) => ({
45
+ title: a.title ?? "",
46
+ url: a.url,
47
+ source: a.source,
48
+ publishedAt: a.published_at
49
+ }));
50
+ }
51
+ function buildUrl(apiEndpoint, options) {
52
+ const url = new URL(apiEndpoint);
53
+ if (options.topic) url.searchParams.set("topic", options.topic);
54
+ return url.toString();
55
+ }
56
+ async function getTrendingNews(options) {
57
+ if (!options.apiEndpoint) {
58
+ throw new Error("trending-news-api: apiEndpoint is required");
59
+ }
60
+ const url = buildUrl(options.apiEndpoint, options);
61
+ const cached = readCachedTrendingNews(url);
62
+ if (cached) return cached;
63
+ const response = await fetch(url, { headers: { "Content-Type": "application/json" } });
64
+ if (!response.ok) {
65
+ throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);
66
+ }
67
+ const data = await response.json();
68
+ if (data.error) throw new Error(data.error);
69
+ const limit = options.limit ?? 25;
70
+ const result = {
71
+ date: data.date,
72
+ topics: (data.topics ?? []).slice(0, limit).map((t) => ({
73
+ topic: t.topic,
74
+ wikiRank: t.wiki_rank,
75
+ wikiViews: t.wiki_views,
76
+ newsCount: t.news_count,
77
+ articles: mapArticles(t.articles)
78
+ }))
79
+ };
80
+ writeCachedTrendingNews(url, result);
81
+ return result;
82
+ }
83
+ async function getTrendingNewsForTopic(topic, options) {
84
+ if (!options.apiEndpoint) {
85
+ throw new Error("trending-news-api: apiEndpoint is required");
86
+ }
87
+ const url = buildUrl(options.apiEndpoint, { ...options, topic });
88
+ const cached = readCachedTrendingNews(url);
89
+ if (cached) return cached;
90
+ const response = await fetch(url, { headers: { "Content-Type": "application/json" } });
91
+ if (!response.ok) {
92
+ throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);
93
+ }
94
+ const data = await response.json();
95
+ if (data.error) throw new Error(data.error);
96
+ const result = {
97
+ topic: data.topic,
98
+ newsCount: data.news_count,
99
+ articles: mapArticles(data.articles)
100
+ };
101
+ writeCachedTrendingNews(url, result);
102
+ return result;
103
+ }
104
+
105
+ // src/hooks/useTrendingNews.ts
106
+ import { useEffect, useState } from "react";
107
+ function useTrendingNews(options = {}) {
108
+ const [data, setData] = useState(null);
109
+ const [error, setError] = useState(null);
110
+ const [loading, setLoading] = useState(Boolean(options.apiEndpoint));
111
+ useEffect(() => {
112
+ if (!options.apiEndpoint) {
113
+ setLoading(false);
114
+ return;
115
+ }
116
+ let active = true;
117
+ setLoading(true);
118
+ setError(null);
119
+ getTrendingNews(options).then((result) => {
120
+ if (active) setData(result);
121
+ }).catch((err) => {
122
+ if (active) setError(err instanceof Error ? err : new Error("Unknown error"));
123
+ }).finally(() => {
124
+ if (active) setLoading(false);
125
+ });
126
+ return () => {
127
+ active = false;
128
+ };
129
+ }, [options.apiEndpoint, options.topic, options.limit]);
130
+ return { data, error, loading };
131
+ }
132
+
133
+ // src/components/TrendingNews.tsx
134
+ import { jsx, jsxs } from "react/jsx-runtime";
135
+ var styles = {
136
+ root: {
137
+ fontFamily: "system-ui, sans-serif",
138
+ border: "1px solid #e5e7eb",
139
+ borderRadius: 12,
140
+ padding: 16,
141
+ background: "#fff",
142
+ color: "#111827",
143
+ maxWidth: 640
144
+ },
145
+ compactRoot: {
146
+ fontFamily: "system-ui, sans-serif",
147
+ display: "flex",
148
+ flexDirection: "column",
149
+ gap: 6,
150
+ padding: "10px 14px",
151
+ borderRadius: 8
152
+ },
153
+ compactHeader: {
154
+ fontSize: 11,
155
+ fontWeight: 600,
156
+ opacity: 0.75,
157
+ letterSpacing: 0.3,
158
+ textTransform: "uppercase"
159
+ },
160
+ topicRow: { display: "flex", gap: 10, overflowX: "auto", paddingBottom: 2 },
161
+ topicCard: {
162
+ minWidth: 160,
163
+ maxWidth: 200,
164
+ padding: "8px 10px",
165
+ borderRadius: 8,
166
+ background: "rgba(255,255,255,0.06)",
167
+ display: "flex",
168
+ flexDirection: "column",
169
+ gap: 4,
170
+ textDecoration: "none",
171
+ color: "inherit"
172
+ },
173
+ topicName: {
174
+ fontSize: 12,
175
+ fontWeight: 600,
176
+ whiteSpace: "nowrap",
177
+ overflow: "hidden",
178
+ textOverflow: "ellipsis"
179
+ },
180
+ headline: {
181
+ fontSize: 11,
182
+ opacity: 0.8,
183
+ display: "-webkit-box",
184
+ WebkitLineClamp: 2,
185
+ WebkitBoxOrient: "vertical",
186
+ overflow: "hidden"
187
+ },
188
+ count: { fontSize: 10, opacity: 0.6 },
189
+ list: { display: "flex", flexDirection: "column", gap: 16 },
190
+ fullTopic: { display: "flex", flexDirection: "column", gap: 6 },
191
+ fullTopicHeader: { display: "flex", alignItems: "baseline", gap: 8 },
192
+ article: { fontSize: 13 },
193
+ articleLink: { color: "inherit", textDecoration: "none" }
194
+ };
195
+ function TrendingNews(props) {
196
+ const { className, style, compact, maxTopics = 8, ...options } = props;
197
+ const { data, loading, error } = useTrendingNews(options);
198
+ if (!options.apiEndpoint) return null;
199
+ if (loading && !data) return null;
200
+ if (error) return null;
201
+ if (!data || data.topics.length === 0) return null;
202
+ const topics = data.topics.slice(0, maxTopics);
203
+ if (compact) {
204
+ return /* @__PURE__ */ jsxs("div", { className, style: { ...styles.compactRoot, ...style }, children: [
205
+ /* @__PURE__ */ jsx("div", { style: styles.compactHeader, children: "Trending" }),
206
+ /* @__PURE__ */ jsx("div", { style: styles.topicRow, children: topics.map((topic) => /* @__PURE__ */ jsxs(
207
+ "a",
208
+ {
209
+ href: topic.articles[0]?.url,
210
+ target: "_blank",
211
+ rel: "noreferrer",
212
+ style: styles.topicCard,
213
+ children: [
214
+ /* @__PURE__ */ jsx("div", { style: styles.topicName, children: topic.topic }),
215
+ topic.articles[0] && /* @__PURE__ */ jsx("div", { style: styles.headline, children: topic.articles[0].title }),
216
+ /* @__PURE__ */ jsxs("div", { style: styles.count, children: [
217
+ topic.newsCount,
218
+ " article",
219
+ topic.newsCount === 1 ? "" : "s"
220
+ ] })
221
+ ]
222
+ },
223
+ topic.topic
224
+ )) })
225
+ ] });
226
+ }
227
+ return /* @__PURE__ */ jsx("div", { className, style: { ...styles.root, ...styles.list, ...style }, children: topics.map((topic) => /* @__PURE__ */ jsxs("div", { style: styles.fullTopic, children: [
228
+ /* @__PURE__ */ jsxs("div", { style: styles.fullTopicHeader, children: [
229
+ /* @__PURE__ */ jsx("strong", { children: topic.topic }),
230
+ /* @__PURE__ */ jsxs("span", { style: styles.count, children: [
231
+ topic.newsCount,
232
+ " article",
233
+ topic.newsCount === 1 ? "" : "s"
234
+ ] })
235
+ ] }),
236
+ topic.articles.slice(0, 5).map((article, i) => /* @__PURE__ */ jsxs("div", { style: styles.article, children: [
237
+ article.url ? /* @__PURE__ */ jsx("a", { href: article.url, target: "_blank", rel: "noreferrer", style: styles.articleLink, children: article.title }) : article.title,
238
+ article.source && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.6 }, children: [
239
+ " \xB7 ",
240
+ article.source
241
+ ] })
242
+ ] }, article.url ?? i))
243
+ ] }, topic.topic)) });
244
+ }
245
+ export {
246
+ TrendingNews,
247
+ clearTrendingNewsCache,
248
+ getTrendingNews,
249
+ getTrendingNewsForTopic,
250
+ useTrendingNews
251
+ };
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/cache.ts","../src/api/trending.ts","../src/hooks/useTrendingNews.ts","../src/components/TrendingNews.tsx"],"sourcesContent":["const CACHE_PREFIX = 'trending-news-cache:';\nconst CACHE_TTL_MS = 10 * 60 * 1000;\n\ntype CacheEntry<T> = {\n timestamp: number;\n data: T;\n};\n\nfunction getStorage(): Storage | null {\n if (typeof window === 'undefined' || !window.localStorage) return null;\n return window.localStorage;\n}\n\n/** Returns the cached value for `key` if it was written within the last 10 minutes. */\nexport function readCachedTrendingNews<T>(key: string): T | null {\n const storage = getStorage();\n if (!storage) return null;\n\n try {\n const raw = storage.getItem(CACHE_PREFIX + key);\n if (!raw) return null;\n\n const entry = JSON.parse(raw) as CacheEntry<T>;\n if (Date.now() - entry.timestamp > CACHE_TTL_MS) {\n storage.removeItem(CACHE_PREFIX + key);\n return null;\n }\n\n return entry.data;\n } catch {\n return null;\n }\n}\n\nexport function writeCachedTrendingNews<T>(key: string, data: T): void {\n const storage = getStorage();\n if (!storage) return;\n\n try {\n const entry: CacheEntry<T> = { timestamp: Date.now(), data };\n storage.setItem(CACHE_PREFIX + key, JSON.stringify(entry));\n } catch {\n // Storage full or unavailable (e.g. private browsing) — safe to ignore,\n // the next call will simply hit the network again.\n }\n}\n\n/** Clears all cached trending news responses. Mainly useful for tests/debugging. */\nexport function clearTrendingNewsCache(): void {\n const storage = getStorage();\n if (!storage) return;\n\n for (let i = storage.length - 1; i >= 0; i--) {\n const key = storage.key(i);\n if (key?.startsWith(CACHE_PREFIX)) storage.removeItem(key);\n }\n}\n","import type { NewsArticle, TrendingNewsData, TrendingNewsOptions, TrendingNewsTopicData } from '../types';\nimport { readCachedTrendingNews, writeCachedTrendingNews } from '../lib/cache';\n\ntype WorkerArticle = {\n title?: string;\n url?: string;\n source?: string;\n published_at?: string;\n};\n\ntype WorkerTopic = {\n topic: string;\n wiki_rank?: number;\n wiki_views?: number;\n news_count: number;\n articles: WorkerArticle[];\n};\n\ntype WorkerTopicsResponse = {\n date?: string;\n topics: WorkerTopic[];\n error?: string;\n};\n\ntype WorkerTopicResponse = {\n topic: string;\n news_count: number;\n articles: WorkerArticle[];\n error?: string;\n};\n\nfunction mapArticles(articles: WorkerArticle[] = []): NewsArticle[] {\n return articles.map((a) => ({\n title: a.title ?? '',\n url: a.url,\n source: a.source,\n publishedAt: a.published_at,\n }));\n}\n\nfunction buildUrl(apiEndpoint: string, options: TrendingNewsOptions) {\n const url = new URL(apiEndpoint);\n if (options.topic) url.searchParams.set('topic', options.topic);\n return url.toString();\n}\n\n/**\n * Fetches trending topics (or, when `options.topic` is set, news for a\n * single topic) from a deployed instance of `worker/index.ts`.\n */\nexport async function getTrendingNews(options: TrendingNewsOptions): Promise<TrendingNewsData> {\n if (!options.apiEndpoint) {\n throw new Error('trending-news-api: apiEndpoint is required');\n }\n\n const url = buildUrl(options.apiEndpoint, options);\n\n const cached = readCachedTrendingNews<TrendingNewsData>(url);\n if (cached) return cached;\n\n const response = await fetch(url, { headers: { 'Content-Type': 'application/json' } });\n if (!response.ok) {\n throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as WorkerTopicsResponse;\n if (data.error) throw new Error(data.error);\n\n const limit = options.limit ?? 25;\n const result: TrendingNewsData = {\n date: data.date,\n topics: (data.topics ?? []).slice(0, limit).map((t) => ({\n topic: t.topic,\n wikiRank: t.wiki_rank,\n wikiViews: t.wiki_views,\n newsCount: t.news_count,\n articles: mapArticles(t.articles),\n })),\n };\n\n writeCachedTrendingNews(url, result);\n return result;\n}\n\n/** Fetches news articles for a single topic. */\nexport async function getTrendingNewsForTopic(\n topic: string,\n options: Omit<TrendingNewsOptions, 'topic'>\n): Promise<TrendingNewsTopicData> {\n if (!options.apiEndpoint) {\n throw new Error('trending-news-api: apiEndpoint is required');\n }\n\n const url = buildUrl(options.apiEndpoint, { ...options, topic });\n\n const cached = readCachedTrendingNews<TrendingNewsTopicData>(url);\n if (cached) return cached;\n\n const response = await fetch(url, { headers: { 'Content-Type': 'application/json' } });\n if (!response.ok) {\n throw new Error(`Trending news request failed: ${response.status} ${response.statusText}`);\n }\n\n const data = (await response.json()) as WorkerTopicResponse;\n if (data.error) throw new Error(data.error);\n\n const result: TrendingNewsTopicData = {\n topic: data.topic,\n newsCount: data.news_count,\n articles: mapArticles(data.articles),\n };\n\n writeCachedTrendingNews(url, result);\n return result;\n}\n","import { useEffect, useState } from 'react';\nimport type { TrendingNewsData, TrendingNewsOptions } from '../types';\nimport { getTrendingNews } from '../api/trending';\n\nexport function useTrendingNews(options: TrendingNewsOptions = {}) {\n const [data, setData] = useState<TrendingNewsData | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(Boolean(options.apiEndpoint));\n\n useEffect(() => {\n if (!options.apiEndpoint) {\n setLoading(false);\n return;\n }\n\n let active = true;\n setLoading(true);\n setError(null);\n\n getTrendingNews(options)\n .then((result) => { if (active) setData(result); })\n .catch((err) => { if (active) setError(err instanceof Error ? err : new Error('Unknown error')); })\n .finally(() => { if (active) setLoading(false); });\n\n return () => { active = false; };\n }, [options.apiEndpoint, options.topic, options.limit]);\n\n return { data, error, loading };\n}\n","import React from 'react';\nimport { useTrendingNews } from '../hooks/useTrendingNews';\nimport type { TrendingNewsOptions } from '../types';\n\ntype Props = TrendingNewsOptions & {\n className?: string;\n style?: React.CSSProperties;\n compact?: boolean;\n /** Max trending topics to render (default 8). */\n maxTopics?: number;\n};\n\nconst styles = {\n root: {\n fontFamily: 'system-ui, sans-serif',\n border: '1px solid #e5e7eb',\n borderRadius: 12,\n padding: 16,\n background: '#fff',\n color: '#111827',\n maxWidth: 640,\n } as React.CSSProperties,\n compactRoot: {\n fontFamily: 'system-ui, sans-serif',\n display: 'flex',\n flexDirection: 'column',\n gap: 6,\n padding: '10px 14px',\n borderRadius: 8,\n } as React.CSSProperties,\n compactHeader: {\n fontSize: 11,\n fontWeight: 600,\n opacity: 0.75,\n letterSpacing: 0.3,\n textTransform: 'uppercase',\n } as React.CSSProperties,\n topicRow: { display: 'flex', gap: 10, overflowX: 'auto', paddingBottom: 2 } as React.CSSProperties,\n topicCard: {\n minWidth: 160,\n maxWidth: 200,\n padding: '8px 10px',\n borderRadius: 8,\n background: 'rgba(255,255,255,0.06)',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n textDecoration: 'none',\n color: 'inherit',\n } as React.CSSProperties,\n topicName: {\n fontSize: 12,\n fontWeight: 600,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n } as React.CSSProperties,\n headline: {\n fontSize: 11,\n opacity: 0.8,\n display: '-webkit-box',\n WebkitLineClamp: 2,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden',\n } as React.CSSProperties,\n count: { fontSize: 10, opacity: 0.6 } as React.CSSProperties,\n list: { display: 'flex', flexDirection: 'column', gap: 16 } as React.CSSProperties,\n fullTopic: { display: 'flex', flexDirection: 'column', gap: 6 } as React.CSSProperties,\n fullTopicHeader: { display: 'flex', alignItems: 'baseline', gap: 8 } as React.CSSProperties,\n article: { fontSize: 13 } as React.CSSProperties,\n articleLink: { color: 'inherit', textDecoration: 'none' } as React.CSSProperties,\n};\n\n/**\n * Trending news widget. Requires `apiEndpoint` pointing at a deployed\n * instance of the bundled Cloudflare Worker (`worker/index.ts`) — renders\n * nothing when it's not configured, still loading, or errored, so it never\n * disrupts the layout it's dropped into.\n */\nexport function TrendingNews(props: Props) {\n const { className, style, compact, maxTopics = 8, ...options } = props;\n const { data, loading, error } = useTrendingNews(options);\n\n if (!options.apiEndpoint) return null;\n if (loading && !data) return null;\n if (error) return null;\n if (!data || data.topics.length === 0) return null;\n\n const topics = data.topics.slice(0, maxTopics);\n\n if (compact) {\n return (\n <div className={className} style={{ ...styles.compactRoot, ...style }}>\n <div style={styles.compactHeader}>Trending</div>\n <div style={styles.topicRow}>\n {topics.map((topic) => (\n <a\n key={topic.topic}\n href={topic.articles[0]?.url}\n target=\"_blank\"\n rel=\"noreferrer\"\n style={styles.topicCard}\n >\n <div style={styles.topicName}>{topic.topic}</div>\n {topic.articles[0] && <div style={styles.headline}>{topic.articles[0].title}</div>}\n <div style={styles.count}>\n {topic.newsCount} article{topic.newsCount === 1 ? '' : 's'}\n </div>\n </a>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className={className} style={{ ...styles.root, ...styles.list, ...style }}>\n {topics.map((topic) => (\n <div key={topic.topic} style={styles.fullTopic}>\n <div style={styles.fullTopicHeader}>\n <strong>{topic.topic}</strong>\n <span style={styles.count}>\n {topic.newsCount} article{topic.newsCount === 1 ? '' : 's'}\n </span>\n </div>\n {topic.articles.slice(0, 5).map((article, i) => (\n <div key={article.url ?? i} style={styles.article}>\n {article.url ? (\n <a href={article.url} target=\"_blank\" rel=\"noreferrer\" style={styles.articleLink}>\n {article.title}\n </a>\n ) : (\n article.title\n )}\n {article.source && <span style={{ opacity: 0.6 }}> &middot; {article.source}</span>}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "trending-news-api",
3
+ "version": "0.1.0",
4
+ "description": "React trending news widget backed by Wikipedia pageviews and The News API, via a Cloudflare Worker proxy",
5
+ "keywords": [
6
+ "react",
7
+ "news",
8
+ "trending",
9
+ "wikipedia",
10
+ "thenewsapi",
11
+ "cloudflare",
12
+ "component-library"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/OpenSourceAGI/qwksearch-research-agent.git",
17
+ "directory": "packages/trending-news-api"
18
+ },
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "sideEffects": false,
35
+ "peerDependencies": {
36
+ "react": "^18 || ^19",
37
+ "react-dom": "^18 || ^19"
38
+ },
39
+ "devDependencies": {
40
+ "@cloudflare/workers-types": "^4.20250115.0",
41
+ "@testing-library/react": "^16.1.0",
42
+ "@types/react": "^19.0.0",
43
+ "@types/react-dom": "^19.0.0",
44
+ "@vitejs/plugin-react": "^6.0.3",
45
+ "@vitest/coverage-v8": "^4.0.18",
46
+ "jsdom": "^28.1.0",
47
+ "react": "^19.2.7",
48
+ "react-dom": "^19.2.7",
49
+ "tsup": "^8.2.4",
50
+ "typescript": "^5.6.3",
51
+ "vitest": "^4.0.18",
52
+ "wrangler": "^3.88.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "vitest run",
58
+ "test:coverage": "vitest run --coverage",
59
+ "worker:typecheck": "tsc --noEmit -p worker/tsconfig.json",
60
+ "worker:dev": "wrangler dev --config worker/wrangler.jsonc",
61
+ "worker:deploy": "wrangler deploy --config worker/wrangler.jsonc"
62
+ }
63
+ }