145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Card, Typography, Segmented, Button, Spin, Flex } from 'antd';
|
|
import {
|
|
CalendarOutlined,
|
|
MailOutlined,
|
|
CompassOutlined,
|
|
UserAddOutlined,
|
|
MessageOutlined,
|
|
HistoryOutlined,
|
|
} from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
import { api } from '@/lib/api';
|
|
import type { ActivityFeedResult, ActivityItem } from '@/types/api';
|
|
|
|
dayjs.extend(relativeTime);
|
|
|
|
const { Text } = Typography;
|
|
|
|
const TYPE_CONFIG: Record<ActivityItem['type'], { color: string; icon: React.ReactNode }> = {
|
|
shift_signup: { color: '#eb2f96', icon: <CalendarOutlined style={{ fontSize: 13 }} /> },
|
|
response_submitted: { color: '#faad14', icon: <MessageOutlined style={{ fontSize: 13 }} /> },
|
|
canvass_completed: { color: '#52c41a', icon: <CompassOutlined style={{ fontSize: 13 }} /> },
|
|
email_sent: { color: '#1890ff', icon: <MailOutlined style={{ fontSize: 13 }} /> },
|
|
user_created: { color: '#722ed1', icon: <UserAddOutlined style={{ fontSize: 13 }} /> },
|
|
};
|
|
|
|
const MODULE_OPTIONS = [
|
|
{ label: 'All', value: 'all' },
|
|
{ label: 'Map', value: 'map' },
|
|
{ label: 'Influence', value: 'influence' },
|
|
{ label: 'Users', value: 'users' },
|
|
];
|
|
|
|
function ActivityRow({ item }: { item: ActivityItem }) {
|
|
const config = TYPE_CONFIG[item.type];
|
|
return (
|
|
<Flex
|
|
align="center"
|
|
gap={8}
|
|
style={{
|
|
padding: '5px 0',
|
|
borderBottom: '1px solid rgba(255,255,255,0.04)',
|
|
lineHeight: 1.4,
|
|
}}
|
|
>
|
|
<span style={{ color: config.color, flexShrink: 0, width: 18, textAlign: 'center' }}>{config.icon}</span>
|
|
<Text strong style={{ fontSize: 13, flexShrink: 0 }}>{item.title}</Text>
|
|
<Text
|
|
type="secondary"
|
|
style={{
|
|
fontSize: 13,
|
|
flex: 1,
|
|
minWidth: 0,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{item.description}
|
|
</Text>
|
|
<Text type="secondary" style={{ fontSize: 11, flexShrink: 0, whiteSpace: 'nowrap' }}>
|
|
{dayjs(item.timestamp).fromNow(true)}
|
|
</Text>
|
|
</Flex>
|
|
);
|
|
}
|
|
|
|
export default function ActivityFeedCard() {
|
|
const [items, setItems] = useState<ActivityItem[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [module, setModule] = useState('all');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fetchActivity = useCallback(async (p: number, mod: string, append: boolean) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.get<ActivityFeedResult>('/dashboard/activity', {
|
|
params: { page: p, limit: 15, module: mod },
|
|
});
|
|
if (append) {
|
|
setItems(prev => [...prev, ...res.data.items]);
|
|
} else {
|
|
setItems(res.data.items);
|
|
}
|
|
setTotal(res.data.total);
|
|
} catch {
|
|
// non-critical
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setPage(1);
|
|
fetchActivity(1, module, false);
|
|
}, [module, fetchActivity]);
|
|
|
|
const handleLoadMore = () => {
|
|
const next = page + 1;
|
|
setPage(next);
|
|
fetchActivity(next, module, true);
|
|
};
|
|
|
|
const hasMore = items.length < total;
|
|
|
|
return (
|
|
<Card
|
|
title={<span style={{ fontSize: 14 }}><HistoryOutlined style={{ marginRight: 6, fontSize: 15 }} />Recent Activity</span>}
|
|
size="small"
|
|
extra={
|
|
<Segmented
|
|
size="small"
|
|
value={module}
|
|
onChange={(val) => setModule(val as string)}
|
|
options={MODULE_OPTIONS}
|
|
/>
|
|
}
|
|
styles={{ body: { padding: '6px 14px 8px' } }}
|
|
>
|
|
{loading && items.length === 0 ? (
|
|
<div style={{ textAlign: 'center', padding: 12 }}><Spin size="small" /></div>
|
|
) : items.length === 0 ? (
|
|
<Text type="secondary" style={{ fontSize: 13, display: 'block', padding: '8px 0' }}>No recent activity</Text>
|
|
) : (
|
|
<>
|
|
<div>
|
|
{items.map(item => (
|
|
<ActivityRow key={item.id} item={item} />
|
|
))}
|
|
</div>
|
|
{hasMore && (
|
|
<div style={{ textAlign: 'center', paddingTop: 4 }}>
|
|
<Button size="small" type="link" onClick={handleLoadMore} loading={loading} style={{ fontSize: 13 }}>
|
|
Load more
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|