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.
- package/dist/NewsFeed.js +83 -25
- 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
|
-
|
|
196
|
-
React.
|
|
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
|
|
207
|
+
const currentCursor = isInitial ? undefined : cursor;
|
|
208
|
+
const limit = isInitial ? 20 : 20; // initialLoad: 20, pageSize: 20
|
|
209
|
+
if (isInitial) {
|
|
202
210
|
setLoading(true);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
}
|