|
1 | 1 | 'use client'; |
2 | 2 |
|
| 3 | +import { useState } from 'react'; |
| 4 | +import { useSelector } from 'react-redux'; |
3 | 5 | import { |
4 | 6 | Bar, |
5 | 7 | BarChart, |
6 | 8 | CartesianGrid, |
| 9 | + Legend, |
7 | 10 | ResponsiveContainer, |
| 11 | + Tooltip, |
8 | 12 | XAxis, |
9 | 13 | YAxis, |
10 | | - Tooltip, |
11 | | - Legend, |
12 | 14 | } from 'recharts'; |
| 15 | +import PropTypes from 'prop-types'; |
13 | 16 | import styles from './EventPopularity.module.css'; |
14 | | -import { useSelector } from 'react-redux'; |
15 | 17 |
|
16 | | -// Sample data |
17 | 18 | const eventTypeData = [ |
18 | | - { name: 'Event Type 1', registered: 75 }, |
19 | | - { name: 'Event Type 2', registered: 60 }, |
20 | | - { name: 'Event Type 3', registered: 55 }, |
21 | | - { name: 'Event Type 4', registered: 50 }, |
22 | | - { name: 'Event Type 5', registered: 45 }, |
23 | | - { name: 'Event Type 6', registered: 40 }, |
| 19 | + { name: 'Community Volunteer Day', registered: 75 }, |
| 20 | + { name: 'Skill Development Workshop', registered: 60 }, |
| 21 | + { name: 'Networking Mixer', registered: 55 }, |
| 22 | + { name: 'Environmental Cleanup', registered: 50 }, |
| 23 | + { name: 'Youth Membership Program', registered: 45 }, |
| 24 | + { name: 'Cultural Exchange Event', registered: 40 }, |
24 | 25 | ]; |
25 | 26 |
|
26 | 27 | const timeData = [ |
27 | | - { time: '9:00', registered: 8, attended: 12 }, |
28 | | - { time: '11:00', registered: 15, attended: 18 }, |
29 | | - { time: '13:00', registered: 20, attended: 25 }, |
30 | | - { time: '15:00', registered: 25, attended: 30 }, |
31 | | - { time: '17:00', registered: 18, attended: 20 }, |
32 | | - { time: '19:00', registered: 10, attended: 15 }, |
33 | | - { time: '21:00', registered: 5, attended: 8 }, |
| 28 | + { time: '9:00 AM', registered: 8, attended: 12 }, |
| 29 | + { time: '11:00 AM', registered: 15, attended: 18 }, |
| 30 | + { time: '1:00 PM', registered: 20, attended: 25 }, |
| 31 | + { time: '3:00 PM', registered: 25, attended: 30 }, |
| 32 | + { time: '5:00 PM', registered: 18, attended: 20 }, |
| 33 | + { time: '7:00 PM', registered: 10, attended: 15 }, |
| 34 | + { time: '9:00 PM', registered: 5, attended: 8 }, |
34 | 35 | ]; |
35 | 36 |
|
36 | 37 | const participationCards = [ |
37 | 38 | { |
38 | | - title: '5+', |
39 | | - subtitle: 'Repeated participation', |
| 39 | + title: '5+ Events', |
| 40 | + subtitle: 'Highly Engaged Members', |
| 41 | + description: 'Users who attended 5 or more events', |
40 | 42 | trend: '-10%', |
41 | 43 | trendType: 'negative', |
42 | 44 | participants: 3, |
43 | 45 | }, |
44 | 46 | { |
45 | | - title: '2+', |
46 | | - subtitle: 'Repeated participation', |
| 47 | + title: '2-4 Events', |
| 48 | + subtitle: 'Regular Participants', |
| 49 | + description: 'Users who attended 2 to 4 events', |
47 | 50 | trend: '+25%', |
48 | 51 | trendType: 'positive', |
49 | 52 | participants: 3, |
50 | 53 | }, |
51 | 54 | { |
52 | | - title: '<1', |
53 | | - subtitle: 'Repeated participation', |
| 55 | + title: '1 Event', |
| 56 | + subtitle: 'New/One-Time Attendees', |
| 57 | + description: 'First-time or one-time participants', |
54 | 58 | trend: '-5%', |
55 | 59 | trendType: 'negative', |
56 | 60 | participants: 3, |
57 | 61 | }, |
58 | 62 | { |
59 | | - title: '420', |
60 | | - subtitle: 'Total Members', |
| 63 | + title: '420 Users', |
| 64 | + subtitle: 'Total Active Members', |
| 65 | + description: 'Total users with at least one event attendance', |
61 | 66 | trend: '+20%', |
62 | 67 | trendType: 'positive', |
63 | 68 | }, |
64 | 69 | ]; |
65 | 70 |
|
| 71 | +const CustomTooltip = ({ active, payload, label }) => { |
| 72 | + if (!active || !payload?.length) { |
| 73 | + return null; |
| 74 | + } |
| 75 | + |
| 76 | + return ( |
| 77 | + <div className={styles.tooltipBox}> |
| 78 | + <div className={styles.tooltipTitle}>{label}</div> |
| 79 | + |
| 80 | + {payload.map(item => ( |
| 81 | + <div key={item.dataKey} className={styles.tooltipRow}> |
| 82 | + <span>{item.name}: </span> |
| 83 | + <strong>{item.value} users</strong> |
| 84 | + </div> |
| 85 | + ))} |
| 86 | + </div> |
| 87 | + ); |
| 88 | +}; |
| 89 | + |
| 90 | +CustomTooltip.propTypes = { |
| 91 | + active: PropTypes.bool, |
| 92 | + payload: PropTypes.arrayOf( |
| 93 | + PropTypes.shape({ |
| 94 | + dataKey: PropTypes.string, |
| 95 | + name: PropTypes.string, |
| 96 | + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
| 97 | + }), |
| 98 | + ), |
| 99 | + label: PropTypes.string, |
| 100 | +}; |
| 101 | + |
| 102 | +const InfoTooltip = ({ text, children }) => { |
| 103 | + const [showTooltip, setShowTooltip] = useState(false); |
| 104 | + |
| 105 | + return ( |
| 106 | + <button |
| 107 | + type="button" |
| 108 | + className={styles.infotooltipHover} |
| 109 | + onMouseEnter={() => setShowTooltip(true)} |
| 110 | + onMouseLeave={() => setShowTooltip(false)} |
| 111 | + onFocus={() => setShowTooltip(true)} |
| 112 | + onBlur={() => setShowTooltip(false)} |
| 113 | + > |
| 114 | + {children} |
| 115 | + |
| 116 | + {showTooltip && <div className={styles.infotooltipheading}>{text}</div>} |
| 117 | + </button> |
| 118 | + ); |
| 119 | +}; |
| 120 | + |
| 121 | +InfoTooltip.propTypes = { |
| 122 | + text: PropTypes.string.isRequired, |
| 123 | + children: PropTypes.node.isRequired, |
| 124 | +}; |
| 125 | + |
66 | 126 | export default function EventDashboard() { |
67 | 127 | const darkMode = useSelector(state => state.theme?.darkMode); |
68 | 128 |
|
| 129 | + const currentDate = new Date(); |
| 130 | + |
| 131 | + const thirtyDaysAgo = new Date(currentDate.getTime() - 30 * 24 * 60 * 60 * 1000); |
| 132 | + |
| 133 | + const dateRangeLabel = `${thirtyDaysAgo.toLocaleDateString()} - ${currentDate.toLocaleDateString()}`; |
| 134 | + |
69 | 135 | return ( |
70 | | - <div className={`${styles.eventpopularity} ${darkMode ? styles.dark : ''}`}> |
71 | | - <h1 className={styles.epheader}>Event Attendance Trend</h1> |
| 136 | + <div className={`${styles.dashboardContainer} ${darkMode ? styles.dark : ''}`}> |
| 137 | + <div className={styles.headerSection}> |
| 138 | + <h1 className={styles.pageTitle}>Event Attendance Dashboard</h1> |
| 139 | + |
| 140 | + <div className={styles.timePeriod}> |
| 141 | + <strong>Time Period:</strong> Last 30 days ({dateRangeLabel}) |
| 142 | + </div> |
72 | 143 |
|
73 | | - <div className={styles.epgrid}> |
74 | | - {/* Event Registration Trend (Type) */} |
75 | | - <div className={styles.epCard}> |
76 | | - <h2>Event Registration Trend (Type)</h2> |
| 144 | + <div className={styles.subHeader}> |
| 145 | + All metrics below reflect data from the selected time period |
| 146 | + </div> |
| 147 | + </div> |
77 | 148 |
|
78 | | - <div> |
79 | | - <div className={styles.epRowLabel}> |
| 149 | + <div className={styles.dashboardGrid}> |
| 150 | + {/* Registration by Type */} |
| 151 | + <div className={styles.chartCard}> |
| 152 | + <div className={styles.cardHeader}> |
| 153 | + <h2 className={styles.cardTitle}>Event Registration by Type</h2> |
| 154 | + |
| 155 | + <InfoTooltip text="Total users who registered for each event type"> |
| 156 | + <span className={styles.infoIcon}>?</span> |
| 157 | + </InfoTooltip> |
| 158 | + </div> |
| 159 | + |
| 160 | + <div className={styles.eventList}> |
| 161 | + <div className={styles.tableHeader}> |
80 | 162 | <span>Event Name</span> |
81 | | - <span>Registered Members</span> |
| 163 | + <span>Registered Users</span> |
82 | 164 | </div> |
83 | 165 |
|
84 | 166 | {eventTypeData.map(event => ( |
85 | | - <div key={event.name} className={styles.epRowLabel}> |
86 | | - <span className={styles.epEventName}>{event.name}</span> |
| 167 | + <div key={event.name} className={styles.eventRow}> |
| 168 | + <span className={styles.eventName}>{event.name}</span> |
87 | 169 |
|
88 | | - <div className={styles.epProgressBar}> |
| 170 | + <div className={styles.progressBarBackground}> |
89 | 171 | <div |
90 | | - className={styles.epProgressFill} |
91 | | - style={{ width: `${(event.registered / 75) * 100}%` }} |
| 172 | + className={styles.progressBarFill} |
| 173 | + style={{ |
| 174 | + width: `${(event.registered / 75) * 100}%`, |
| 175 | + }} |
92 | 176 | /> |
93 | 177 | </div> |
94 | 178 |
|
95 | | - <span>{event.registered}</span> |
| 179 | + <span className={styles.eventCount}>{event.registered} users</span> |
96 | 180 | </div> |
97 | 181 | ))} |
98 | 182 | </div> |
99 | 183 |
|
100 | | - <div className={styles.epStatsGrid}> |
101 | | - <div className={styles.epStatCard}> |
102 | | - <h3 style={{ color: 'var(--ep-primary)' }}>325</h3> |
103 | | - <p className={styles.epStatSubtitle}>Total Registered Members</p> |
104 | | - </div> |
| 184 | + <div className={styles.statsGrid}> |
| 185 | + {[ |
| 186 | + { |
| 187 | + title: '325 Users', |
| 188 | + subtitle: 'Total Registrations', |
| 189 | + isPrimary: true, |
| 190 | + }, |
| 191 | + { |
| 192 | + title: 'Community Volunteer Day', |
| 193 | + subtitle: 'Most Popular', |
| 194 | + }, |
| 195 | + { |
| 196 | + title: 'Cultural Exchange Event', |
| 197 | + subtitle: 'Least Popular', |
| 198 | + }, |
| 199 | + ].map(card => ( |
| 200 | + <div key={card.title} className={styles.statCard}> |
| 201 | + <h3 className={card.isPrimary ? styles.primaryStatTitle : styles.statTitle}> |
| 202 | + {card.title} |
| 203 | + </h3> |
105 | 204 |
|
106 | | - <div className={styles.epStatCard}> |
107 | | - <h3>Event Type 1</h3> |
108 | | - <p className={styles.epStatSubtitle}>Most Popular Event Type</p> |
109 | | - </div> |
110 | | - |
111 | | - <div className={styles.epStatCard}> |
112 | | - <h3>Event Type 6</h3> |
113 | | - <p className={styles.epStatSubtitle}>Least Popular Event Type</p> |
114 | | - </div> |
| 205 | + <p className={styles.statSubtitle}>{card.subtitle}</p> |
| 206 | + </div> |
| 207 | + ))} |
115 | 208 | </div> |
116 | 209 | </div> |
117 | 210 |
|
118 | | - {/* Event Registration Trend (Time) */} |
119 | | - <div className={styles.epChartCard}> |
120 | | - <h2>Event Registration Trend (Time)</h2> |
121 | | - |
122 | | - <ResponsiveContainer width="100%" height={200}> |
123 | | - <BarChart data={timeData}> |
124 | | - <CartesianGrid stroke="var(--ep-grid-stroke)" strokeDasharray="3 3" /> |
125 | | - <XAxis dataKey="time" stroke="var(--ep-chart-tick)" /> |
126 | | - <YAxis stroke="var(--ep-chart-tick)" /> |
127 | | - <Tooltip |
128 | | - contentStyle={{ |
129 | | - background: 'var(--ep-card-bg)', |
130 | | - color: 'var(--ep-text-color)', |
| 211 | + {/* Attendance by Time */} |
| 212 | + <div className={styles.chartCard}> |
| 213 | + <div className={styles.cardHeader}> |
| 214 | + <h2 className={styles.cardTitle}>Event Attendance by Time Slot</h2> |
| 215 | + |
| 216 | + <InfoTooltip text="Registered = sign-ups | Attended = actual participants"> |
| 217 | + <span className={styles.infoIcon}>?</span> |
| 218 | + </InfoTooltip> |
| 219 | + </div> |
| 220 | + |
| 221 | + <div className={styles.chartWrapper}> |
| 222 | + <ResponsiveContainer width="100%" height={250}> |
| 223 | + <BarChart |
| 224 | + data={timeData} |
| 225 | + margin={{ |
| 226 | + top: 20, |
| 227 | + right: 20, |
| 228 | + left: 80, |
| 229 | + bottom: 40, |
131 | 230 | }} |
132 | | - /> |
133 | | - <Legend /> |
134 | | - <Bar dataKey="registered" fill="var(--ep-primary)" /> |
135 | | - <Bar dataKey="attended" fill="var(--ep-primary-2)" /> |
136 | | - </BarChart> |
137 | | - </ResponsiveContainer> |
138 | | - |
139 | | - <div className={styles.epParticipationGrid}> |
| 231 | + > |
| 232 | + <CartesianGrid strokeDasharray="3 3" stroke="var(--ep-grid-stroke, #d1d5db)" /> |
| 233 | + |
| 234 | + <XAxis |
| 235 | + dataKey="time" |
| 236 | + interval={0} |
| 237 | + angle={-35} |
| 238 | + textAnchor="end" |
| 239 | + height={60} |
| 240 | + stroke="var(--ep-chart-tick, #64748b)" |
| 241 | + /> |
| 242 | + |
| 243 | + <YAxis |
| 244 | + width={80} |
| 245 | + tick={{ fontSize: 12 }} |
| 246 | + stroke="var(--ep-chart-tick, #64748b)" |
| 247 | + label={{ |
| 248 | + value: 'Number of Users', |
| 249 | + angle: -90, |
| 250 | + position: 'insideLeft', |
| 251 | + dx: -25, |
| 252 | + style: { |
| 253 | + textAnchor: 'middle', |
| 254 | + fill: 'var(--ep-chart-tick, #64748b)', |
| 255 | + fontSize: 12, |
| 256 | + }, |
| 257 | + }} |
| 258 | + /> |
| 259 | + |
| 260 | + <Tooltip content={<CustomTooltip />} /> |
| 261 | + |
| 262 | + <Legend |
| 263 | + wrapperStyle={{ |
| 264 | + paddingTop: '20px', |
| 265 | + }} |
| 266 | + /> |
| 267 | + |
| 268 | + <Bar |
| 269 | + dataKey="registered" |
| 270 | + name="Registered Users" |
| 271 | + fill="var(--ep-primary, #4A90E2)" |
| 272 | + /> |
| 273 | + |
| 274 | + <Bar dataKey="attended" name="Attended Users" fill="var(--ep-primary-2, #82B7FF)" /> |
| 275 | + </BarChart> |
| 276 | + </ResponsiveContainer> |
| 277 | + </div> |
| 278 | + |
| 279 | + <div className={styles.participationGrid}> |
140 | 280 | {participationCards.map(card => ( |
141 | | - <div key={card.title} className={styles.epParticipationCard}> |
142 | | - <h3>{card.title}</h3> |
143 | | - <p className={styles.epStatSubtitle}>{card.subtitle}</p> |
| 281 | + <div key={card.title} className={styles.participationCard}> |
| 282 | + <div className={styles.participationHeader}> |
| 283 | + <h3 className={styles.participationTitle}>{card.title}</h3> |
| 284 | + |
| 285 | + <InfoTooltip text={card.description}> |
| 286 | + <span className={styles.smallInfoIcon}>?</span> |
| 287 | + </InfoTooltip> |
| 288 | + </div> |
| 289 | + |
| 290 | + <p className={styles.participationSubtitle}>{card.subtitle}</p> |
144 | 291 |
|
145 | | - {!!card.participants && <div> +{card.participants}</div>} |
| 292 | + {Boolean(card.participants) && ( |
| 293 | + <div className={styles.participantCount}>👥 {card.participants} users</div> |
| 294 | + )} |
146 | 295 |
|
147 | 296 | <p |
148 | 297 | className={ |
149 | | - card.trendType === 'positive' ? styles.trendPositive : styles.trendNegative |
| 298 | + card.trendType === 'positive' ? styles.positiveTrend : styles.negativeTrend |
150 | 299 | } |
151 | 300 | > |
152 | | - {card.trend} Monthly |
| 301 | + {card.trend} vs last month |
153 | 302 | </p> |
154 | 303 | </div> |
155 | 304 | ))} |
|
0 commit comments