refactor: Community 目录结构重组 + 修复导入路径 + 添加 Mock 数据

## 目录重构
- DynamicNewsCard/ → DynamicNews/(含 layouts/, hooks/ 子目录)
- EventCard 原子组件 → EventCard/atoms/
- EventDetailModal 独立目录化
- HotEvents 独立目录化(含 CSS)
- SearchFilters 独立目录化(CompactSearchBox, TradingTimeFilter)

## 导入路径修复
- EventCard/*.js: 统一使用 @constants/, @utils/, @components/ 别名
- atoms/*.js: 修复移动后的相对路径问题
- DynamicNewsCard.js: 更新 contexts, store, constants 导入
- EventHeaderInfo.js, CompactMetaBar.js: 修复 EventFollowButton 导入

## Mock Handler 添加
- /api/events/:eventId/expectation-score - 事件超预期得分
- /api/index/:indexCode/realtime - 指数实时行情

## 警告修复
- CitationMark.js: overlayInnerStyle → styles (Antd 5.x)
- CitedContent.js: 移除不支持的 jsx 属性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-09 13:16:43 +08:00
parent c704b12bce
commit 15f5c445c5
47 changed files with 409 additions and 76 deletions

View File

@@ -0,0 +1,695 @@
// src/views/Community/components/DynamicNews/DynamicNewsCard.js
// 横向滚动事件卡片组件(实时要闻·动态追踪)
import React, { forwardRef, useState, useEffect, useMemo, useCallback, useRef, useImperativeHandle } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Card,
CardHeader,
CardBody,
Box,
Flex,
VStack,
HStack,
Heading,
Text,
Badge,
Center,
Spinner,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useColorModeValue,
useToast,
useDisclosure,
Switch,
Tooltip,
Icon,
} from '@chakra-ui/react';
import { TimeIcon, BellIcon } from '@chakra-ui/icons';
import { useNotification } from '@contexts/NotificationContext';
import EventScrollList from './EventScrollList';
import ModeToggleButtons from './ModeToggleButtons';
import PaginationControl from './PaginationControl';
import DynamicNewsDetailPanel from '@components/EventDetailPanel';
import CompactSearchBox from '../SearchFilters/CompactSearchBox';
import {
fetchDynamicNews,
toggleEventFollow,
selectEventFollowStatus,
selectVerticalEventsWithLoading,
selectFourRowEventsWithLoading
} from '@store/slices/communityDataSlice';
import { usePagination } from './hooks/usePagination';
import { PAGINATION_CONFIG, DISPLAY_MODES, REFRESH_DEBOUNCE_DELAY } from './constants';
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
import { debounce } from '@utils/debounce';
import { useDevice } from '@hooks/useDevice';
// 🔍 调试:渲染计数器
let dynamicNewsCardRenderCount = 0;
/**
* 实时要闻·动态追踪 - 事件展示卡片组件
* @param {Object} filters - 筛选条件
* @param {Array} popularKeywords - 热门关键词
* @param {Date} lastUpdateTime - 最后更新时间
* @param {Function} onSearch - 搜索回调
* @param {Function} onSearchFocus - 搜索框获得焦点回调
* @param {Function} onEventClick - 事件点击回调
* @param {Function} onViewDetail - 查看详情回调
* @param {Object} trackingFunctions - PostHog 追踪函数集合
* @param {Object} ref - 用于滚动的ref
*/
const DynamicNewsCardComponent = forwardRef(({
filters = {},
popularKeywords = [],
lastUpdateTime,
onSearch,
onSearchFocus,
onEventClick,
onViewDetail,
trackingFunctions = {},
...rest
}, ref) => {
const dispatch = useDispatch();
const toast = useToast();
const cardBg = PROFESSIONAL_COLORS.background.card;
const borderColor = PROFESSIONAL_COLORS.border.default;
// 通知权限相关
const { browserPermission, requestBrowserPermission } = useNotification();
const { isMobile } = useDevice();
// Refs
const cardHeaderRef = useRef(null);
const cardBodyRef = useRef(null);
const virtualizedGridRef = useRef(null); // ⚡ VirtualizedFourRowGrid 的 ref用于获取滚动位置
// 从 Redux 读取关注状态
const eventFollowStatus = useSelector(selectEventFollowStatus);
// 本地状态:模式(先初始化,后面会被 usePagination 更新)
const [currentMode, setCurrentMode] = useState('vertical');
// 根据当前模式从 Redux 读取对应的数据(添加默认值避免 undefined
const verticalData = useSelector(selectVerticalEventsWithLoading) || {};
const fourRowData = useSelector(selectFourRowEventsWithLoading) || {};
// 🔍 调试:从 Redux 读取数据
console.log('%c[DynamicNewsCard] 从 Redux 读取数据', 'color: #3B82F6; font-weight: bold;', {
currentMode,
'verticalData.data type': typeof verticalData.data,
'verticalData.data keys': verticalData.data ? Object.keys(verticalData.data) : [],
'verticalData.total': verticalData.total,
'verticalData.cachedPageCount': verticalData.cachedPageCount,
'verticalData.loading': verticalData.loading,
'fourRowData.data?.length': fourRowData.data?.length || 0,
'fourRowData.total': fourRowData.total,
});
// 根据模式选择数据源(使用 useMemo 缓存,避免重复计算)
// 纵向模式data 是页码映射 { 1: [...], 2: [...] }
// 平铺模式data 是数组 [...]
const modeData = useMemo(
() => currentMode === 'four-row' ? fourRowData : verticalData,
[currentMode, fourRowData, verticalData]
);
const {
data = currentMode === 'vertical' ? {} : [], // 纵向是对象,平铺是数组
loading = false,
error = null,
pagination, // 分页元数据
total = 0, // 向后兼容
cachedCount = 0,
cachedPageCount = 0
} = modeData;
// 传递给 usePagination 的数据(使用 useMemo 缓存,避免重复计算)
const allCachedEventsByPage = useMemo(
() => currentMode === 'vertical' ? data : undefined,
[currentMode, data]
);
const allCachedEvents = useMemo(
() => currentMode === 'four-row' ? data : undefined,
[currentMode, data]
);
// 🔍 调试:选择的数据源
console.log('%c[DynamicNewsCard] 选择的数据源', 'color: #3B82F6; font-weight: bold;', {
mode: currentMode,
'allCachedEventsByPage': allCachedEventsByPage ? Object.keys(allCachedEventsByPage) : 'undefined',
'allCachedEvents?.length': allCachedEvents?.length,
total,
cachedCount,
cachedPageCount,
loading,
error
});
// 🔍 调试:记录每次渲染
dynamicNewsCardRenderCount++;
console.log(`%c🔍 [DynamicNewsCard] 渲染 #${dynamicNewsCardRenderCount} - mode=${currentMode}, allCachedEvents.length=${allCachedEvents?.length || 0}, total=${total}`, 'color: #FF9800; font-weight: bold; font-size: 14px;');
// 关注按钮点击处理
const handleToggleFollow = useCallback((eventId) => {
dispatch(toggleEventFollow(eventId));
}, [dispatch]);
// 通知开关处理
const handleNotificationToggle = useCallback(async () => {
if (browserPermission === 'granted') {
// 已授权,提示用户去浏览器设置中关闭
toast({
title: '已开启通知',
description: '要关闭通知,请在浏览器地址栏左侧点击锁图标,找到"通知"选项进行设置',
status: 'info',
duration: 5000,
isClosable: true,
});
} else {
// 未授权,请求权限
await requestBrowserPermission();
}
}, [browserPermission, requestBrowserPermission, toast]);
// 本地状态
const [selectedEvent, setSelectedEvent] = useState(null);
// 弹窗状态(用于四排模式)
const { isOpen: isModalOpen, onOpen: onModalOpen, onClose: onModalClose } = useDisclosure();
const [modalEvent, setModalEvent] = useState(null);
// 初始化标记 - 确保初始加载只执行一次
const hasInitialized = useRef(false);
// 追踪是否已自动选中过首个事件
const hasAutoSelectedFirstEvent = useRef(false);
// 追踪筛选条件 useEffect 是否是第一次渲染(避免初始加载时重复请求)
const isFirstRenderForFilters = useRef(true);
// 使用分页 Hook
const {
currentPage,
mode,
loadingPage,
pageSize,
totalPages,
hasMore,
currentPageEvents,
displayEvents, // 当前显示的事件列表
handlePageChange,
handleModeToggle,
loadNextPage, // 加载下一页
loadPrevPage // 加载上一页
} = usePagination({
allCachedEventsByPage, // 纵向模式:页码映射
allCachedEvents, // 平铺模式:数组
pagination, // 分页元数据对象
total, // 向后兼容
cachedCount,
dispatch,
toast,
filters, // 传递筛选条件
initialMode: currentMode // 传递当前显示模式
});
// 同步 mode 到 currentMode
useEffect(() => {
setCurrentMode(mode);
}, [mode]);
/**
* ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑)
*
* 此函数会被 debounce 包装,避免短时间内频繁刷新
*/
const executeRefresh = useCallback(() => {
const state = {
mode,
currentPage: pagination?.current_page || 1,
};
console.log('[DynamicNewsCard] ⏰ executeRefresh() 执行(防抖延迟后)', state);
if (mode === 'vertical') {
// ========== 纵向模式 ==========
// 只在第1页时刷新避免打断用户浏览其他页
if (state.currentPage === 1) {
console.log('[DynamicNewsCard] 纵向模式 + 第1页 → 强制刷新列表');
handlePageChange(1, true); // ⚡ 传递 force = true强制刷新第1页
toast({
title: '检测到新事件',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
console.log(`[DynamicNewsCard] 纵向模式 + 第${state.currentPage}页 → 不刷新(避免打断用户)`);
}
} else if (mode === 'four-row') {
// ========== 平铺模式 ==========
// 检查滚动位置,只有在顶部时才刷新
const scrollPos = virtualizedGridRef.current?.getScrollPosition();
if (scrollPos?.isNearTop) {
// 用户在顶部 10% 区域,安全刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动在顶部 → 刷新列表');
handlePageChange(1); // 清空并刷新
toast({
title: '检测到新事件,已刷新',
status: 'info',
duration: 2000,
isClosable: true,
});
} else {
// 用户不在顶部,显示提示但不自动刷新
console.log('[DynamicNewsCard] 平铺模式 + 滚动不在顶部 → 仅提示,不刷新');
toast({
title: '有新事件发布',
description: '滚动到顶部查看',
status: 'info',
duration: 3000,
isClosable: true,
});
}
}
}, [mode, pagination, handlePageChange, toast]);
/**
* ⚡【防抖包装】创建防抖版本的刷新函数
*
* 使用 useMemo 确保防抖函数在 executeRefresh 不变时保持引用稳定
* 防抖延迟REFRESH_DEBOUNCE_DELAY (2000ms)
*
* 效果:短时间内收到多个新事件,只执行最后一次刷新
*/
const debouncedRefresh = useMemo(
() => debounce(executeRefresh, REFRESH_DEBOUNCE_DELAY),
[executeRefresh]
);
/**
* ⚡ 暴露方法给父组件(用于 Socket 自动刷新)
*/
useImperativeHandle(ref, () => ({
/**
* 智能刷新方法(带防抖,避免频繁刷新)
*
* 调用此方法时:
* 1. 清除之前的定时器(如果有)
* 2. 设置新的定时器(延迟 REFRESH_DEBOUNCE_DELAY 后执行)
* 3. 如果在延迟期间再次调用,重复步骤 1-2
* 4. 只有最后一次调用会在延迟后实际执行 executeRefresh()
*/
refresh: () => {
console.log('[DynamicNewsCard] 🔔 refresh() 被调用(设置防抖定时器)', {
mode,
currentPage: pagination?.current_page || 1,
debounceDelay: `${REFRESH_DEBOUNCE_DELAY}ms`,
});
// 调用防抖包装后的函数
debouncedRefresh();
},
/**
* 获取当前状态(用于调试)
*/
getState: () => ({
mode,
currentPage: pagination?.current_page || 1,
totalPages: pagination?.total_pages || 1,
total: pagination?.total || 0,
loading,
}),
}), [mode, pagination, loading, debouncedRefresh]);
/**
* ⚡【清理逻辑】组件卸载时取消待执行的防抖函数
*
* 作用:避免组件卸载后仍然执行刷新操作(防止内存泄漏和潜在错误)
*/
useEffect(() => {
return () => {
console.log('[DynamicNewsCard] 🧹 组件卸载,取消待执行的防抖刷新');
debouncedRefresh.cancel();
};
}, [debouncedRefresh]);
// 监听 error 状态,显示空数据提示
useEffect(() => {
if (error && error.includes('暂无更多数据')) {
toast({
title: '提示',
description: error,
status: 'info',
duration: 2000,
isClosable: true,
});
}
}, [error, toast]);
// 四排模式的事件点击处理(打开弹窗)
const handleFourRowEventClick = useCallback((event) => {
console.log('%c🔲 [四排模式] 点击事件,打开详情弹窗', 'color: #8B5CF6; font-weight: bold;', { eventId: event.id, title: event.title });
// 🎯 追踪事件详情打开
if (trackingFunctions.trackNewsDetailOpened) {
trackingFunctions.trackNewsDetailOpened({
eventId: event.id,
eventTitle: event.title,
importance: event.importance,
source: 'four_row_mode',
displayMode: 'modal',
timestamp: new Date().toISOString(),
});
}
setModalEvent(event);
onModalOpen();
}, [onModalOpen, trackingFunctions]);
// 初始加载 - 只在组件首次挂载且对应模式数据为空时执行
useEffect(() => {
// 添加防抖:如果已经初始化,不再执行
if (hasInitialized.current) return;
const isDataEmpty = currentMode === 'vertical'
? Object.keys(allCachedEventsByPage || {}).length === 0
: (allCachedEvents?.length || 0) === 0;
if (isDataEmpty) {
hasInitialized.current = true;
dispatch(fetchDynamicNews({
mode: mode, // 传递当前模式
per_page: pageSize,
pageSize: pageSize, // 传递 pageSize 确保索引计算一致
clearCache: true,
...filters, // 先展开筛选条件
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
}));
}
}, [dispatch, currentMode, mode, pageSize]); // 移除 allCachedEventsByPage, allCachedEvents 依赖,避免数据更新触发重复请求
// 监听筛选条件变化 - 清空缓存并重新请求数据
useEffect(() => {
// 跳过初始加载(由上面的 useEffect 处理)
if (!hasInitialized.current) return;
// 跳过第一次渲染(避免与初始加载 useEffect 重复)
if (isFirstRenderForFilters.current) {
isFirstRenderForFilters.current = false;
return;
}
console.log('%c🔍 [筛选] 筛选条件改变,重新请求数据', 'color: #8B5CF6; font-weight: bold;', filters);
// 筛选条件改变时清空对应模式的缓存并从第1页开始加载
dispatch(fetchDynamicNews({
mode: mode, // 传递当前模式
per_page: pageSize,
pageSize: pageSize,
clearCache: true, // 清空缓存
...filters, // 先展开筛选条件
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
}));
}, [
filters.sort,
filters.importance,
filters.q,
filters.start_date, // 时间筛选参数:开始时间
filters.end_date, // 时间筛选参数:结束时间
filters.recent_days, // 时间筛选参数近N天
filters.industry_code,
filters._forceRefresh, // 强制刷新标志(用于重置按钮)
mode, // 添加 mode 到依赖
pageSize, // 添加 pageSize 到依赖
dispatch
]); // 只监听筛选参数的变化,不监听 page
// 监听模式切换 - 如果新模式数据为空,请求数据
useEffect(() => {
const isDataEmpty = currentMode === 'vertical'
? Object.keys(allCachedEventsByPage || {}).length === 0
: (allCachedEvents?.length || 0) === 0;
if (hasInitialized.current && isDataEmpty) {
console.log(`%c🔄 [模式切换] ${mode} 模式数据为空,开始加载`, 'color: #8B5CF6; font-weight: bold;');
// 🔧 根据 mode 直接计算 per_page避免使用可能过时的 pageSize prop
const modePageSize = mode === DISPLAY_MODES.FOUR_ROW
? PAGINATION_CONFIG.FOUR_ROW_PAGE_SIZE // 30
: PAGINATION_CONFIG.VERTICAL_PAGE_SIZE; // 10
console.log(`%c 计算的 per_page: ${modePageSize} (mode: ${mode})`, 'color: #8B5CF6;');
dispatch(fetchDynamicNews({
mode: mode,
per_page: modePageSize, // 使用计算的值,不是 pageSize prop
pageSize: modePageSize,
clearCache: true,
...filters, // 先展开筛选条件
page: PAGINATION_CONFIG.INITIAL_PAGE, // 然后覆盖 page 参数
}));
}
}, [mode, currentMode, allCachedEventsByPage, allCachedEvents, dispatch]); // 移除 filters 依赖,避免与筛选 useEffect 循环触发 // 添加所有依赖
// 自动选中逻辑 - 只在首次加载时自动选中第一个事件,翻页时不自动选中
useEffect(() => {
if (currentPageEvents.length > 0) {
// 情况1: 首次加载 - 自动选中第一个事件并触发详情加载
if (!hasAutoSelectedFirstEvent.current && !selectedEvent) {
console.log('%c🎯 [首次加载] 自动选中第一个事件', 'color: #10B981; font-weight: bold;');
hasAutoSelectedFirstEvent.current = true;
setSelectedEvent(currentPageEvents[0]);
// 🎯 追踪事件点击(首次自动选中)
if (trackingFunctions.trackNewsArticleClicked) {
trackingFunctions.trackNewsArticleClicked({
eventId: currentPageEvents[0].id,
eventTitle: currentPageEvents[0].title,
importance: currentPageEvents[0].importance,
source: 'auto_select_first',
displayMode: mode,
timestamp: new Date().toISOString(),
});
}
return;
}
// 情况2: 翻页 - 如果选中的事件不在当前页,根据模式决定处理方式
const selectedEventInCurrentPage = currentPageEvents.find(
e => e.id === selectedEvent?.id
);
}
}, [currentPageEvents, selectedEvent?.id, mode, trackingFunctions]);
// 组件卸载时清理选中状态
useEffect(() => {
return () => {
setSelectedEvent(null);
};
}, []);
// 页码切换时滚动到顶部
const handlePageChangeWithScroll = useCallback((page) => {
// 先切换页码
handlePageChange(page);
// 延迟一帧确保DOM更新完成后再滚动
requestAnimationFrame(() => {
// 查找所有标记为滚动容器的元素
const containers = document.querySelectorAll('[data-scroll-container]');
containers.forEach(container => {
container.scrollTo({ top: 0, behavior: 'smooth' });
});
console.log('📜 页码切换,滚动到顶部', { containersFound: containers.length });
});
}, [handlePageChange]);
// 测量 CardHeader 高度
// 固定模式逻辑已移除,改用 sticky 定位
return (
<Card
ref={ref}
{...rest}
bg={cardBg}
borderColor={borderColor}
mb={4}
position="relative"
zIndex={1}
animation="fadeInUp 0.8s ease-out 0.2s both"
sx={{
'@keyframes fadeInUp': {
'0%': { opacity: 0, transform: 'translateY(30px)' },
'100%': { opacity: 1, transform: 'translateY(0)' }
}
}}
>
{/* 标题和搜索部分 - 优化版 */}
<CardHeader
ref={cardHeaderRef}
position="relative"
zIndex={1}
pb={3}
px={isMobile ? 2 : undefined}
>
<VStack spacing={3} align="stretch">
{/* 第一行:标题 + 模式切换 + 通知开关 + 更新时间 */}
<Flex justify="space-between" align="center">
{/* 左侧:标题 + 模式切换按钮 */}
<HStack spacing={4}>
<Heading size={isMobile ? "sm" : "md"} color={PROFESSIONAL_COLORS.text.primary}>
<HStack spacing={2}>
<TimeIcon color={PROFESSIONAL_COLORS.gold[500]} />
<Text bgGradient={PROFESSIONAL_COLORS.gradients.gold} bgClip="text">实时要闻·动态追踪</Text>
</HStack>
</Heading>
{/* 模式切换按钮(移动端隐藏) */}
{!isMobile && <ModeToggleButtons mode={mode} onModeChange={handleModeToggle} />}
</HStack>
{/* 右侧:通知开关 + 更新时间 */}
<HStack spacing={3}>
{/* 通知开关 - 移动端隐藏 */}
{!isMobile && (
<HStack
spacing={2}
cursor="pointer"
onClick={handleNotificationToggle}
_hover={{ opacity: 0.8 }}
transition="opacity 0.2s"
>
<Icon
as={BellIcon}
boxSize={3.5}
color={PROFESSIONAL_COLORS.gold[500]}
/>
<Text
fontSize="sm"
color={PROFESSIONAL_COLORS.text.secondary}
>
实时消息推送{browserPermission === 'granted' ? '已开启' : '未开启'}
</Text>
<Switch
size="sm"
isChecked={browserPermission === 'granted'}
pointerEvents="none"
colorScheme="yellow"
/>
</HStack>
)}
{/* 更新时间 */}
<Text fontSize="xs" color={PROFESSIONAL_COLORS.text.secondary} whiteSpace="nowrap">
最后更新: {lastUpdateTime?.toLocaleTimeString() || '--'}
</Text>
</HStack>
</Flex>
{/* 第二行:筛选组件 */}
<Box>
<CompactSearchBox
onSearch={onSearch}
onSearchFocus={onSearchFocus}
filters={filters}
mode={mode}
pageSize={pageSize}
trackingFunctions={trackingFunctions}
isMobile={isMobile}
/>
</Box>
</VStack>
</CardHeader>
{/* 主体内容 */}
<CardBody
ref={cardBodyRef}
position="relative"
pt={0}
px={0}
mx={0}
display="flex"
flexDirection="column"
overflow="visible"
zIndex={1}
>
{/* 内容区域 - 撑满剩余高度 */}
<Box flex="1" minH={0} position="relative">
{/* Loading 蒙层 - 数据请求时显示 */}
{loading && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(26, 31, 46, 0.95)"
display="flex"
alignItems="center"
justifyContent="center"
zIndex={10}
borderRadius="md"
>
<VStack spacing={3}>
<Spinner size="xl" color={PROFESSIONAL_COLORS.gold[500]} thickness="4px" />
<Text color={PROFESSIONAL_COLORS.text.primary} fontWeight="medium">
正在加载最新事件...
</Text>
</VStack>
</Box>
)}
{/* 列表内容 - 始终渲染 */}
<EventScrollList
events={currentPageEvents}
displayEvents={displayEvents} // 累积显示的事件列表(平铺模式)
loadNextPage={loadNextPage} // 加载下一页
loadPrevPage={loadPrevPage} // 加载上一页
onFourRowEventClick={handleFourRowEventClick} // 四排模式事件点击
selectedEvent={selectedEvent}
onEventSelect={setSelectedEvent}
borderColor={borderColor}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChangeWithScroll}
loading={loadingPage !== null}
error={error}
mode={mode}
eventFollowStatus={eventFollowStatus}
onToggleFollow={handleToggleFollow}
hasMore={hasMore}
virtualizedGridRef={virtualizedGridRef} // ⚡ 传递 ref 给 VirtualizedFourRowGrid
/>
</Box>
</CardBody>
{/* 四排模式详情弹窗 - 未打开时不渲染 */}
{isModalOpen && (
<Modal isOpen={isModalOpen} onClose={onModalClose} size="full" scrollBehavior="inside">
<ModalOverlay />
<ModalContent maxW="1600px" mx="auto" my={8}>
<ModalHeader>
{modalEvent?.title || '事件详情'}
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
{modalEvent && <DynamicNewsDetailPanel event={modalEvent} />}
</ModalBody>
</ModalContent>
</Modal>
)}
</Card>
);
});
DynamicNewsCardComponent.displayName = 'DynamicNewsCard';
// ⚡ 使用 React.memo 优化性能(减少不必要的重渲染)
const DynamicNewsCard = React.memo(DynamicNewsCardComponent);
export default DynamicNewsCard;