finsignal-feed-explore 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/NewsFeed.js +83 -25
  2. package/package.json +1 -1
package/dist/NewsFeed.js CHANGED
@@ -191,38 +191,86 @@ function formatTimeAgo(dateString) {
191
191
  export function FeedList({ feedType = 'trending', items: itemsProp, onItemClick, className = '', feedApiUrl = 'https://explore-gateway.changesandbox.ru', useMockData = false }) {
192
192
  const [items, setItems] = React.useState(itemsProp || mockFeedItems);
193
193
  const [loading, setLoading] = React.useState(false);
194
+ const [loadingMore, setLoadingMore] = React.useState(false);
194
195
  const [error, setError] = React.useState(null);
195
- // Загрузка новостей с API
196
- React.useEffect(() => {
196
+ const [cursor, setCursor] = React.useState(undefined);
197
+ const [hasMore, setHasMore] = React.useState(true);
198
+ const scrollContainerRef = React.useRef(null);
199
+ // Загрузка начальных новостей
200
+ const fetchNews = React.useCallback((...args_1) => __awaiter(this, [...args_1], void 0, function* (isInitial = true) {
201
+ var _a;
197
202
  if (useMockData || itemsProp) {
198
203
  setItems(itemsProp || mockFeedItems);
204
+ setHasMore(false);
199
205
  return;
200
206
  }
201
- const fetchNews = () => __awaiter(this, void 0, void 0, function* () {
207
+ const currentCursor = isInitial ? undefined : cursor;
208
+ const limit = isInitial ? 20 : 20; // initialLoad: 20, pageSize: 20
209
+ if (isInitial) {
202
210
  setLoading(true);
203
- setError(null);
204
- try {
205
- const response = yield fetch(`${feedApiUrl}/api/feed/${feedType}`);
206
- if (!response.ok) {
207
- throw new Error(`HTTP error! status: ${response.status}`);
208
- }
209
- const data = yield response.json();
210
- // Преобразуем данные из API в формат FeedItem
211
- const newsItems = data.items || data;
212
- setItems(newsItems);
211
+ }
212
+ else {
213
+ setLoadingMore(true);
214
+ }
215
+ setError(null);
216
+ try {
217
+ const url = new URL(`${feedApiUrl}/api/feed/${feedType}`);
218
+ if (currentCursor)
219
+ url.searchParams.append('cursor', currentCursor);
220
+ url.searchParams.append('limit', limit.toString());
221
+ const response = yield fetch(url.toString());
222
+ if (!response.ok) {
223
+ throw new Error(`HTTP error! status: ${response.status}`);
213
224
  }
214
- catch (err) {
215
- console.error('Error fetching news:', err);
216
- setError(err instanceof Error ? err.message : 'Failed to fetch news');
217
- // Fallback на моки при ошибке
225
+ const data = yield response.json();
226
+ // Формат ответа: { items: [], next_offset?: string }
227
+ const newsItems = data.items || data;
228
+ const nextOffset = (_a = data.next_offset) === null || _a === void 0 ? void 0 : _a.toString();
229
+ setItems(prev => isInitial ? newsItems : [...prev, ...newsItems]);
230
+ setCursor(nextOffset);
231
+ setHasMore(!!nextOffset);
232
+ }
233
+ catch (err) {
234
+ console.error('Error fetching news:', err);
235
+ setError(err instanceof Error ? err.message : 'Failed to fetch news');
236
+ // Fallback на моки при ошибке только для первой загрузки
237
+ if (isInitial) {
218
238
  setItems(mockFeedItems);
239
+ setHasMore(false);
219
240
  }
220
- finally {
221
- setLoading(false);
222
- }
223
- });
224
- fetchNews();
225
- }, [feedApiUrl, feedType, useMockData, itemsProp]);
241
+ }
242
+ finally {
243
+ setLoading(false);
244
+ setLoadingMore(false);
245
+ }
246
+ }), [feedApiUrl, feedType, useMockData, itemsProp, cursor]);
247
+ // Начальная загрузка
248
+ React.useEffect(() => {
249
+ fetchNews(true);
250
+ }, [feedApiUrl, feedType, useMockData]);
251
+ // Infinite scroll: обработка скролла
252
+ const handleScroll = React.useCallback(() => {
253
+ if (!scrollContainerRef.current || !hasMore || loadingMore)
254
+ return;
255
+ const container = scrollContainerRef.current;
256
+ const scrollTop = container.scrollTop;
257
+ const scrollHeight = container.scrollHeight;
258
+ const clientHeight = container.clientHeight;
259
+ // Подгружаем когда осталось 70% до конца (prefetchThreshold: 0.7)
260
+ const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
261
+ if (scrollPercentage > 0.7) {
262
+ console.log('📜 Loading more news...');
263
+ fetchNews(false);
264
+ }
265
+ }, [hasMore, loadingMore, fetchNews]);
266
+ // Добавляем слушатель скролла
267
+ React.useEffect(() => {
268
+ const container = scrollContainerRef.current;
269
+ if (!container)
270
+ return;
271
+ container.addEventListener('scroll', handleScroll);
272
+ return () => container.removeEventListener('scroll', handleScroll);
273
+ }, [handleScroll]);
226
274
  const getTypeLabel = (type) => {
227
275
  const labels = {
228
276
  signal: 'Сигнал',
@@ -247,7 +295,7 @@ export function FeedList({ feedType = 'trending', items: itemsProp, onItemClick,
247
295
  };
248
296
  return colors[type] || colors.post;
249
297
  };
250
- return (_jsxs("div", { className: `feed-list ${className}`, children: [loading && (_jsx("div", { style: {
298
+ return (_jsxs("div", { ref: scrollContainerRef, className: `feed-list ${className}`, style: Object.assign({ overflowY: 'auto', maxHeight: '100%' }, ((className === '') && { height: '100%' })), children: [loading && (_jsx("div", { style: {
251
299
  textAlign: 'center',
252
300
  padding: '20px',
253
301
  color: '#6b7280',
@@ -270,5 +318,15 @@ export function FeedList({ feedType = 'trending', items: itemsProp, onItemClick,
270
318
  }
271
319
  // Для остальных типов оставляем обычный рендеринг
272
320
  return (_jsxs("div", { className: "feed-item", onClick: () => onItemClick === null || onItemClick === void 0 ? void 0 : onItemClick(item), children: [_jsxs("div", { className: "feed-item-header", children: [_jsx("span", { className: "feed-type-badge", style: { backgroundColor: getTypeColor(item.type) }, children: getTypeLabel(item.type) }), _jsx("span", { className: "feed-time", children: formatTimeAgo(item.created_at) })] }), item.title && _jsx("h3", { className: "feed-title", children: item.title }), item.body && _jsx("p", { className: "feed-body", children: item.body }), item.metadata.signal && (_jsxs("div", { className: "feed-signal-metrics", children: [_jsxs("div", { className: "signal-stock", children: [_jsx("span", { className: "signal-symbol", children: item.metadata.signal.symbol }), _jsx("span", { className: "signal-company", children: item.metadata.signal.companyName })] }), _jsxs("div", { className: "signal-prices", children: [_jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "Entry:" }), _jsx("span", { className: "value", children: item.metadata.signal.metrics.entry })] }), _jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "TP:" }), _jsx("span", { className: "value green", children: item.metadata.signal.metrics.takeProfit })] }), _jsxs("div", { className: "signal-price-item", children: [_jsx("span", { className: "label", children: "SL:" }), _jsx("span", { className: "value red", children: item.metadata.signal.metrics.stopLoss })] })] })] })), ((_c = item.metadata.news) === null || _c === void 0 ? void 0 : _c.stocks) && (_jsx("div", { className: "feed-stocks", children: item.metadata.news.stocks.map((stock, idx) => (_jsxs("div", { className: "stock-card", children: [_jsx("span", { className: "stock-symbol", children: stock.symbol }), _jsx("span", { className: "stock-price", children: stock.price }), _jsx("span", { className: `stock-change ${stock.changeType}`, children: stock.change })] }, idx))) })), ((_d = item.metadata.ai_categorization) === null || _d === void 0 ? void 0 : _d.tags) && (_jsx("div", { className: "feed-tags", children: item.metadata.ai_categorization.tags.map((tag, idx) => (_jsxs("span", { className: "feed-tag", children: ["#", tag] }, idx))) }))] }, item.content_id));
273
- })] }));
321
+ }), loadingMore && (_jsx("div", { style: {
322
+ textAlign: 'center',
323
+ padding: '16px',
324
+ color: '#6b7280',
325
+ fontSize: '14px'
326
+ }, children: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0435\u0449\u0435..." })), !hasMore && !loading && items.length > 0 && (_jsx("div", { style: {
327
+ textAlign: 'center',
328
+ padding: '16px',
329
+ color: '#9ca3af',
330
+ fontSize: '12px'
331
+ }, children: "\u0412\u0441\u0435 \u043D\u043E\u0432\u043E\u0441\u0442\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u044B" }))] }));
274
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finsignal-feed-explore",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "News feed explorer components for React web applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",