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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # trending-news-api
2
2
 
3
+ [![Coverage](https://codecov.io/gh/OpenSourceAGI/qwksearch-research-agent/graph/badge.svg?component=package-trending-news-api)](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 { className, style, compact, maxTopics = 8, ...options } = props;
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.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
- )) })
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.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)) });
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 = {
@@ -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 }}> &middot; {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 }}> &middot; {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 { className, style, compact, maxTopics = 8, ...options } = props;
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__ */ 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
- )) })
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__ */ 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)) });
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 }}> &middot; {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 }}> &middot; {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.0",
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
+ }