diff --git a/src/components/CommunityPortal/EventPersonalization/EventStats.jsx b/src/components/CommunityPortal/EventPersonalization/EventStats.jsx index 7538380e77..f0f0377686 100644 --- a/src/components/CommunityPortal/EventPersonalization/EventStats.jsx +++ b/src/components/CommunityPortal/EventPersonalization/EventStats.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { useSelector } from 'react-redux'; import styles from './EventStats.module.css'; +import { useSelector } from 'react-redux'; const dummyData = [ { @@ -58,6 +58,9 @@ export default function PopularEvents() { const [timeFilter, setTimeFilter] = useState('All day'); const [typeFilter, setTypeFilter] = useState('All'); + const darkMode = useSelector(state => state.theme.darkMode); + + // ✅ KEEP YOUR SAFE FIX const calculatePercentage = (attended, enrolled) => enrolled === 0 ? 0 : Math.round((attended / enrolled) * 100); @@ -96,81 +99,156 @@ export default function PopularEvents() { filteredData[0], ) : null; - const darkMode = useSelector(state => state.theme.darkMode); + return ( -
-
-

+
+ {/* Header */} +
+

Most Popular Event

-
- setTimeFilter(e.target.value)} + className={` + ${styles.selectBase} + ${darkMode ? 'bg-space-cadet text-light border-light' : ''} + `} + > + + + + - setTypeFilter(e.target.value)} + className={` + ${styles.selectBase} + ${darkMode ? 'bg-space-cadet text-light border-light' : ''} + `} + > + + +
-
- {filteredData.map(event => { - const percentage = calculatePercentage(event.attended, event.enrolled); - return ( -
-
{event.type}
-
-
-
-
- {`${percentage}% (${event.attended}/${event.enrolled})`} -
+ {/* Stats Section */} +
+ {filteredData.map(event => ( +
+
+ {event.type} +
+ +
+
+
+ +
+ {`${calculatePercentage(event.attended, event.enrolled)}% (${event.attended}/${ + event.enrolled + })`}
- ); - })} -
-
-
-
- Total Number of Events -
-
- {filteredData.length}
+ ))} +
+ + {/* Summary */} +
+
+
Total Number of Events
+
{filteredData.length}
-
-
- Total Number of Event Enrollments -
-
- {filteredData.reduce((acc, event) => acc + event.enrolled, 0)} + +
+
Total Number of Event Enrollments
+
+ {filteredData.reduce((acc, e) => acc + e.enrolled, 0)}
+ {filteredData.length > 0 && ( <> -
-
- Most Popular Event -
-
- {mostPopularEvent.type || 'N/A'} -
+
+
Most Popular Event
+
{mostPopularEvent?.type || 'N/A'}
-
-
- Least Popular Event -
-
- {leastPopularEvent.type || 'N/A'} -
+ +
+
Least Popular Event
+
{leastPopularEvent?.type || 'N/A'}
)} diff --git a/src/components/CommunityPortal/EventPersonalization/EventStats.module.css b/src/components/CommunityPortal/EventPersonalization/EventStats.module.css index 2f589f6b12..bd6da94400 100644 --- a/src/components/CommunityPortal/EventPersonalization/EventStats.module.css +++ b/src/components/CommunityPortal/EventPersonalization/EventStats.module.css @@ -1,107 +1,64 @@ -.popularEventsContainer { +/* Container */ +.popular-events-container { max-height: 100%; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif; - background: white; } -.popularEventsContainerDark { - margin: 0 auto; - padding: 20px; - font-family: Arial, sans-serif; - background: #1B2A41; - min-height: 100%; -} - -.headerContainer { - display: flex; - justify-content: space-between; - align-items: center; -} - -.headerContainerDark { +/* Header */ +.header-container { display: flex; justify-content: space-between; align-items: center; - color: #1C2541; } -.popularEventsHeader { +.popular-events-header { font-size: 1.5rem; - color: #000000; margin: 0; } -.popularEventsHeaderDark { - font-size: 1.5rem; - color: #ffffff; - margin: 0; - background-color: #1C2541; -} - +/* Filters */ .filters { display: flex; gap: 10px; } -.filtersDark { - display: flex; - gap: 10px; - background-color: #1C2541; -} - -.filters select { +.selectBase { padding: 6px; font-size: 14px; - border: 1px solid #ccc; border-radius: 5px; cursor: pointer; + border: 1px solid #ccc; } +/* Stats */ .stats { display: flex; flex-direction: column; gap: 20px; border-radius: 8px; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); padding: 30px; } -.statsDark { +.stat-item { display: flex; - flex-direction: column; - gap: 20px; - border-radius: 8px; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); - padding: 30px; - background-color: #1C2541; -} - -.statItem { - display: grid; - grid-template-columns: 140px 1fr auto; align-items: center; gap: 15px; } -.statLabel { +.stat-label { + flex: 1; font-size: 14px; font-weight: bold; - color: #333; } -.statLabelDark { - font-size: 14px; - font-weight: bold; - color: #ffffff; -} - -.statBar { - width: 100%; - background: #f0f0f0; +.stat-bar { + flex: 3; height: 8px; border-radius: 5px; + margin: 0 10px; + background: #f0f0f0; position: relative; overflow: hidden; } @@ -112,37 +69,17 @@ transition: width 0.5s ease-in-out; } -.bar.green { - background-color: #4caf50; -} - -.bar.orange { - background-color: #ff9800; -} - -.bar.red { - background-color: #f44336; -} +.green { background-color: #4caf50; } +.orange { background-color: #ff9800; } +.red { background-color: #f44336; } -.statValue { +.stat-value { font-size: 14px; font-weight: bold; - color: #333; - white-space: nowrap; - text-align: right; - min-width: 110px; } -.statValueDark { - font-size: 14px; - font-weight: bold; - color: #ffffff; - white-space: nowrap; - text-align: right; - min-width: 110px; -} - -.eventSummary { +/* Summary */ +.event-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; @@ -152,169 +89,19 @@ padding-bottom: 20px; } -.summaryItem { - background: white; +.summary-item { padding: 12px; border-radius: 8px; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - max-height: 90px; - min-height: 80px; - overflow: hidden; - text-align: center; -} - -.summaryItemDark { - background: #3A506B; - padding: 12px; - border-radius: 8px; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - max-height: 90px; - min-height: 80px; - overflow: hidden; - text-align: center; -} - -.summaryTitle { - font-size: 14px; - font-weight: bold; - text-align: center; - white-space: normal; - word-wrap: break-word; - max-width: 90%; - color: #777; -} - -.summaryTitleDark { - font-size: 14px; - font-weight: bold; - text-align: center; - white-space: normal; - word-wrap: break-word; - max-width: 90%; - color: #ffffff; } -.summaryValue { +.summary-title { font-size: 14px; font-weight: bold; - color: #000; - margin-top: 4px; } -.summaryValueDark { +.summary-value { font-size: 14px; font-weight: bold; - color: #ffffff; margin-top: 4px; -} - -/* ── Responsive: tablets and below ── */ -@media (max-width: 768px) { - .popularEventsContainer, - .popularEventsContainerDark { - padding: 14px; - } - - .headerContainer, - .headerContainerDark { - flex-direction: column; - align-items: flex-start; - gap: 10px; - } - - .popularEventsHeader, - .popularEventsHeaderDark { - font-size: 1.2rem; - } - - .filters, - .filtersDark { - width: 100%; - flex-wrap: wrap; - } - - .filters select, - .filtersDark select { - flex: 1; - min-width: 100px; - } - - .stats, - .statsDark { - padding: 16px; - gap: 14px; - } - - .statItem { - grid-template-columns: 1fr auto; - grid-template-rows: auto auto; - gap: 6px 10px; - } - - .statLabel, - .statLabelDark { - grid-column: 1 / -1; - font-size: 13px; - } - - .statBar { - grid-column: 1; - grid-row: 2; - } - - .statValue, - .statValueDark { - grid-column: 2; - grid-row: 2; - font-size: 13px; - min-width: unset; - } - - .eventSummary { - grid-template-columns: repeat(2, 1fr); - gap: 10px; - padding-bottom: 14px; - } - - .summaryItem, - .summaryItemDark { - min-height: 70px; - padding: 10px; - } - - .summaryTitle, - .summaryTitleDark { - font-size: 12px; - } - - .summaryValue, - .summaryValueDark { - font-size: 13px; - } -} - -/* ── Responsive: small phones ── */ -@media (max-width: 480px) { - .popularEventsHeader, - .popularEventsHeaderDark { - font-size: 1.1rem; - } - - .statLabel, - .statLabelDark { - font-size: 12px; - } - - .statValue, - .statValueDark { - font-size: 12px; - } } \ No newline at end of file diff --git a/src/components/CommunityPortal/EventPersonalization/__tests__/EventStats.test.jsx b/src/components/CommunityPortal/EventPersonalization/__tests__/EventStats.test.jsx new file mode 100644 index 0000000000..697c7e70fd --- /dev/null +++ b/src/components/CommunityPortal/EventPersonalization/__tests__/EventStats.test.jsx @@ -0,0 +1,134 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import PopularEvents from '../EventStats'; + +// Minimal mock reducer +const mockThemeReducer = (darkMode = false) => () => ({ darkMode }); + +const renderWithStore = (darkMode = false) => { + const store = configureStore({ + reducer: { + theme: mockThemeReducer(darkMode), + }, + }); + + return render( + + + , + ); +}; + +describe('PopularEvents Component', () => { + // --------------------------- + // BASIC RENDER + // --------------------------- + test('renders header', () => { + renderWithStore(); + expect(screen.getByRole('heading', { name: /Most Popular Event/i })).toBeInTheDocument(); + }); + + test('renders exactly 7 event labels initially', () => { + renderWithStore(); + const labels = screen.getAllByTestId('stat-label'); + expect(labels.length).toBe(7); + }); + + // --------------------------- + // FILTERS + // --------------------------- + test('filters events by type (Offline)', () => { + renderWithStore(); + const typeSelect = screen.getAllByRole('combobox')[1]; + + fireEvent.change(typeSelect, { target: { value: 'Offline' } }); + + const labels = screen.getAllByTestId('stat-label'); + expect(labels.length).toBe(4); + }); + + test('filters events by time (Morning)', () => { + renderWithStore(); + const timeSelect = screen.getAllByRole('combobox')[0]; + + fireEvent.change(timeSelect, { target: { value: 'Morning' } }); + + const labels = screen.getAllByTestId('stat-label'); + expect(labels.length).toBe(3); + }); + + test('changing filters multiple times restores all 7 events', () => { + renderWithStore(); + const select = screen.getAllByRole('combobox')[1]; + + fireEvent.change(select, { target: { value: 'Offline' } }); + fireEvent.change(select, { target: { value: 'Online' } }); + fireEvent.change(select, { target: { value: 'All' } }); + + const labels = screen.getAllByTestId('stat-label'); + expect(labels.length).toBe(7); + }); + + // --------------------------- + // SUMMARY CARDS + // --------------------------- + test('summary shows correct values', () => { + renderWithStore(); + + expect(screen.getByText('Total Number of Events')).toBeInTheDocument(); + expect(screen.getByTestId('summary-total-events')).toHaveTextContent('7'); + + expect(screen.getByTestId('summary-total-enrollments')).toHaveTextContent('145'); + + expect(screen.getAllByText('Most Popular Event').length).toBe(2); + expect(screen.getByTestId('summary-most')).toHaveTextContent('Type of Event 2'); + + expect(screen.getAllByText('Least Popular Event').length).toBe(1); + expect(screen.getByTestId('summary-least')).toHaveTextContent('Type of Event 7'); + }); + + // --------------------------- + // DARK MODE + // --------------------------- + test('dark mode applies proper class', () => { + renderWithStore(true); + + // specifically select ONLY the main header, not the summary box + const heading = screen.getByRole('heading', { + name: 'Most Popular Event', + }); + + expect(heading.className.includes('text-light')).toBe(true); + }); + + // --------------------------- + // EMPTY FILTER RESULTS + // --------------------------- + test('no summary cards when filteredData is empty', () => { + renderWithStore(); + + const timeSelect = screen.getAllByRole('combobox')[0]; + fireEvent.change(timeSelect, { target: { value: 'NonExistent' } }); + + // Header stays + expect(screen.getByRole('heading', { name: 'Most Popular Event' })).toBeInTheDocument(); + + // These disappear because filteredData is empty + expect(screen.queryByTestId('summary-most')).not.toBeInTheDocument(); + expect(screen.queryByTestId('summary-least')).not.toBeInTheDocument(); + }); + + // --------------------------- + // BAR WIDTH + // --------------------------- + test('bars have inline width style', () => { + renderWithStore(); + + const bars = screen.getAllByTestId('stat-bar-inner'); + + bars.forEach(inner => { + expect(inner.style.width).toMatch(/%/); + }); + }); +});