trending-news-api 0.1.0 → 0.1.1
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 +3 -0
- package/dist/index.cjs +129 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +129 -41
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# trending-news-api
|
|
2
2
|
|
|
3
|
+
[](https://codecov.io/gh/OpenSourceAGI/qwksearch-research-agent)
|
|
4
|
+
|
|
3
5
|
React trending news widget: daily top Wikipedia pages (via the Wikimedia Pageviews API) matched
|
|
4
6
|
against headlines from [The News API](https://www.thenewsapi.com/), served through a bundled
|
|
5
7
|
Cloudflare Worker so the News API key never reaches the browser.
|
|
@@ -8,6 +10,7 @@ Cloudflare Worker so the News API key never reaches the browser.
|
|
|
8
10
|
|
|
9
11
|
- Daily trending topics, ranked by Wikipedia pageviews.
|
|
10
12
|
- Matching headlines per topic from The News API.
|
|
13
|
+
- Article thumbnail images, shown alongside headlines (toggle off with `showImages={false}`).
|
|
11
14
|
- Single-topic headline lookup.
|
|
12
15
|
- Compact card row or full article-list layouts.
|
|
13
16
|
- `localStorage` response caching (10 minutes).
|
package/dist/index.cjs
CHANGED
|
@@ -75,7 +75,8 @@ function mapArticles(articles = []) {
|
|
|
75
75
|
title: a.title ?? "",
|
|
76
76
|
url: a.url,
|
|
77
77
|
source: a.source,
|
|
78
|
-
publishedAt: a.published_at
|
|
78
|
+
publishedAt: a.published_at,
|
|
79
|
+
imageUrl: a.image_url
|
|
79
80
|
}));
|
|
80
81
|
}
|
|
81
82
|
function buildUrl(apiEndpoint, options) {
|
|
@@ -161,7 +162,25 @@ function useTrendingNews(options = {}) {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// src/components/TrendingNews.tsx
|
|
165
|
+
var import_react2 = require("react");
|
|
164
166
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
167
|
+
function ChevronIcon({ up }) {
|
|
168
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
169
|
+
"svg",
|
|
170
|
+
{
|
|
171
|
+
width: 14,
|
|
172
|
+
height: 14,
|
|
173
|
+
viewBox: "0 0 24 24",
|
|
174
|
+
fill: "none",
|
|
175
|
+
stroke: "currentColor",
|
|
176
|
+
strokeWidth: 2,
|
|
177
|
+
strokeLinecap: "round",
|
|
178
|
+
strokeLinejoin: "round",
|
|
179
|
+
style: { transform: up ? "rotate(180deg)" : void 0, transition: "transform 150ms ease" },
|
|
180
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "6 9 12 15 18 9" })
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
}
|
|
165
184
|
var styles = {
|
|
166
185
|
root: {
|
|
167
186
|
fontFamily: "system-ui, sans-serif",
|
|
@@ -180,6 +199,7 @@ var styles = {
|
|
|
180
199
|
padding: "10px 14px",
|
|
181
200
|
borderRadius: 8
|
|
182
201
|
},
|
|
202
|
+
compactHeaderRow: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 },
|
|
183
203
|
compactHeader: {
|
|
184
204
|
fontSize: 11,
|
|
185
205
|
fontWeight: 600,
|
|
@@ -187,19 +207,47 @@ var styles = {
|
|
|
187
207
|
letterSpacing: 0.3,
|
|
188
208
|
textTransform: "uppercase"
|
|
189
209
|
},
|
|
210
|
+
expandToggle: {
|
|
211
|
+
display: "flex",
|
|
212
|
+
alignItems: "center",
|
|
213
|
+
justifyContent: "center",
|
|
214
|
+
background: "transparent",
|
|
215
|
+
border: "none",
|
|
216
|
+
padding: 2,
|
|
217
|
+
margin: 0,
|
|
218
|
+
color: "inherit",
|
|
219
|
+
opacity: 0.6,
|
|
220
|
+
cursor: "pointer"
|
|
221
|
+
},
|
|
222
|
+
expandedList: { display: "flex", flexDirection: "column", gap: 12, maxHeight: 360, overflowY: "auto" },
|
|
190
223
|
topicRow: { display: "flex", gap: 10, overflowX: "auto", paddingBottom: 2 },
|
|
191
224
|
topicCard: {
|
|
192
225
|
minWidth: 160,
|
|
193
226
|
maxWidth: 200,
|
|
194
|
-
padding: "8px 10px",
|
|
195
227
|
borderRadius: 8,
|
|
196
228
|
background: "rgba(255,255,255,0.06)",
|
|
197
229
|
display: "flex",
|
|
198
230
|
flexDirection: "column",
|
|
199
231
|
gap: 4,
|
|
200
232
|
textDecoration: "none",
|
|
201
|
-
color: "inherit"
|
|
233
|
+
color: "inherit",
|
|
234
|
+
overflow: "hidden"
|
|
202
235
|
},
|
|
236
|
+
topicCardBody: { padding: "8px 10px", display: "flex", flexDirection: "column", gap: 4 },
|
|
237
|
+
topicThumb: {
|
|
238
|
+
width: "100%",
|
|
239
|
+
height: 84,
|
|
240
|
+
objectFit: "cover",
|
|
241
|
+
display: "block"
|
|
242
|
+
},
|
|
243
|
+
articleThumb: {
|
|
244
|
+
width: 48,
|
|
245
|
+
height: 48,
|
|
246
|
+
objectFit: "cover",
|
|
247
|
+
borderRadius: 6,
|
|
248
|
+
flexShrink: 0
|
|
249
|
+
},
|
|
250
|
+
fullTopicWithThumb: { display: "flex", gap: 10, alignItems: "flex-start" },
|
|
203
251
|
topicName: {
|
|
204
252
|
fontSize: 12,
|
|
205
253
|
fontWeight: 600,
|
|
@@ -222,55 +270,95 @@ var styles = {
|
|
|
222
270
|
article: { fontSize: 13 },
|
|
223
271
|
articleLink: { color: "inherit", textDecoration: "none" }
|
|
224
272
|
};
|
|
273
|
+
function ArticleLine({ article, showImage }) {
|
|
274
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...styles.article, ...showImage && article.imageUrl ? styles.fullTopicWithThumb : {} }, children: [
|
|
275
|
+
showImage && article.imageUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: article.imageUrl, alt: "", style: styles.articleThumb }),
|
|
276
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
277
|
+
article.url ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: article.url, target: "_blank", rel: "noreferrer", style: styles.articleLink, children: article.title }) : article.title,
|
|
278
|
+
article.source && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { opacity: 0.6 }, children: [
|
|
279
|
+
" \xB7 ",
|
|
280
|
+
article.source
|
|
281
|
+
] })
|
|
282
|
+
] })
|
|
283
|
+
] });
|
|
284
|
+
}
|
|
285
|
+
function TopicArticleList({ topic, showImages }) {
|
|
286
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.fullTopic, children: [
|
|
287
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.fullTopicHeader, children: [
|
|
288
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: topic.topic }),
|
|
289
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: styles.count, children: [
|
|
290
|
+
topic.newsCount,
|
|
291
|
+
" article",
|
|
292
|
+
topic.newsCount === 1 ? "" : "s"
|
|
293
|
+
] })
|
|
294
|
+
] }),
|
|
295
|
+
topic.articles.slice(0, 5).map((article, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArticleLine, { article, showImage: showImages }, article.url ?? i))
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
225
298
|
function TrendingNews(props) {
|
|
226
|
-
const {
|
|
299
|
+
const {
|
|
300
|
+
className,
|
|
301
|
+
style,
|
|
302
|
+
compact,
|
|
303
|
+
maxTopics = 8,
|
|
304
|
+
expandable,
|
|
305
|
+
expandedMaxTopics = 15,
|
|
306
|
+
showImages = true,
|
|
307
|
+
...options
|
|
308
|
+
} = props;
|
|
227
309
|
const { data, loading, error } = useTrendingNews(options);
|
|
310
|
+
const [expanded, setExpanded] = (0, import_react2.useState)(false);
|
|
228
311
|
if (!options.apiEndpoint) return null;
|
|
229
312
|
if (loading && !data) return null;
|
|
230
313
|
if (error) return null;
|
|
231
314
|
if (!data || data.topics.length === 0) return null;
|
|
232
315
|
const topics = data.topics.slice(0, maxTopics);
|
|
233
316
|
if (compact) {
|
|
317
|
+
const showExpanded = expandable && expanded;
|
|
318
|
+
const expandedTopics = showExpanded ? data.topics.slice(0, expandedMaxTopics) : [];
|
|
234
319
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, style: { ...styles.compactRoot, ...style }, children: [
|
|
235
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
320
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.compactHeaderRow, children: [
|
|
321
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.compactHeader, children: "Trending" }),
|
|
322
|
+
expandable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
323
|
+
"button",
|
|
324
|
+
{
|
|
325
|
+
type: "button",
|
|
326
|
+
onClick: () => setExpanded((e) => !e),
|
|
327
|
+
"aria-expanded": showExpanded,
|
|
328
|
+
"aria-label": showExpanded ? "Collapse trending news" : "Expand trending news",
|
|
329
|
+
style: styles.expandToggle,
|
|
330
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronIcon, { up: showExpanded })
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
] }),
|
|
334
|
+
showExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.expandedList, children: expandedTopics.map((topic) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TopicArticleList, { topic, showImages }, topic.topic)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.topicRow, children: topics.map((topic) => {
|
|
335
|
+
const thumb = showImages ? topic.articles[0]?.imageUrl : void 0;
|
|
336
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
337
|
+
"a",
|
|
338
|
+
{
|
|
339
|
+
href: topic.articles[0]?.url,
|
|
340
|
+
target: "_blank",
|
|
341
|
+
rel: "noreferrer",
|
|
342
|
+
style: styles.topicCard,
|
|
343
|
+
children: [
|
|
344
|
+
thumb && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: thumb, alt: "", style: styles.topicThumb }),
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.topicCardBody, children: [
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.topicName, children: topic.topic }),
|
|
347
|
+
topic.articles[0] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.headline, children: topic.articles[0].title }),
|
|
348
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.count, children: [
|
|
349
|
+
topic.newsCount,
|
|
350
|
+
" article",
|
|
351
|
+
topic.newsCount === 1 ? "" : "s"
|
|
352
|
+
] })
|
|
353
|
+
] })
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
topic.topic
|
|
357
|
+
);
|
|
358
|
+
}) })
|
|
255
359
|
] });
|
|
256
360
|
}
|
|
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.
|
|
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)) });
|
|
361
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: { ...styles.root, ...styles.list, ...style }, children: topics.map((topic) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TopicArticleList, { topic, showImages }, topic.topic)) });
|
|
274
362
|
}
|
|
275
363
|
// Annotate the CommonJS export names for ESM import in node:
|
|
276
364
|
0 && (module.exports = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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 }}> · {article.source}</span>}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
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 image_url?: 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 imageUrl: a.image_url,\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, { useState } from 'react';\nimport { useTrendingNews } from '../hooks/useTrendingNews';\nimport type { TrendingNewsOptions, TrendingTopic } 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 * When set on a `compact` card, a chevron toggles between the compact\n * topic row and a full topic-by-topic article list, without leaving the\n * card's layout. Ignored outside `compact` mode, which already shows the\n * full list.\n */\n expandable?: boolean;\n /** Max topics to render once expanded (default 15). */\n expandedMaxTopics?: number;\n /** Show each topic's lead article thumbnail image, when available (default true). */\n showImages?: boolean;\n};\n\nfunction ChevronIcon({ up }: { up?: boolean }) {\n return (\n <svg\n width={14}\n height={14}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{ transform: up ? 'rotate(180deg)' : undefined, transition: 'transform 150ms ease' }}\n >\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n );\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 compactHeaderRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } 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 expandToggle: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'transparent',\n border: 'none',\n padding: 2,\n margin: 0,\n color: 'inherit',\n opacity: 0.6,\n cursor: 'pointer',\n } as React.CSSProperties,\n expandedList: { display: 'flex', flexDirection: 'column', gap: 12, maxHeight: 360, overflowY: 'auto' } 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 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 overflow: 'hidden',\n } as React.CSSProperties,\n topicCardBody: { padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 4 } as React.CSSProperties,\n topicThumb: {\n width: '100%',\n height: 84,\n objectFit: 'cover',\n display: 'block',\n } as React.CSSProperties,\n articleThumb: {\n width: 48,\n height: 48,\n objectFit: 'cover',\n borderRadius: 6,\n flexShrink: 0,\n } as React.CSSProperties,\n fullTopicWithThumb: { display: 'flex', gap: 10, alignItems: 'flex-start' } 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\nfunction ArticleLine({ article, showImage }: { article: TrendingTopic['articles'][number]; showImage?: boolean }) {\n return (\n <div style={{ ...styles.article, ...(showImage && article.imageUrl ? styles.fullTopicWithThumb : {}) }}>\n {showImage && article.imageUrl && <img src={article.imageUrl} alt=\"\" style={styles.articleThumb} />}\n <div>\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 }}> · {article.source}</span>}\n </div>\n </div>\n );\n}\n\nfunction TopicArticleList({ topic, showImages }: { topic: TrendingTopic; showImages: boolean }) {\n return (\n <div 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 <ArticleLine key={article.url ?? i} article={article} showImage={showImages} />\n ))}\n </div>\n );\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 {\n className,\n style,\n compact,\n maxTopics = 8,\n expandable,\n expandedMaxTopics = 15,\n showImages = true,\n ...options\n } = props;\n const { data, loading, error } = useTrendingNews(options);\n const [expanded, setExpanded] = useState(false);\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 const showExpanded = expandable && expanded;\n const expandedTopics = showExpanded ? data.topics.slice(0, expandedMaxTopics) : [];\n\n return (\n <div className={className} style={{ ...styles.compactRoot, ...style }}>\n <div style={styles.compactHeaderRow}>\n <div style={styles.compactHeader}>Trending</div>\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((e) => !e)}\n aria-expanded={showExpanded}\n aria-label={showExpanded ? 'Collapse trending news' : 'Expand trending news'}\n style={styles.expandToggle}\n >\n <ChevronIcon up={showExpanded} />\n </button>\n )}\n </div>\n\n {showExpanded ? (\n <div style={styles.expandedList}>\n {expandedTopics.map((topic) => (\n <TopicArticleList key={topic.topic} topic={topic} showImages={showImages} />\n ))}\n </div>\n ) : (\n <div style={styles.topicRow}>\n {topics.map((topic) => {\n const thumb = showImages ? topic.articles[0]?.imageUrl : undefined;\n return (\n <a\n key={topic.topic}\n href={topic.articles[0]?.url}\n target=\"_blank\"\n rel=\"noreferrer\"\n style={styles.topicCard}\n >\n {thumb && <img src={thumb} alt=\"\" style={styles.topicThumb} />}\n <div style={styles.topicCardBody}>\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 </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n );\n }\n\n return (\n <div className={className} style={{ ...styles.root, ...styles.list, ...style }}>\n {topics.map((topic) => (\n <TopicArticleList key={topic.topic} topic={topic} showImages={showImages} />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["import_react"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -5,6 +5,7 @@ type NewsArticle = {
|
|
|
5
5
|
url?: string;
|
|
6
6
|
source?: string;
|
|
7
7
|
publishedAt?: string;
|
|
8
|
+
imageUrl?: string;
|
|
8
9
|
};
|
|
9
10
|
type TrendingTopic = {
|
|
10
11
|
topic: string;
|
|
@@ -54,6 +55,17 @@ type Props = TrendingNewsOptions & {
|
|
|
54
55
|
compact?: boolean;
|
|
55
56
|
/** Max trending topics to render (default 8). */
|
|
56
57
|
maxTopics?: number;
|
|
58
|
+
/**
|
|
59
|
+
* When set on a `compact` card, a chevron toggles between the compact
|
|
60
|
+
* topic row and a full topic-by-topic article list, without leaving the
|
|
61
|
+
* card's layout. Ignored outside `compact` mode, which already shows the
|
|
62
|
+
* full list.
|
|
63
|
+
*/
|
|
64
|
+
expandable?: boolean;
|
|
65
|
+
/** Max topics to render once expanded (default 15). */
|
|
66
|
+
expandedMaxTopics?: number;
|
|
67
|
+
/** Show each topic's lead article thumbnail image, when available (default true). */
|
|
68
|
+
showImages?: boolean;
|
|
57
69
|
};
|
|
58
70
|
/**
|
|
59
71
|
* Trending news widget. Requires `apiEndpoint` pointing at a deployed
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type NewsArticle = {
|
|
|
5
5
|
url?: string;
|
|
6
6
|
source?: string;
|
|
7
7
|
publishedAt?: string;
|
|
8
|
+
imageUrl?: string;
|
|
8
9
|
};
|
|
9
10
|
type TrendingTopic = {
|
|
10
11
|
topic: string;
|
|
@@ -54,6 +55,17 @@ type Props = TrendingNewsOptions & {
|
|
|
54
55
|
compact?: boolean;
|
|
55
56
|
/** Max trending topics to render (default 8). */
|
|
56
57
|
maxTopics?: number;
|
|
58
|
+
/**
|
|
59
|
+
* When set on a `compact` card, a chevron toggles between the compact
|
|
60
|
+
* topic row and a full topic-by-topic article list, without leaving the
|
|
61
|
+
* card's layout. Ignored outside `compact` mode, which already shows the
|
|
62
|
+
* full list.
|
|
63
|
+
*/
|
|
64
|
+
expandable?: boolean;
|
|
65
|
+
/** Max topics to render once expanded (default 15). */
|
|
66
|
+
expandedMaxTopics?: number;
|
|
67
|
+
/** Show each topic's lead article thumbnail image, when available (default true). */
|
|
68
|
+
showImages?: boolean;
|
|
57
69
|
};
|
|
58
70
|
/**
|
|
59
71
|
* Trending news widget. Requires `apiEndpoint` pointing at a deployed
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,8 @@ function mapArticles(articles = []) {
|
|
|
45
45
|
title: a.title ?? "",
|
|
46
46
|
url: a.url,
|
|
47
47
|
source: a.source,
|
|
48
|
-
publishedAt: a.published_at
|
|
48
|
+
publishedAt: a.published_at,
|
|
49
|
+
imageUrl: a.image_url
|
|
49
50
|
}));
|
|
50
51
|
}
|
|
51
52
|
function buildUrl(apiEndpoint, options) {
|
|
@@ -131,7 +132,25 @@ function useTrendingNews(options = {}) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
// src/components/TrendingNews.tsx
|
|
135
|
+
import { useState as useState2 } from "react";
|
|
134
136
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
137
|
+
function ChevronIcon({ up }) {
|
|
138
|
+
return /* @__PURE__ */ jsx(
|
|
139
|
+
"svg",
|
|
140
|
+
{
|
|
141
|
+
width: 14,
|
|
142
|
+
height: 14,
|
|
143
|
+
viewBox: "0 0 24 24",
|
|
144
|
+
fill: "none",
|
|
145
|
+
stroke: "currentColor",
|
|
146
|
+
strokeWidth: 2,
|
|
147
|
+
strokeLinecap: "round",
|
|
148
|
+
strokeLinejoin: "round",
|
|
149
|
+
style: { transform: up ? "rotate(180deg)" : void 0, transition: "transform 150ms ease" },
|
|
150
|
+
children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" })
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
135
154
|
var styles = {
|
|
136
155
|
root: {
|
|
137
156
|
fontFamily: "system-ui, sans-serif",
|
|
@@ -150,6 +169,7 @@ var styles = {
|
|
|
150
169
|
padding: "10px 14px",
|
|
151
170
|
borderRadius: 8
|
|
152
171
|
},
|
|
172
|
+
compactHeaderRow: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 },
|
|
153
173
|
compactHeader: {
|
|
154
174
|
fontSize: 11,
|
|
155
175
|
fontWeight: 600,
|
|
@@ -157,19 +177,47 @@ var styles = {
|
|
|
157
177
|
letterSpacing: 0.3,
|
|
158
178
|
textTransform: "uppercase"
|
|
159
179
|
},
|
|
180
|
+
expandToggle: {
|
|
181
|
+
display: "flex",
|
|
182
|
+
alignItems: "center",
|
|
183
|
+
justifyContent: "center",
|
|
184
|
+
background: "transparent",
|
|
185
|
+
border: "none",
|
|
186
|
+
padding: 2,
|
|
187
|
+
margin: 0,
|
|
188
|
+
color: "inherit",
|
|
189
|
+
opacity: 0.6,
|
|
190
|
+
cursor: "pointer"
|
|
191
|
+
},
|
|
192
|
+
expandedList: { display: "flex", flexDirection: "column", gap: 12, maxHeight: 360, overflowY: "auto" },
|
|
160
193
|
topicRow: { display: "flex", gap: 10, overflowX: "auto", paddingBottom: 2 },
|
|
161
194
|
topicCard: {
|
|
162
195
|
minWidth: 160,
|
|
163
196
|
maxWidth: 200,
|
|
164
|
-
padding: "8px 10px",
|
|
165
197
|
borderRadius: 8,
|
|
166
198
|
background: "rgba(255,255,255,0.06)",
|
|
167
199
|
display: "flex",
|
|
168
200
|
flexDirection: "column",
|
|
169
201
|
gap: 4,
|
|
170
202
|
textDecoration: "none",
|
|
171
|
-
color: "inherit"
|
|
203
|
+
color: "inherit",
|
|
204
|
+
overflow: "hidden"
|
|
172
205
|
},
|
|
206
|
+
topicCardBody: { padding: "8px 10px", display: "flex", flexDirection: "column", gap: 4 },
|
|
207
|
+
topicThumb: {
|
|
208
|
+
width: "100%",
|
|
209
|
+
height: 84,
|
|
210
|
+
objectFit: "cover",
|
|
211
|
+
display: "block"
|
|
212
|
+
},
|
|
213
|
+
articleThumb: {
|
|
214
|
+
width: 48,
|
|
215
|
+
height: 48,
|
|
216
|
+
objectFit: "cover",
|
|
217
|
+
borderRadius: 6,
|
|
218
|
+
flexShrink: 0
|
|
219
|
+
},
|
|
220
|
+
fullTopicWithThumb: { display: "flex", gap: 10, alignItems: "flex-start" },
|
|
173
221
|
topicName: {
|
|
174
222
|
fontSize: 12,
|
|
175
223
|
fontWeight: 600,
|
|
@@ -192,55 +240,95 @@ var styles = {
|
|
|
192
240
|
article: { fontSize: 13 },
|
|
193
241
|
articleLink: { color: "inherit", textDecoration: "none" }
|
|
194
242
|
};
|
|
243
|
+
function ArticleLine({ article, showImage }) {
|
|
244
|
+
return /* @__PURE__ */ jsxs("div", { style: { ...styles.article, ...showImage && article.imageUrl ? styles.fullTopicWithThumb : {} }, children: [
|
|
245
|
+
showImage && article.imageUrl && /* @__PURE__ */ jsx("img", { src: article.imageUrl, alt: "", style: styles.articleThumb }),
|
|
246
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
247
|
+
article.url ? /* @__PURE__ */ jsx("a", { href: article.url, target: "_blank", rel: "noreferrer", style: styles.articleLink, children: article.title }) : article.title,
|
|
248
|
+
article.source && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.6 }, children: [
|
|
249
|
+
" \xB7 ",
|
|
250
|
+
article.source
|
|
251
|
+
] })
|
|
252
|
+
] })
|
|
253
|
+
] });
|
|
254
|
+
}
|
|
255
|
+
function TopicArticleList({ topic, showImages }) {
|
|
256
|
+
return /* @__PURE__ */ jsxs("div", { style: styles.fullTopic, children: [
|
|
257
|
+
/* @__PURE__ */ jsxs("div", { style: styles.fullTopicHeader, children: [
|
|
258
|
+
/* @__PURE__ */ jsx("strong", { children: topic.topic }),
|
|
259
|
+
/* @__PURE__ */ jsxs("span", { style: styles.count, children: [
|
|
260
|
+
topic.newsCount,
|
|
261
|
+
" article",
|
|
262
|
+
topic.newsCount === 1 ? "" : "s"
|
|
263
|
+
] })
|
|
264
|
+
] }),
|
|
265
|
+
topic.articles.slice(0, 5).map((article, i) => /* @__PURE__ */ jsx(ArticleLine, { article, showImage: showImages }, article.url ?? i))
|
|
266
|
+
] });
|
|
267
|
+
}
|
|
195
268
|
function TrendingNews(props) {
|
|
196
|
-
const {
|
|
269
|
+
const {
|
|
270
|
+
className,
|
|
271
|
+
style,
|
|
272
|
+
compact,
|
|
273
|
+
maxTopics = 8,
|
|
274
|
+
expandable,
|
|
275
|
+
expandedMaxTopics = 15,
|
|
276
|
+
showImages = true,
|
|
277
|
+
...options
|
|
278
|
+
} = props;
|
|
197
279
|
const { data, loading, error } = useTrendingNews(options);
|
|
280
|
+
const [expanded, setExpanded] = useState2(false);
|
|
198
281
|
if (!options.apiEndpoint) return null;
|
|
199
282
|
if (loading && !data) return null;
|
|
200
283
|
if (error) return null;
|
|
201
284
|
if (!data || data.topics.length === 0) return null;
|
|
202
285
|
const topics = data.topics.slice(0, maxTopics);
|
|
203
286
|
if (compact) {
|
|
287
|
+
const showExpanded = expandable && expanded;
|
|
288
|
+
const expandedTopics = showExpanded ? data.topics.slice(0, expandedMaxTopics) : [];
|
|
204
289
|
return /* @__PURE__ */ jsxs("div", { className, style: { ...styles.compactRoot, ...style }, children: [
|
|
205
|
-
/* @__PURE__ */
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
290
|
+
/* @__PURE__ */ jsxs("div", { style: styles.compactHeaderRow, children: [
|
|
291
|
+
/* @__PURE__ */ jsx("div", { style: styles.compactHeader, children: "Trending" }),
|
|
292
|
+
expandable && /* @__PURE__ */ jsx(
|
|
293
|
+
"button",
|
|
294
|
+
{
|
|
295
|
+
type: "button",
|
|
296
|
+
onClick: () => setExpanded((e) => !e),
|
|
297
|
+
"aria-expanded": showExpanded,
|
|
298
|
+
"aria-label": showExpanded ? "Collapse trending news" : "Expand trending news",
|
|
299
|
+
style: styles.expandToggle,
|
|
300
|
+
children: /* @__PURE__ */ jsx(ChevronIcon, { up: showExpanded })
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
] }),
|
|
304
|
+
showExpanded ? /* @__PURE__ */ jsx("div", { style: styles.expandedList, children: expandedTopics.map((topic) => /* @__PURE__ */ jsx(TopicArticleList, { topic, showImages }, topic.topic)) }) : /* @__PURE__ */ jsx("div", { style: styles.topicRow, children: topics.map((topic) => {
|
|
305
|
+
const thumb = showImages ? topic.articles[0]?.imageUrl : void 0;
|
|
306
|
+
return /* @__PURE__ */ jsxs(
|
|
307
|
+
"a",
|
|
308
|
+
{
|
|
309
|
+
href: topic.articles[0]?.url,
|
|
310
|
+
target: "_blank",
|
|
311
|
+
rel: "noreferrer",
|
|
312
|
+
style: styles.topicCard,
|
|
313
|
+
children: [
|
|
314
|
+
thumb && /* @__PURE__ */ jsx("img", { src: thumb, alt: "", style: styles.topicThumb }),
|
|
315
|
+
/* @__PURE__ */ jsxs("div", { style: styles.topicCardBody, children: [
|
|
316
|
+
/* @__PURE__ */ jsx("div", { style: styles.topicName, children: topic.topic }),
|
|
317
|
+
topic.articles[0] && /* @__PURE__ */ jsx("div", { style: styles.headline, children: topic.articles[0].title }),
|
|
318
|
+
/* @__PURE__ */ jsxs("div", { style: styles.count, children: [
|
|
319
|
+
topic.newsCount,
|
|
320
|
+
" article",
|
|
321
|
+
topic.newsCount === 1 ? "" : "s"
|
|
322
|
+
] })
|
|
323
|
+
] })
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
topic.topic
|
|
327
|
+
);
|
|
328
|
+
}) })
|
|
225
329
|
] });
|
|
226
330
|
}
|
|
227
|
-
return /* @__PURE__ */ jsx("div", { className, style: { ...styles.root, ...styles.list, ...style }, children: topics.map((topic) => /* @__PURE__ */
|
|
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)) });
|
|
331
|
+
return /* @__PURE__ */ jsx("div", { className, style: { ...styles.root, ...styles.list, ...style }, children: topics.map((topic) => /* @__PURE__ */ jsx(TopicArticleList, { topic, showImages }, topic.topic)) });
|
|
244
332
|
}
|
|
245
333
|
export {
|
|
246
334
|
TrendingNews,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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 }}> · {article.source}</span>}\n </div>\n ))}\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
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 image_url?: 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 imageUrl: a.image_url,\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, { useState } from 'react';\nimport { useTrendingNews } from '../hooks/useTrendingNews';\nimport type { TrendingNewsOptions, TrendingTopic } 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 * When set on a `compact` card, a chevron toggles between the compact\n * topic row and a full topic-by-topic article list, without leaving the\n * card's layout. Ignored outside `compact` mode, which already shows the\n * full list.\n */\n expandable?: boolean;\n /** Max topics to render once expanded (default 15). */\n expandedMaxTopics?: number;\n /** Show each topic's lead article thumbnail image, when available (default true). */\n showImages?: boolean;\n};\n\nfunction ChevronIcon({ up }: { up?: boolean }) {\n return (\n <svg\n width={14}\n height={14}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={{ transform: up ? 'rotate(180deg)' : undefined, transition: 'transform 150ms ease' }}\n >\n <polyline points=\"6 9 12 15 18 9\" />\n </svg>\n );\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 compactHeaderRow: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } 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 expandToggle: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'transparent',\n border: 'none',\n padding: 2,\n margin: 0,\n color: 'inherit',\n opacity: 0.6,\n cursor: 'pointer',\n } as React.CSSProperties,\n expandedList: { display: 'flex', flexDirection: 'column', gap: 12, maxHeight: 360, overflowY: 'auto' } 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 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 overflow: 'hidden',\n } as React.CSSProperties,\n topicCardBody: { padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 4 } as React.CSSProperties,\n topicThumb: {\n width: '100%',\n height: 84,\n objectFit: 'cover',\n display: 'block',\n } as React.CSSProperties,\n articleThumb: {\n width: 48,\n height: 48,\n objectFit: 'cover',\n borderRadius: 6,\n flexShrink: 0,\n } as React.CSSProperties,\n fullTopicWithThumb: { display: 'flex', gap: 10, alignItems: 'flex-start' } 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\nfunction ArticleLine({ article, showImage }: { article: TrendingTopic['articles'][number]; showImage?: boolean }) {\n return (\n <div style={{ ...styles.article, ...(showImage && article.imageUrl ? styles.fullTopicWithThumb : {}) }}>\n {showImage && article.imageUrl && <img src={article.imageUrl} alt=\"\" style={styles.articleThumb} />}\n <div>\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 }}> · {article.source}</span>}\n </div>\n </div>\n );\n}\n\nfunction TopicArticleList({ topic, showImages }: { topic: TrendingTopic; showImages: boolean }) {\n return (\n <div 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 <ArticleLine key={article.url ?? i} article={article} showImage={showImages} />\n ))}\n </div>\n );\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 {\n className,\n style,\n compact,\n maxTopics = 8,\n expandable,\n expandedMaxTopics = 15,\n showImages = true,\n ...options\n } = props;\n const { data, loading, error } = useTrendingNews(options);\n const [expanded, setExpanded] = useState(false);\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 const showExpanded = expandable && expanded;\n const expandedTopics = showExpanded ? data.topics.slice(0, expandedMaxTopics) : [];\n\n return (\n <div className={className} style={{ ...styles.compactRoot, ...style }}>\n <div style={styles.compactHeaderRow}>\n <div style={styles.compactHeader}>Trending</div>\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((e) => !e)}\n aria-expanded={showExpanded}\n aria-label={showExpanded ? 'Collapse trending news' : 'Expand trending news'}\n style={styles.expandToggle}\n >\n <ChevronIcon up={showExpanded} />\n </button>\n )}\n </div>\n\n {showExpanded ? (\n <div style={styles.expandedList}>\n {expandedTopics.map((topic) => (\n <TopicArticleList key={topic.topic} topic={topic} showImages={showImages} />\n ))}\n </div>\n ) : (\n <div style={styles.topicRow}>\n {topics.map((topic) => {\n const thumb = showImages ? topic.articles[0]?.imageUrl : undefined;\n return (\n <a\n key={topic.topic}\n href={topic.articles[0]?.url}\n target=\"_blank\"\n rel=\"noreferrer\"\n style={styles.topicCard}\n >\n {thumb && <img src={thumb} alt=\"\" style={styles.topicThumb} />}\n <div style={styles.topicCardBody}>\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 </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n );\n }\n\n return (\n <div className={className} style={{ ...styles.root, ...styles.list, ...style }}>\n {topics.map((topic) => (\n <TopicArticleList key={topic.topic} topic={topic} showImages={showImages} />\n ))}\n </div>\n );\n}\n"],"mappings":"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","names":["useState","useState"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trending-news-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "React trending news widget backed by Wikipedia pageviews and The News API, via a Cloudflare Worker proxy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -60,4 +60,4 @@
|
|
|
60
60
|
"worker:dev": "wrangler dev --config worker/wrangler.jsonc",
|
|
61
61
|
"worker:deploy": "wrangler deploy --config worker/wrangler.jsonc"
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
}
|