merrycoral 78cc41b453 이벤트 엔티티 및 페이지 기능 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 13:22:26 +09:00

921 lines
36 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import {
Box,
Container,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Card,
CardContent,
Typography,
InputAdornment,
Pagination,
Grid,
LinearProgress,
Chip,
} from '@mui/material';
import {
Search,
FilterList,
Event,
TrendingUp,
People,
CardGiftcard,
Phone,
Share,
ShoppingCart,
Email,
LocalFireDepartment,
NewReleases,
Warning,
Star,
} from '@mui/icons-material';
import Header from '@/shared/ui/Header';
import { cardStyles, colors, responsiveText } from '@/shared/lib/button-styles';
import { useEvents } from '@/entities/event/model/useEvents';
import type { EventStatus as ApiEventStatus } from '@/entities/event/model/types';
// ==================== API 연동 ====================
// Mock 데이터를 실제 API 호출로 교체
// 백업 파일: page.tsx.backup
type EventStatus = 'all' | 'active' | 'scheduled' | 'ended';
type Period = '1month' | '3months' | '6months' | '1year' | 'all';
type SortBy = 'latest' | 'participants' | 'roi';
export default function EventsPage() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<EventStatus>('all');
const [periodFilter, setPeriodFilter] = useState<Period>('1month');
const [sortBy, setSortBy] = useState<SortBy>('latest');
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;
// API 데이터 가져오기
const { events: apiEvents, loading, error, pageInfo, refetch } = useEvents({
page: currentPage - 1,
size: itemsPerPage,
sort: 'createdAt',
order: 'desc'
});
// API 상태를 UI 상태로 매핑
const mapApiStatus = (apiStatus: ApiEventStatus): EventStatus => {
switch (apiStatus) {
case 'PUBLISHED':
return 'active';
case 'DRAFT':
return 'scheduled';
case 'ENDED':
return 'ended';
default:
return 'all';
}
};
// API 이벤트를 UI 형식으로 변환
const transformedEvents = apiEvents.map(event => ({
id: event.eventId,
title: event.eventName || '제목 없음',
status: mapApiStatus(event.status),
startDate: event.startDate ? new Date(event.startDate).toLocaleDateString('ko-KR') : '-',
endDate: event.endDate ? new Date(event.endDate).toLocaleDateString('ko-KR') : '-',
prize: event.aiRecommendations[0]?.reward || '경품 정보 없음',
method: event.aiRecommendations[0]?.participationMethod || '참여 방법 없음',
participants: event.participants || 0,
targetParticipants: event.targetParticipants || 0,
roi: event.roi || 0,
daysLeft: event.endDate
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
: 0,
isUrgent: event.endDate
? Math.ceil((new Date(event.endDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) <= 3
: false,
isPopular: event.participants && event.targetParticipants
? (event.participants / event.targetParticipants) >= 0.8
: false,
isHighROI: event.roi ? event.roi >= 300 : false,
isNew: event.createdAt
? (Date.now() - new Date(event.createdAt).getTime()) < (7 * 24 * 60 * 60 * 1000)
: false,
}));
// 필터링 및 정렬
const filteredEvents = transformedEvents
.filter((event) => {
const matchesSearch = event.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || event.status === statusFilter;
return matchesSearch && matchesStatus;
})
.sort((a, b) => {
if (sortBy === 'latest') {
return new Date(b.startDate).getTime() - new Date(a.startDate).getTime();
} else if (sortBy === 'participants') {
return b.participants - a.participants;
} else if (sortBy === 'roi') {
return b.roi - a.roi;
}
return 0;
});
// 페이지네이션
const totalPages = Math.ceil(filteredEvents.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, filteredEvents.length);
const pageEvents = filteredEvents.slice(startIndex, endIndex);
const handleEventClick = (eventId: string) => {
router.push(`/events/${eventId}`);
};
const getStatusStyle = (status: string) => {
switch (status) {
case 'active':
return {
bgcolor: colors.mint,
color: 'white',
};
case 'scheduled':
return {
bgcolor: colors.blue,
color: 'white',
};
case 'ended':
return {
bgcolor: colors.gray[300],
color: colors.gray[700],
};
default:
return {
bgcolor: colors.gray[200],
color: colors.gray[600],
};
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active':
return '진행중';
case 'scheduled':
return '예정';
case 'ended':
return '종료';
default:
return status;
}
};
const getMethodIcon = (method: string) => {
switch (method) {
case '전화번호 입력':
return <Phone sx={{ fontSize: 18 }} />;
case 'SNS 팔로우':
return <Share sx={{ fontSize: 18 }} />;
case '구매 인증':
return <ShoppingCart sx={{ fontSize: 18 }} />;
case '이메일 등록':
return <Email sx={{ fontSize: 18 }} />;
default:
return <Event sx={{ fontSize: 18 }} />;
}
};
const calculateProgress = (event: typeof transformedEvents[0]) => {
if (event.status !== 'active') return 0;
const startTime = new Date(event.startDate).getTime();
const endTime = new Date(event.endDate).getTime();
const total = endTime - startTime;
const elapsed = Date.now() - startTime;
return Math.min(Math.max((elapsed / total) * 100, 0), 100);
};
// 통계 계산
const stats = {
total: transformedEvents.length,
active: transformedEvents.filter((e) => e.status === 'active').length,
totalParticipants: transformedEvents.reduce((sum, e) => sum + e.participants, 0),
avgROI: transformedEvents.filter((e) => e.roi > 0).length > 0
? Math.round(
transformedEvents.filter((e) => e.roi > 0).reduce((sum, e) => sum + e.roi, 0) /
transformedEvents.filter((e) => e.roi > 0).length
)
: 0,
};
return (
<>
<Header title="이벤트 목록" showBack={true} showMenu={false} showProfile={true} />
<Box
sx={{
pt: { xs: 7, sm: 8 },
pb: 10,
bgcolor: colors.gray[50],
minHeight: '100vh',
}}
>
<Container
maxWidth="lg"
sx={{ pt: { xs: 4, sm: 8 }, pb: { xs: 4, sm: 6 }, px: { xs: 3, sm: 6, md: 10 } }}
>
{/* Loading State */}
{loading && (
<Box sx={{ mb: 4 }}>
<LinearProgress sx={{ borderRadius: 1 }} />
<Typography
sx={{
mt: 2,
textAlign: 'center',
color: colors.gray[600],
fontSize: { xs: '0.875rem', sm: '1rem' },
}}
>
...
</Typography>
</Box>
)}
{/* Error State */}
{error && (
<Card elevation={0} sx={{ ...cardStyles.default, mb: 4, bgcolor: '#FEE2E2' }}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Warning sx={{ fontSize: 48, color: '#DC2626', mb: 2 }} />
<Typography
variant="h6"
sx={{ mb: 1, color: '#991B1B', fontSize: { xs: '1rem', sm: '1.25rem' } }}
>
</Typography>
<Typography variant="body2" sx={{ color: '#7F1D1D', mb: 2 }}>
{error.message}
</Typography>
<Box
component="button"
onClick={() => refetch()}
sx={{
px: 3,
py: 1.5,
borderRadius: 2,
border: 'none',
bgcolor: '#DC2626',
color: 'white',
fontSize: '0.875rem',
fontWeight: 600,
cursor: 'pointer',
'&:hover': { bgcolor: '#B91C1C' },
}}
>
</Box>
</CardContent>
</Card>
)}
{/* Summary Statistics */}
<Grid container spacing={{ xs: 2, sm: 4 }} sx={{ mb: { xs: 4, sm: 8 } }}>
<Grid item xs={6} sm={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.purpleLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
>
<Event
sx={{
fontSize: { xs: 24, sm: 32 },
color: colors.gray[900],
mb: 1,
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}}
/>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.25rem', sm: '1.75rem' },
mb: 0.5,
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{stats.total}
</Typography>
<Typography
variant="body2"
sx={{
color: colors.gray[700],
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}
>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} sm={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.mint} 0%, ${colors.mintLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
>
<LocalFireDepartment
sx={{
fontSize: { xs: 24, sm: 32 },
color: colors.gray[900],
mb: 1,
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}}
/>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.25rem', sm: '1.75rem' },
mb: 0.5,
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{stats.active}
</Typography>
<Typography
variant="body2"
sx={{
color: colors.gray[700],
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}
>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} sm={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.blue} 0%, ${colors.blueLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
>
<People
sx={{
fontSize: { xs: 24, sm: 32 },
color: colors.gray[900],
mb: 1,
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}}
/>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.25rem', sm: '1.75rem' },
mb: 0.5,
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{stats.totalParticipants}
</Typography>
<Typography
variant="body2"
sx={{
color: colors.gray[700],
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}
>
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={6} sm={3}>
<Card
elevation={0}
sx={{
...cardStyles.default,
background: `linear-gradient(135deg, ${colors.orange} 0%, ${colors.orangeLight} 100%)`,
borderColor: 'transparent',
}}
>
<CardContent
sx={{ textAlign: 'center', py: { xs: 3, sm: 4 }, px: { xs: 2, sm: 3 } }}
>
<TrendingUp
sx={{
fontSize: { xs: 24, sm: 32 },
color: colors.gray[900],
mb: 1,
filter: 'drop-shadow(0px 2px 4px rgba(0,0,0,0.2))',
}}
/>
<Typography
variant="h4"
sx={{
fontWeight: 700,
color: colors.gray[900],
fontSize: { xs: '1.25rem', sm: '1.75rem' },
mb: 0.5,
textShadow: '0px 2px 4px rgba(0,0,0,0.15)',
}}
>
{stats.avgROI}%
</Typography>
<Typography
variant="body2"
sx={{
color: colors.gray[700],
fontSize: { xs: '0.6875rem', sm: '0.875rem' },
textShadow: '0px 1px 2px rgba(0,0,0,0.1)',
}}
>
ROI
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Search Section */}
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<TextField
fullWidth
placeholder="이벤트명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search sx={{ color: colors.gray[400], fontSize: { xs: 20, sm: 24 } }} />
</InputAdornment>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
bgcolor: 'white',
fontSize: { xs: '0.875rem', sm: '1rem' },
'& fieldset': {
borderColor: colors.gray[200],
},
'&:hover fieldset': {
borderColor: colors.gray[300],
},
'&.Mui-focused fieldset': {
borderColor: colors.purple,
},
},
}}
/>
</Box>
{/* Filters */}
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 2, sm: 4 }, flexWrap: 'wrap' }}>
<FormControl sx={{ flex: 1, minWidth: 120 }}>
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></InputLabel>
<Select
value={statusFilter}
label="상태"
onChange={(e) => setStatusFilter(e.target.value as EventStatus)}
sx={{
borderRadius: 2,
bgcolor: 'white',
fontSize: { xs: '0.875rem', sm: '1rem' },
'& .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[200],
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[300],
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: colors.purple,
},
}}
>
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="active" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="scheduled" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
<MenuItem value="ended" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
</Select>
</FormControl>
<FormControl sx={{ flex: 1, minWidth: 140 }}>
<InputLabel sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></InputLabel>
<Select
value={periodFilter}
label="기간"
onChange={(e) => setPeriodFilter(e.target.value as Period)}
sx={{
borderRadius: 2,
bgcolor: 'white',
fontSize: { xs: '0.875rem', sm: '1rem' },
'& .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[200],
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[300],
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: colors.purple,
},
}}
>
<MenuItem value="1month" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> 1</MenuItem>
<MenuItem value="3months" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> 3</MenuItem>
<MenuItem value="6months" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> 6</MenuItem>
<MenuItem value="1year" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}> 1</MenuItem>
<MenuItem value="all" sx={{ fontSize: { xs: '0.875rem', sm: '1rem' } }}></MenuItem>
</Select>
</FormControl>
</Box>
</Box>
{/* Sorting */}
<Box sx={{ mb: { xs: 4, sm: 8 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2" sx={{ ...responsiveText.body2, fontWeight: 600 }}>
</Typography>
<FormControl sx={{ width: { xs: 140, sm: 160 } }}>
<Select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortBy)}
size="small"
sx={{
borderRadius: 2,
bgcolor: 'white',
fontSize: { xs: '0.8125rem', sm: '0.875rem' },
'& .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[200],
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: colors.gray[300],
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: colors.purple,
},
}}
>
<MenuItem value="latest" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}></MenuItem>
<MenuItem value="participants" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}></MenuItem>
<MenuItem value="roi" sx={{ fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}></MenuItem>
</Select>
</FormControl>
</Box>
</Box>
{/* Event List */}
<Box sx={{ mb: { xs: 5, sm: 10 } }}>
{pageEvents.length === 0 ? (
<Card
elevation={0}
sx={{
...cardStyles.default,
}}
>
<CardContent sx={{ textAlign: 'center', py: { xs: 10, sm: 20 } }}>
<Box sx={{ color: colors.gray[300], mb: { xs: 2, sm: 3 } }}>
<Box
component="span"
className="material-icons"
sx={{ fontSize: { xs: 48, sm: 72 } }}
>
event_busy
</Box>
</Box>
<Typography variant="h6" sx={{ mb: { xs: 1, sm: 2 }, color: colors.gray[700], fontSize: { xs: '1rem', sm: '1.25rem' } }}>
</Typography>
<Typography variant="body2" sx={{ color: colors.gray[500], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}>
</Typography>
</CardContent>
</Card>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { xs: 3, sm: 6 } }}>
{pageEvents.map((event) => (
<Card
key={event.id}
elevation={0}
sx={{
...cardStyles.clickable,
}}
onClick={() => handleEventClick(event.id)}
>
<CardContent sx={{ p: { xs: 3, sm: 6, md: 8 } }}>
{/* Header with Badges */}
<Box sx={{ mb: { xs: 2, sm: 4 } }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'start',
mb: { xs: 2, sm: 3 },
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1.5, sm: 0 },
}}
>
<Typography
variant="h6"
sx={{ fontWeight: 700, color: colors.gray[900], flex: 1, fontSize: { xs: '1rem', sm: '1.25rem' } }}
>
{event.title}
</Typography>
<Box
sx={{
px: { xs: 2, sm: 2.5 },
py: { xs: 0.5, sm: 0.75 },
borderRadius: 2,
fontSize: { xs: '0.75rem', sm: '0.875rem' },
fontWeight: 600,
...getStatusStyle(event.status),
}}
>
{getStatusText(event.status)}
{event.status === 'active'
? ` | D-${event.daysLeft}`
: event.status === 'scheduled'
? ` | D+${event.daysLeft}`
: ''}
</Box>
</Box>
{/* Status Badges */}
<Box sx={{ display: 'flex', gap: { xs: 1, sm: 1.5 }, flexWrap: 'wrap' }}>
{event.isUrgent && (
<Chip
icon={<Warning sx={{ fontSize: { xs: 14, sm: 16 } }} />}
label="마감임박"
size="small"
sx={{
bgcolor: '#FEF3C7',
color: '#92400E',
fontWeight: 600,
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
height: { xs: 24, sm: 28 },
'& .MuiChip-icon': { color: '#92400E' },
}}
/>
)}
{event.isPopular && (
<Chip
icon={<LocalFireDepartment sx={{ fontSize: { xs: 14, sm: 16 } }} />}
label="인기"
size="small"
sx={{
bgcolor: '#FEE2E2',
color: '#991B1B',
fontWeight: 600,
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
height: { xs: 24, sm: 28 },
'& .MuiChip-icon': { color: '#991B1B' },
}}
/>
)}
{event.isHighROI && (
<Chip
icon={<Star sx={{ fontSize: { xs: 14, sm: 16 } }} />}
label="높은 ROI"
size="small"
sx={{
bgcolor: '#DCFCE7',
color: '#166534',
fontWeight: 600,
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
height: { xs: 24, sm: 28 },
'& .MuiChip-icon': { color: '#166534' },
}}
/>
)}
{event.isNew && (
<Chip
icon={<NewReleases sx={{ fontSize: { xs: 14, sm: 16 } }} />}
label="신규"
size="small"
sx={{
bgcolor: '#DBEAFE',
color: '#1E40AF',
fontWeight: 600,
fontSize: { xs: '0.6875rem', sm: '0.75rem' },
height: { xs: 24, sm: 28 },
'& .MuiChip-icon': { color: '#1E40AF' },
}}
/>
)}
</Box>
</Box>
{/* Progress Bar for Active Events */}
{event.status === 'active' && (
<Box sx={{ mb: { xs: 2, sm: 4 } }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 1,
}}
>
<Typography
variant="caption"
sx={{ color: colors.gray[600], fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
>
</Typography>
<Typography
variant="caption"
sx={{ color: colors.gray[700], fontWeight: 600, fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
>
{Math.round(calculateProgress(event))}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={calculateProgress(event)}
sx={{
height: { xs: 6, sm: 8 },
borderRadius: 4,
bgcolor: colors.gray[200],
'& .MuiLinearProgress-bar': {
borderRadius: 4,
background: `linear-gradient(90deg, ${colors.mint} 0%, ${colors.blue} 100%)`,
},
}}
/>
</Box>
)}
{/* Event Info and Stats Container */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-end',
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 3, sm: 0 },
}}
>
{/* Left: Event Info */}
<Box sx={{ width: { xs: '100%', sm: 'auto' } }}>
<Box sx={{ mb: { xs: 2, sm: 4 }, display: 'flex', flexDirection: 'column', gap: { xs: 1.5, sm: 2 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CardGiftcard sx={{ fontSize: { xs: 16, sm: 18 }, color: colors.pink }} />
<Typography
variant="body2"
sx={{ color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}
>
{event.prize}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getMethodIcon(event.method)}
<Typography
variant="body2"
sx={{ color: colors.gray[700], fontSize: { xs: '0.8125rem', sm: '0.875rem' } }}
>
{event.method}
</Typography>
</Box>
</Box>
{/* Date */}
<Typography
variant="body2"
sx={{
color: colors.gray[600],
display: 'flex',
alignItems: 'center',
gap: { xs: 1.5, sm: 2 },
fontSize: { xs: '0.75rem', sm: '0.875rem' },
}}
>
<span>📅</span>
<span>
{event.startDate} ~ {event.endDate}
</span>
</Typography>
</Box>
{/* Right: Stats */}
<Box sx={{ display: 'flex', gap: { xs: 4, sm: 8 }, textAlign: 'right', width: { xs: '100%', sm: 'auto' }, justifyContent: { xs: 'flex-start', sm: 'flex-end' } }}>
<Box>
<Typography
variant="body2"
sx={{ mb: { xs: 0.5, sm: 1 }, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
</Typography>
<Typography
variant="h5"
sx={{ fontWeight: 700, color: colors.gray[900], mb: 0.5, fontSize: { xs: '1.125rem', sm: '1.5rem' } }}
>
{event.participants.toLocaleString()}
<Typography
component="span"
variant="body2"
sx={{ ml: 0.5, color: colors.gray[600], fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
</Typography>
</Typography>
{event.targetParticipants > 0 && (
<Typography
variant="caption"
sx={{ color: colors.gray[500], fontSize: { xs: '0.6875rem', sm: '0.75rem' } }}
>
: {event.targetParticipants} (
{Math.round((event.participants / event.targetParticipants) * 100)}
%)
</Typography>
)}
</Box>
<Box>
<Typography
variant="body2"
sx={{ mb: { xs: 0.5, sm: 1 }, color: colors.gray[600], fontWeight: 500, fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
>
ROI
</Typography>
<Typography
variant="h5"
sx={{
fontWeight: 700,
fontSize: { xs: '1.125rem', sm: '1.5rem' },
color:
event.roi >= 400
? colors.mint
: event.roi >= 200
? colors.orange
: colors.gray[500],
}}
>
{event.roi}%
</Typography>
</Box>
</Box>
</Box>
</CardContent>
</Card>
))}
</Box>
)}
</Box>
{/* Pagination */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Pagination
count={totalPages}
page={currentPage}
onChange={(_, page) => setCurrentPage(page)}
size="large"
sx={{
'& .MuiPaginationItem-root': {
color: colors.gray[700],
fontWeight: 600,
'&.Mui-selected': {
background: `linear-gradient(135deg, ${colors.purple} 0%, ${colors.blue} 100%)`,
color: 'white',
'&:hover': {
background: `linear-gradient(135deg, ${colors.purpleLight} 0%, ${colors.blueLight} 100%)`,
},
},
'&:hover': {
bgcolor: colors.gray[100],
},
},
}}
/>
</Box>
)}
</Container>
</Box>
</>
);
}