Skip to content
Merged
191 changes: 101 additions & 90 deletions src/components/CommunityPortal/Activities/ActivityList.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
// Activity List Component
import { useState, useEffect, useMemo } from 'react';
import { useSelector, useStore } from 'react-redux';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
import styles from './ActivityList.module.css';
import { mockActivities } from './mockActivities';

function ActivityList() {
let darkMode = false;

try {
const store = useStore();
darkMode = store?.getState()?.theme?.darkMode ?? false;
} catch (e) {
darkMode = false;
}

const [activities, setActivities] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedActivity, setSelectedActivity] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
const darkMode = useSelector(state => state.theme.darkMode);
const [filter, setFilter] = useState({
type: '',
date: '',
location: '',
pastEvents: false,
});
const [locationSuggestions, setLocationSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [sortOrder, setSortOrder] = useState('earliest');
const [showPastEvents, setShowPastEvents] = useState(false);

Expand Down Expand Up @@ -60,36 +54,15 @@ function ActivityList() {
fetchActivities();
}, []);

const getLocationSuggestions = input => {
if (!input.trim()) return [];

const uniqueLocations = [...new Set(activities.map(a => a.location))];
const lowerInput = input.toLowerCase();

return uniqueLocations.filter(loc => loc.toLowerCase().startsWith(lowerInput)).slice(0, 10);
};

const handleFilterChange = e => {
const { name, value } = e.target;
setFilter({ ...filter, [name]: value });

if (name === 'location') {
const suggestions = getLocationSuggestions(value);
setLocationSuggestions(suggestions);
setShowSuggestions(true);
}
};

const handleSortChange = e => {
setSortOrder(e.target.value);
};

const handleSuggestionClick = location => {
setFilter({ ...filter, location });
setShowSuggestions(false);
setLocationSuggestions([]);
};

const handleClearFilters = () => {
setFilter({
type: '',
Expand All @@ -98,8 +71,15 @@ function ActivityList() {
showPastEvents: false,
});
setShowPastEvents(false);
setLocationSuggestions([]);
setShowSuggestions(false);
};

const handleActivityClick = activity => {
setSelectedActivity(activity);
setModalOpen(true);
};

const handleCloseModal = () => {
setModalOpen(false);
};

const startOfToday = useMemo(() => {
Expand All @@ -108,6 +88,20 @@ function ActivityList() {
return d;
}, []);

const activityTypes = useMemo(() => {
const typeOrder = new Map();

activities.forEach(activity => {
if (activity.type && !typeOrder.has(activity.type)) {
typeOrder.set(activity.type, typeOrder.size);
}
});

return [...typeOrder.keys()].sort(
(typeA, typeB) => typeOrder.get(typeA) - typeOrder.get(typeB),
);
}, [activities]);

const filteredActivities = activities
.filter(activity => showPastEvents || activity._dateObj >= startOfToday)
.filter(activity => {
Expand All @@ -134,15 +128,20 @@ function ActivityList() {

<div className={`${darkMode ? styles.darkModeFilters : styles.filters}`}>
<label className={darkMode ? 'text-light' : ''}>
Type:
<input
type="text"
Type:{' '}
<select
name="type"
value={filter.type}
onChange={handleFilterChange}
placeholder="Enter type"
className={darkMode ? styles.darkModeInput : ''}
/>
>
<option value="">All Types</option>
{activityTypes.map(type => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>

<label className={darkMode ? 'text-light' : ''}>
Expand Down Expand Up @@ -171,50 +170,14 @@ function ActivityList() {

<label className={darkMode ? 'text-light' : ''}>
Location:
<div style={{ position: 'relative' }}>
<input
type="text"
name="location"
value={filter.location}
onChange={handleFilterChange}
onFocus={() => {
if (filter.location) {
const suggestions = getLocationSuggestions(filter.location);
setLocationSuggestions(suggestions);
setShowSuggestions(true);
}
}}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
placeholder="Enter location"
autoComplete="off"
className={darkMode ? styles.darkModeInput : ''}
/>

{showSuggestions && locationSuggestions.length > 0 && (
<div className={`${styles.suggestions} ${darkMode ? styles.darkSuggestions : ''}`}>
{locationSuggestions.map((location, index) => (
<div
key={index}
role="button"
tabIndex={0}
className={styles.suggestionItem}
onMouseDown={e => {
e.preventDefault();
handleSuggestionClick(location);
}}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSuggestionClick(location);
}
}}
>
{location}
</div>
))}
</div>
)}
</div>
<input
type="text"
name="location"
value={filter.location}
onChange={handleFilterChange}
placeholder="Enter location"
className={darkMode ? styles.darkModeInput : ''}
/>
</label>
<label className={`${styles.showPastToggle} ${darkMode ? styles.darkShowPastToggle : ''}`}>
Show Past Events:
Expand Down Expand Up @@ -246,21 +209,69 @@ function ActivityList() {
) : filteredActivities.length > 0 ? (
<ul>
{filteredActivities.map(activity => (
<li
<div
key={activity.id}
className={`${styles.activityItem} ${darkMode ? styles.darkModeItem : ''}`}
style={{ cursor: 'pointer' }}
onClick={() => handleActivityClick(activity)}
tabIndex={0}
role="button"
aria-label={`View details for ${activity.name}`}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
handleActivityClick(activity);
}
}}
>
<strong>{activity.name}</strong>
<span>
{activity.type} – {activity.date} – {activity.location}
</span>
</li>
<li
key={activity.id}
className={`${styles.activityItem} ${darkMode ? styles.darkModeItem : ''}`}
>
<strong>{activity.name}</strong>
<span>
{activity.type} – {activity.date} – {activity.location}
</span>
</li>
</div>
))}
</ul>
) : (
<p className={darkMode ? 'text-light' : ''}>No activities found</p>
)}
</div>

{/* Modal for activity details */}
<Modal isOpen={modalOpen} toggle={handleCloseModal}>
<ModalHeader toggle={handleCloseModal}>
{selectedActivity ? selectedActivity.name : ''}
</ModalHeader>
<ModalBody>
{selectedActivity && (
<div>
<p>
<strong>Type:</strong> {selectedActivity.type}
</p>
<p>
<strong>Date:</strong> {selectedActivity.date}
</p>
<p>
<strong>Time:</strong> {selectedActivity.time}
</p>
<p>
<strong>Location:</strong> {selectedActivity.location}
</p>
<p>
<strong>Description:</strong> {selectedActivity.description}
</p>
{/* Add more details as needed */}
</div>
)}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={handleCloseModal}>
Close
</Button>
</ModalFooter>
</Modal>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import ActivityList from '../ActivityList';

const mockStore = configureMockStore([]);

const renderActivityList = (initialState = { theme: { darkMode: false } }) => {

Check warning on line 8 in src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use an object literal as default for parameter `initialState`.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2EgwuIG7Y7XwT7mlvr&open=AZ2EgwuIG7Y7XwT7mlvr&pullRequest=4955
const store = mockStore(initialState);
return render(
<Provider store={store}>
<ActivityList />
</Provider>,
);
};

describe('ActivityList', () => {
test('renders Activity List heading', () => {
render(<ActivityList />);
renderActivityList();
expect(screen.getByText('Activity List')).toBeInTheDocument();
});

test('renders activities from mock data', () => {
render(<ActivityList />);
renderActivityList();
fireEvent.click(screen.getByLabelText(/Show Past Events:/i));
expect(screen.getByText('Yoga Class')).toBeInTheDocument();
expect(screen.getByText('Book Club')).toBeInTheDocument();
});

test('filters activities by type', () => {
render(<ActivityList />);
renderActivityList();
fireEvent.click(screen.getByLabelText(/Show Past Events:/i));

fireEvent.change(screen.getByPlaceholderText('Enter type'), {
fireEvent.change(screen.getByLabelText(/Type:/i), {
target: { value: 'Fitness' },
});

Expand All @@ -27,7 +40,7 @@
});

test('filters activities by date', () => {
render(<ActivityList />);
renderActivityList();
fireEvent.click(screen.getByLabelText(/Show Past Events:/i));

fireEvent.change(screen.getByLabelText(/Date:/i), {
Expand All @@ -40,13 +53,13 @@
});

test('sorts activities by date (latest to earliest)', () => {
render(<ActivityList />);
renderActivityList();

fireEvent.change(screen.getByLabelText(/Sort By:/i), {
target: { value: 'latest' },
});

const items = screen.getAllByRole('listitem');
expect(items[0]).toHaveTextContent('Marathon Training');
expect(items[0]).toHaveTextContent('Book Club - March');
});
});
Loading
Loading