Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
*.png
*.svg
src/actions/**
src/App.css
src/config.json

src/languages/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import styles from './ActivityComments.module.css';

// Utility function to calculate relative time
Expand Down Expand Up @@ -81,7 +82,6 @@ const sanitizeInput = input => {
return result.trim().substring(0, 5000); // Limit length to prevent DoS attacks
};

// Utility function to generate secure random numbers for demo purposes
// Utility function to generate secure random numbers for demo purposes
const getSecureRandomInt = (min, max) => {
// Use a deterministic approach for demo data instead of Math.random()
Expand Down Expand Up @@ -244,6 +244,8 @@ const mockFeedbacks = [
];

function ActivityComments() {
const darkMode = useSelector(state => state.theme?.darkMode || false);

// Utility function to restore Date objects from localStorage
const restoreDates = items => {
return items.map(item => ({
Expand Down Expand Up @@ -632,20 +634,44 @@ function ActivityComments() {
);
};

/**
* Filter and sort feedbacks based on search, filter, and sort criteria
*
* Search Parameters:
* - Reviewer name (feedback.name): Searches in the reviewer's name
* - Feedback text (feedback.text): Searches in the feedback comment/description
*
* The search uses case-insensitive partial matching (contains) for both fields.
*/
const filteredFeedbacks = feedbacks
.filter(feedback => {
const matchesSearch =
feedback.text.toLowerCase().includes(feedbackSearch.toLowerCase()) ||
feedback.name.toLowerCase().includes(feedbackSearch.toLowerCase());
// Search logic: check if search term matches reviewer name or feedback text
const searchTerm = feedbackSearch.trim().toLowerCase();
let matchesSearch = true;

if (searchTerm) {
const reviewerName = (feedback.name || '').toLowerCase();
const feedbackText = (feedback.text || '').toLowerCase();

// Explicit search matching: check both fields with OR logic
matchesSearch = reviewerName.includes(searchTerm) || feedbackText.includes(searchTerm);
}

// Rating filter logic
const matchesFilter =
feedbackFilter === 'All' || feedback.rating.toString() === feedbackFilter;

return matchesSearch && matchesFilter;
})
.sort((a, b) => {
if (feedbackSort === 'Oldest') return new Date(a.timestamp) - new Date(b.timestamp);
// Sort by creation date with null safety
const dateA = a.createdAt ? new Date(a.createdAt) : new Date(0);
const dateB = b.createdAt ? new Date(b.createdAt) : new Date(0);

if (feedbackSort === 'Oldest') return dateA - dateB;
if (feedbackSort === 'Highest Rated') return b.rating - a.rating;
if (feedbackSort === 'Lowest Rated') return a.rating - b.rating;
return new Date(b.timestamp) - new Date(a.timestamp); // Newest
return dateB - dateA; // Newest (default)
});

return (
Expand Down Expand Up @@ -1117,16 +1143,18 @@ function ActivityComments() {
>
<input
type="text"
placeholder="Search feedback..."
placeholder="Search by reviewer name or feedback text..."
value={feedbackSearch}
onChange={e => setFeedbackSearch(e.target.value)}
style={{
flex: 1,
minWidth: '200px',
padding: '8px 12px',
border: '1px solid #ddd',
border: darkMode ? '1px solid #4a5a77' : '1px solid #ddd',
borderRadius: '6px',
fontSize: '0.9rem',
backgroundColor: darkMode ? '#3a506b' : '#fff',
color: darkMode ? '#ffffff' : '#222',
}}
/>
<select
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Unit tests for Feedback search functionality in ActivityComments component
* Tests search by reviewer name and feedback text with case-insensitive partial matching
*/

describe('ActivityComments - Feedback Search Functionality', () => {
const mockFeedbacks = [
{
id: 1,
name: 'Sarah Johnson',
text: 'This was an absolutely fantastic event!',
rating: 5,
createdAt: new Date('2024-01-01'),
},
{
id: 2,
name: 'Anonymous User',
text: 'Really enjoyed the event overall.',
rating: 4,
createdAt: new Date('2024-01-02'),
},
{
id: 3,
name: 'Mike Chen',
text: 'The event was okay. Some parts were interesting.',
rating: 3,
createdAt: new Date('2024-01-03'),
},
];

// Helper function to simulate the search filter logic
const filterFeedbacks = (feedbacks, searchTerm, filter = 'All') => {
return feedbacks.filter(feedback => {
const trimmedSearch = searchTerm.trim().toLowerCase();
let matchesSearch = true;

if (trimmedSearch) {
const reviewerName = (feedback.name || '').toLowerCase();
const feedbackText = (feedback.text || '').toLowerCase();
matchesSearch =
reviewerName.includes(trimmedSearch) || feedbackText.includes(trimmedSearch);
}

const matchesFilter = filter === 'All' || feedback.rating.toString() === filter;
return matchesSearch && matchesFilter;
});
};

// Helper function for common assertions
const expectSingleResult = (results, expectedName, expectedText) => {
expect(results).toHaveLength(1);
if (expectedName) expect(results[0].name).toBe(expectedName);
if (expectedText) expect(results[0].text).toContain(expectedText);
};

describe('Search by reviewer name', () => {
test.each([
['Sarah Johnson', 'Sarah Johnson', null],
['Sarah', 'Sarah Johnson', null],
['sarah', 'Sarah Johnson', null],
['Mike', 'Mike Chen', null],
])('should find feedback for search term "%s"', (searchTerm, expectedName) => {
const results = filterFeedbacks(mockFeedbacks, searchTerm);
expectSingleResult(results, expectedName, null);
});
});

describe('Search by feedback text', () => {
test.each([
['fantastic event', null, 'fantastic'],
['enjoyed', null, 'enjoyed'],
['FANTASTIC', null, 'fantastic'],
])('should find feedback for text search "%s"', (searchTerm, expectedName, expectedText) => {
const results = filterFeedbacks(mockFeedbacks, searchTerm);
expectSingleResult(results, expectedName, expectedText);
});
});

describe('Search edge cases', () => {
test.each([
['', 3],
[' ', 3],
['nonexistent', 0],
])('should return %d results for search term "%s"', (searchTerm, expectedCount) => {
const results = filterFeedbacks(mockFeedbacks, searchTerm);
expect(results).toHaveLength(expectedCount);
});

test('should handle null or undefined name gracefully', () => {
const feedbackWithNullName = {
id: 4,
name: null,
text: 'Some feedback text',
rating: 5,
createdAt: new Date('2024-01-04'),
};
const results = filterFeedbacks([feedbackWithNullName], 'text');
expect(results).toHaveLength(1);
});

test('should handle null or undefined text gracefully', () => {
const feedbackWithNullText = {
id: 5,
name: 'John Doe',
text: null,
rating: 5,
createdAt: new Date('2024-01-05'),
};
const results = filterFeedbacks([feedbackWithNullText], 'John');
expect(results).toHaveLength(1);
});
});

describe('Search with rating filter', () => {
test.each([
['event', '5', 1, 5],
['Sarah', '1', 0, null],
])(
'should filter by rating %s and search "%s" to return %d results',
(searchTerm, rating, expectedCount, expectedRating) => {
const results = filterFeedbacks(mockFeedbacks, searchTerm, rating);
expect(results).toHaveLength(expectedCount);
if (expectedRating) expect(results[0].rating).toBe(expectedRating);
},
);
});

describe('Partial matching', () => {
test.each([
['John', 'name', 'Johnson'],
['absolutely', 'text', 'absolutely'],
])('should match partial words in %s', (searchTerm, field, expectedContent) => {
const results = filterFeedbacks(mockFeedbacks, searchTerm);
expect(results).toHaveLength(1);
expect(results[0][field]).toContain(expectedContent);
});
});
});
21 changes: 14 additions & 7 deletions src/utils/authInit.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ export default function initAuth() {
const token = localStorage.getItem(config.tokenKey);
if (!token) return;

const decoded = jwtDecode(token);
const nowSec = Date.now() / 1000;
const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000;
try {
const decoded = jwtDecode(token);
const nowSec = Date.now() / 1000;
const expirySec = new Date(decoded.expiryTimestamp).getTime() / 1000;

if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) {
if (expirySec - TOKEN_LIFETIME_BUFFER < nowSec) {
store.dispatch(logoutUser());
} else {
httpService.setjwt(token);
store.dispatch(setCurrentUser(decoded));
}
} catch (error) {
// Handle invalid or malformed token
console.error('Invalid token detected, clearing authentication:', error);
localStorage.removeItem(config.tokenKey);
store.dispatch(logoutUser());
} else {
httpService.setjwt(token);
store.dispatch(setCurrentUser(decoded));
}
}
Loading