diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index 35ba15cf3c..97d3b9ea5c 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -453,3 +453,785 @@ md-select .md-select-value span:first-child:after { color: #000; // Ensure the icon is visible font-size: 15px; // Adjust size if needed } +.filter-content { + padding: 15px; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 10px; + background-color: #f9f9f9; +} + +.filter-content label { + display: block; + margin-bottom: 10px; + font-weight: 500; +} + +.filter-content input[type="text"] { + width: -webkit-fill-available !important; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + margin-top: 5px; +} + +input[type="checkbox"] { + appearance: auto !important; + -webkit-appearance: checkbox !important; + -moz-appearance: checkbox !important; + width: 18px !important; + height: 18px !important; + opacity: 1 !important; + position: static !important; + pointer-events: auto !important; + visibility: visible !important; + margin-right: 8px; +} + +.sort-options { + margin: 15px 0; +} + +.ChallengeHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +.checkbox-container { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.checkbox-container label { + display: inline; + cursor: pointer; + margin-bottom: 0; + font-weight: normal; +} + +.filter-buttons { + margin-top: 20px; + display: flex; + height: max-content; + gap: 40px; +} + +.apply-button, .reset-button { + padding: 8px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + min-width: 100px; +} + +.filter-content label input { + border: none !important; + border-bottom: 1px solid #ccc !important; + outline: none ; + box-shadow: none; + border-radius: 0; +} + +.apply-button { + background-color: rgba(0, 0, 0, 0); + font: 14px Roboto; + color: #3c3e49; + border-radius: 20px; + border: 1px solid #3c3e49; + box-shadow: 0 4px 8px transparent; + transition: background-color 0.3s ease; +} + +.apply-button:hover { + box-shadow: 0 0 8px #9d9d9d; + background: #3c3e49; + color: #fff; +} + +.reset-button:hover { + box-shadow: 0 0 8px #9d9d9d; + background: #3c3e49; + color: #fff; +} + +.apply-button:focus{ + background-color: #3c3e49 !important; + color: white; +} +.reset-button:focus{ + background-color: #3c3e49 !important; + color: white; +} + +.reset-button { + background-color: rgba(0, 0, 0, 0); + font: 14px Roboto; + transition: background-color 0.3s ease; + color: #3c3e49; + border-radius: 20px; + border: 1px solid #3c3e49; + box-shadow: 0 4px 8px transparent; +} + +.search-filter-bar { + width: 54rem; + display: flex; + gap: 40px; + align-items: center; + margin-bottom: 15px; + margin-top: 15px; +} + +.search-box { + flex: 1; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +.filter-button { + width: 130px; + font: 14px Roboto; + border-radius: 50px; + background: #3c3e49; + font-weight: 600; + color: #fff; + box-shadow: 0px 4px 8px #9d9d9d; + cursor: pointer; + padding: 7px; +} + +.filter-button:hover { + background-color: #3c3e49; +} + +.filter-button:focus { + background-color: #3c3e49 !important; +} + +.no-results { + padding: 20px; + text-align: center; + font-weight: bold; + color: #666; + background-color: #f5f5f5; + border-radius: 4px; + margin-bottom: 20px; +} + +* { + box-sizing: border-box; +} + +.filter-panel { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + justify-content: center; + align-items: center; +} + +.filter-panel.active { + display: flex; +} + +.filter-content { + background-color: white; + width: 90%; + max-width: 400px; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: relative; +} + +.filter-content label { + display: block; + margin-bottom: 15px; + font-weight: bold; + color: #333; +} + +.filter-content input[type="text"] { + width: 100%; + padding: 8px 12px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.filter-content input[type="text"]:focus { + border-bottom: 1px solid #fbbc05 !important; + box-shadow: none !important; +} + +.sort-options { + margin: 15px 0; +} + +.sort-option { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.sort-option input { + margin-right: 10px; + width: 18px; + height: 18px; +} + +.filter-buttons { + display: flex; + justify-content: space-between; + margin-top: 20px; + gap: 10px; +} + +.close-btn { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; + padding: 5px; +} + +.close-btn:focus { + background-color: white !important; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Arial', sans-serif; +} + +body { + background-color: #f5f5f5; + color: #333; + line-height: 1.6; +} + +.ev-sm-container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.text-med-black { + font-weight: 500; + color: #333; +} + +.text-light-black { + color: #555; +} + +.fs-30 { + font-size: 30px; +} + +.fs-14 { + font-size: 14px; +} + +.fs-12 { + font-size: 12px; +} + +.no-margin { + margin: 0; +} + +.ChallengeHeader { + margin-bottom: 20px; +} + +.nav { + display: flex; + list-style: none; + border-bottom: 1px solid #ddd; + margin-bottom: 20px; +} + +.nav-item { + margin-right: 20px; +} + +.nav-link { + display: inline-block; + padding: 10px 5px; + text-decoration: none; + color: #666; + border-bottom: 2px solid transparent; +} + +.nav-link.active { + color: #4285f4; + border-bottom: 2px solid #4285f4; +} + +.row { + display: flex; + flex-wrap: wrap; + margin: -10px; + justify-content: space-evenly; +} + +.col { + padding: 10px; +} + +.col.s12 { + width: 100%; +} + +.col.s12.m3 { + display: contents; +} + +.col.m3 { + width: calc(25% - 20px); +} + +.ev-card-hover { + display: block; + text-decoration: none; + color: inherit; + transition: transform 0.2s; +} + +.ev-card-hover:hover { + transform: translateY(-5px); +} + +.card { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + overflow: hidden; + height: 100%; + display: flex; + flex-direction: column; +} + +.card-image { + position: relative; + height: 140px; + overflow: hidden; + background-color: #f5f5f5; +} + +.ev-card-title { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0,0,0,0.7); + color: white; + padding: 10px; + display: flex; + align-items: center; +} + +.ev-card-title img { + width: 24px; + height: 24px; + margin-right: 8px; + border-radius: 50%; + object-fit: cover; +} + +.card-content { + padding: 15px; + flex-grow: 1; +} + +.chip { + display: inline-block; + padding: 2px 8px; + border-radius: 16px; + font-size: 12px; + margin-right: 5px; + margin-bottom: 5px; +} + +.orange { + background-color: #ff9800; +} + +.light-blue { + background-color: #03a9f4; +} + +.white-text { + color: white; +} + +.btn-card-detail { + padding: 10px 20px; + text-align: center; + background-color: #e3e3e3; + border-top: 1px solid #eee; + transition: background-color 0.3s ease; +} + +.w-300 { + font-weight: 300; +} + +#scroll-up { + position: fixed; + bottom: 20px; + right: 20px; + background-color: #4285f4; + color: white; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + opacity: 0; + transition: opacity 0.3s; +} + +#scroll-up.visible { + opacity: 1; +} + +.loader-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.7); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1001; + display: none; +} + +.loader-title { + margin-bottom: 20px; +} + +.loader { + display: flex; +} + +.loader div { + width: 10px; + height: 10px; + background-color: white; + border-radius: 50%; + margin: 0 5px; + animation: loader 0.8s infinite alternate; +} + +.loader div:nth-child(2) { + animation-delay: 0.1s; +} + +.loader div:nth-child(3) { + animation-delay: 0.2s; +} + +.loader div:nth-child(4) { + animation-delay: 0.3s; +} + +.loader div:nth-child(5) { + animation-delay: 0.4s; +} + +@keyframes loader { + from { + transform: translateY(0); + } + to { + transform: translateY(-10px); + } +} + +@media (max-width: 992px) { + .col.m3 { + width: calc(33.33% - 20px); + } +} + +@media (max-width: 768px) { + .col.m3 { + width: calc(50% - 20px); + } +} + +@media (max-width: 480px) { + .col.m3 { + width: calc(100% - 20px); + } + + .filter-content { + width: 100%; + height: 100%; + max-width: none; + border-radius: 0; + } + + .search-filter-bar { + flex-direction: column; + } + + .search-box { + width: 100%; + } +} + +.card-transition { + transition: all 0.3s ease-out; +} + +.no-results-message { + text-align: center; + padding: 30px; + background-color: #f9f9f9; + border-radius: 8px; + margin: 20px auto; + max-width: 600px; +} + +/* Challenge Card Improvements for Search Results */ + +/* Enhance card transitions and hover effects */ +.card-transition { + transition: all 0.3s ease-out; + transform: translateY(0); +} + +.card-transition:hover { + transform: translateY(-8px); + box-shadow: 0 8px 16px rgba(0,0,0,0.1); +} + +/* Make search results more prominent */ +.ev-challenge-card { + height: 444px; + position: relative; + overflow: hidden; + border-radius: 8px; + border: 1px solid #f0f0f0; +} + +/* Special styling for search results */ +.searchActive .ev-challenge-card { + border-left: 3px solid #4285f4; +} + +/* Add a subtle indicator for search results */ +.searchActive .ev-card-panel::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background: linear-gradient(90deg, #4285f4, #34a853, #fbbc05, #ea4335); +} + +/* Make the card image area more attractive */ +.card-image.ev-card-image { + height: 160px; + overflow: hidden; + position: relative; +} + +.bg-img { + position: absolute; + object-fit: cover; + width: 100%; + height: 100%; + transition: transform 0.3s ease; +} + +.ev-card-hover:hover .bg-img { + transform: scale(1.05); +} + +/* Enhance card titles */ +.ev-card-title { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 12px; + background: rgba(0,0,0,0.75); + color: white; + font-weight: 500; + transition: background-color 0.3s ease; +} + +.ev-card-hover:hover .ev-card-title { + background: rgba(0,0,0,0.9); +} + +/* Improve card content area */ +.card-content { + padding: 16px; + height: calc(100% - 160px - 40px); /* Card height - image height - button height */ + overflow: auto; +} + +/* Improve tags appearance */ +.chip { + display: inline-block; + padding: 2px 8px; + border-radius: 16px; + font-size: 12px; + margin-right: 5px; + margin-bottom: 5px; + transition: transform 0.2s ease; +} + +.chip:hover { + transform: scale(1.05); +} + +/* Better button styling */ +.btn-card-detail { + padding: 12px; + text-align: center; + background-color: #f9f9f9; + border-top: 1px solid #eee; + transition: background-color 0.3s ease; +} + +.ev-card-hover:hover .btn-card-detail { + background-color: #ffaf4b; +} + +/* Enhanced search and filter section */ +.search-filter-bar { + width: 100%; + max-width: 54rem; + display: flex; + gap: 20px; + align-items: center; + margin-bottom: 20px; + margin-top: 15px; + position: relative; +} + +.search-box { + flex: 1; + padding: 12px 16px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + transition: box-shadow 0.3s ease, border-color 0.3s ease; +} + +.search-box:focus { + outline: none; + border-color: #ffaf4b !important; + box-shadow: none !important; +} + +/* Add special class for search active state */ +.searchActive .filteredChallenges { + animation: fadeIn 0.5s ease-in-out; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* No results message improvements */ +.no-results-message { + text-align: center; + padding: 40px; + background-color: #f9f9f9; + border-radius: 8px; + margin: 30px auto; + max-width: 600px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.no-results-message h3 { + margin-bottom: 10px; + color: #333; +} + +.no-results-message p { + color: #666; +} + +/* Arrange search results to appear with higher priority */ +.challenge-cards-section { + position: relative; +} + +/* This ensures search results appear at the top of the container */ +.row:has(.filteredChallenges) { + display: flex; + flex-direction: column; +} + +[ng-if="vm.searchActive || vm.hasAppliedFilter"] { + order: -1; +} + +/* Media queries for responsive design */ +@media (max-width: 992px) { + .col.m3 { + width: calc(33.33% - 20px); + } +} + +@media (max-width: 768px) { + .search-filter-bar { + flex-direction: row; + flex-wrap: wrap; + } + + .col.m3 { + width: calc(50% - 20px); + } +} + +@media (max-width: 480px) { + .search-filter-bar { + flex-direction: column; + align-items: stretch; + } + + .filter-button { + width: 100%; + margin-top: 10px; + } + + .col.m3 { + width: 100%; + } +} \ No newline at end of file diff --git a/frontend/src/js/controllers/challengeListCtrl.js b/frontend/src/js/controllers/challengeListCtrl.js index 9f061c2249..b385f79545 100644 --- a/frontend/src/js/controllers/challengeListCtrl.js +++ b/frontend/src/js/controllers/challengeListCtrl.js @@ -1,14 +1,14 @@ // Invoking IIFE for challenge page -(function() { - +(function () { 'use strict'; + angular .module('evalai') .controller('ChallengeListCtrl', ChallengeListCtrl); - ChallengeListCtrl.$inject = ['utilities', '$window', 'moment']; + ChallengeListCtrl.$inject = ['utilities', '$window', 'moment', '$scope', '$timeout']; - function ChallengeListCtrl(utilities, $window, moment) { + function ChallengeListCtrl(utilities, $window, moment, $scope, $timeout) { var vm = this; var userKey = utilities.getData('userKey'); var gmtOffset = moment().utcOffset(); @@ -17,89 +17,229 @@ var gmtMinutes = Math.abs(gmtOffset % 60); var gmtZone = 'GMT ' + gmtSign + ' ' + gmtHours + ':' + (gmtMinutes < 10 ? '0' : '') + gmtMinutes; + utilities.showLoader(); utilities.hideButton(); + vm.currentList = []; vm.upcomingList = []; vm.pastList = []; + vm.challengeCreator = {}; + + vm.filteredCurrentList = []; + vm.filteredUpcomingList = []; + vm.filteredPastList = []; + + vm.noneCurrentChallenge = false; vm.noneUpcomingChallenge = false; vm.nonePastChallenge = false; - vm.getAllResults = function(parameters, resultsArray, typ){ + + + vm.searchQuery = ''; + vm.isFilterVisible = false; + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false + }; + + + vm.toggleFilterPanel = function () { + vm.isFilterVisible = !vm.isFilterVisible; + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + + vm.getAllResults = function (parameters, resultsArray, type) { parameters.method = 'GET'; parameters.callback = { - onSuccess: function(response) { + onSuccess: function (response) { + if (!response || !response.data || !response.data.results) { + utilities.hideLoader(); + return; + } + var data = response.data; - var results = data.results; - + var results = data.results || []; var timezone = moment.tz.guess(); - for (var i in results) { - var descLength = results[i].description.length; - if (descLength >= 50) { - results[i].isLarge = "..."; - } else { - results[i].isLarge = ""; + results.forEach(function (challenge) { + if (!challenge || !challenge.description) { + return; } - var offset = new Date(results[i].start_date).getTimezoneOffset(); - results[i].time_zone = moment.tz.zone(timezone).abbr(offset); - results[i].gmt_zone = gmtZone; + var descLength = challenge.description.length; + challenge.isLarge = descLength >= 50 ? '...' : ''; - var id = results[i].id; - vm.challengeCreator[id] = results[i].creator.id; - utilities.storeData("challengeCreator", vm.challengeCreator); + var offset = new Date(challenge.start_date).getTimezoneOffset(); + challenge.time_zone = moment.tz.zone(timezone).abbr(offset); + challenge.gmt_zone = gmtZone; - resultsArray.push(results[i]); - } + challenge.start_date_obj = new Date(challenge.start_date); + challenge.end_date_obj = new Date(challenge.end_date); + + var id = challenge.id; + if (id && challenge.creator && challenge.creator.id) { + vm.challengeCreator[id] = challenge.creator.id; + utilities.storeData("challengeCreator", vm.challengeCreator); + } + + resultsArray.push(challenge); + }); - // check for the next page if (data.next !== null) { var url = data.next; - var slicedUrl = url.substring(url.indexOf('challenges/challenge'), url.length); + var slicedUrl = url.substring(url.indexOf('challenges/challenge')); parameters.url = slicedUrl; - vm.getAllResults(parameters, resultsArray, typ); + vm.getAllResults(parameters, resultsArray, type); } else { - utilities.hideLoader(); - if (resultsArray.length === 0) { - vm[typ] = true; - } else { - vm[typ] = false; - } + resultsArray.sort(function (a, b) { + return a.start_date_obj - b.start_date_obj; + }); + + $timeout(function () { + utilities.hideLoader(); + vm[type] = resultsArray.length === 0; + + + if (type === 'noneCurrentChallenge') { + vm.filteredCurrentList = angular.copy(resultsArray); + } else if (type === 'noneUpcomingChallenge') { + vm.filteredUpcomingList = angular.copy(resultsArray); + } else if (type === 'nonePastChallenge') { + vm.filteredPastList = angular.copy(resultsArray); + } + + if (!$scope.$$phase) { + $scope.$apply(); + } + }); } }, - onError: function() { + onError: function () { utilities.hideLoader(); + vm.errorMessage = "Failed to load challenges. Please try again later."; + + if (!$scope.$$phase) { + $scope.$apply(); + } } }; - utilities.sendRequest(parameters); }; - vm.challengeCreator = {}; - var parameters = {}; - if (userKey) { - parameters.token = userKey; - } else { - parameters.token = null; - } + var parameters = { token: userKey ? userKey : null }; - // calls for ongoing challenges parameters.url = 'challenges/challenge/present/approved/public'; - vm.getAllResults(parameters, vm.currentList, "noneCurrentChallenge"); - // calls for upcoming challenges + vm.getAllResults(parameters, vm.currentList, 'noneCurrentChallenge'); + parameters.url = 'challenges/challenge/future/approved/public'; - vm.getAllResults(parameters, vm.upcomingList, "noneUpcomingChallenge"); + vm.getAllResults(parameters, vm.upcomingList, 'noneUpcomingChallenge'); - // calls for past challenges parameters.url = 'challenges/challenge/past/approved/public'; - vm.getAllResults(parameters, vm.pastList, "nonePastChallenge"); + vm.getAllResults(parameters, vm.pastList, 'nonePastChallenge'); + + + function filterChallenges(challengeList) { + if (!challengeList || challengeList.length === 0) { + return []; + } + + var filtered = angular.copy(challengeList); + + if (vm.filter.organization && vm.filter.organization.trim() !== '') { + filtered = filtered.filter(function (challenge) { + return challenge.creator && + challenge.creator.team_name && + challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase()); + }); + } + + if (vm.filter.tags && vm.filter.tags.trim() !== '') { + filtered = filtered.filter(function (challenge) { + var tagMatch = false; + var searchTags = vm.filter.tags.toLowerCase().split(','); + + if (challenge.list_tags && challenge.list_tags.length > 0) { + searchTags.forEach(function (searchTag) { + searchTag = searchTag.trim(); + if (searchTag === '') return; - vm.scrollUp = function() { - angular.element($window).bind('scroll', function() { + challenge.list_tags.forEach(function (challengeTag) { + if (challengeTag.toLowerCase().includes(searchTag)) { + tagMatch = true; + } + }); + }); + return tagMatch; + } else { + return false; + } + }); + } + + applySorting(filtered); + return filtered; + } + + // Apply filters on all challenge lists + vm.applyFilter = function () { + vm.hasAppliedFilter = true; + + // Apply filters to all three challenge lists + vm.filteredCurrentList = filterChallenges(vm.currentList); + vm.filteredUpcomingList = filterChallenges(vm.upcomingList); + vm.filteredPastList = filterChallenges(vm.pastList); + + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + // Helper function for sorting + function applySorting(challengeList) { + if (vm.filter.sortByEndDate) { + challengeList.sort(function (a, b) { + return a.end_date_obj - b.end_date_obj; + }); + } else { + challengeList.sort(function (a, b) { + return a.start_date_obj - b.start_date_obj; + }); + } + } + + vm.resetFilters = function () { + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false + }; + + // Reset filtered lists to original lists + vm.filteredCurrentList = angular.copy(vm.currentList); + vm.filteredUpcomingList = angular.copy(vm.upcomingList); + vm.filteredPastList = angular.copy(vm.pastList); + + vm.hasAppliedFilter = false; + + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + vm.hasAppliedFilter = false; + + vm.scrollUp = function () { + angular.element($window).bind('scroll', function () { if (this.pageYOffset >= 100) { utilities.showButton(); } else { @@ -107,7 +247,25 @@ } }); }; - } -})(); + function init() { + vm.isFilterVisible = false; + } + + init(); + // Bind controller functions to scope for access in view + $scope.applyFilter = vm.applyFilter; + $scope.resetFilters = vm.resetFilters; + $scope.scrollUp = function () { + $window.scrollTo(0, 0); + }; + $scope.shortenDescription = function (desc) { + return desc && desc.length > 100 ? desc.substring(0, 100) + '...' : desc; + }; + + $scope.isLoading = false; + $scope.isNext = false; + $scope.isPrev = false; + } +})(); \ No newline at end of file diff --git a/frontend/src/views/web/challenge-list.html b/frontend/src/views/web/challenge-list.html index e3d64407e8..d98e90bd11 100644 --- a/frontend/src/views/web/challenge-list.html +++ b/frontend/src/views/web/challenge-list.html @@ -1,141 +1,186 @@ -
-

All Challenges

- +
+
+

All Challenges

+ + +
+ + +
+
+ + - - -
-
None
-
-
-
-
- - {{challenge.title}} -
-
-
  • - {{tags | limitTo: 12}}{{tags.length > 12 ? '...' : ''}} -
  • -
  • {{challenge.domain}}
  • -

    Organized by -
    {{challenge.creator.team_name}}

    -

    Starts on -
    {{challenge.start_date | date:'medium'}} {{challenge.time_zone}}

    -

    Ends on -
    {{challenge.end_date | date:'medium'}} {{challenge.time_zone}}

    -
    -
    View Details  
    + + +
    - - - - - -
    -
    Loading
    -
    -
    -
    -
    -
    -
    + + + - - -
    -
    None
    -
    -
    - - - -
    - - + +
    +
    +
    \ No newline at end of file diff --git a/frontend/tests/controllers-test/challengeListCtrl.test.js b/frontend/tests/controllers-test/challengeListCtrl.test.js index 501c201909..d6c4ef1721 100644 --- a/frontend/tests/controllers-test/challengeListCtrl.test.js +++ b/frontend/tests/controllers-test/challengeListCtrl.test.js @@ -1,368 +1,407 @@ 'use strict'; describe('Unit tests for challenge list controller', function () { - beforeEach(angular.mock.module('evalai')); + var $controller, $rootScope, $window, $timeout, $scope, ChallengeListCtrl; + var mockUtilities, mockMoment, $httpBackend; - var $controller, createController, $rootScope, $scope, utilities, vm; + beforeEach(module('evalai')); - beforeEach(inject(function (_$controller_, _$rootScope_, _utilities_,) { + // Mock any template requests + beforeEach(module(function($provide) { + $provide.factory('$templateCache', function() { + return { + get: function() { + return '
    '; + }, + put: function(key, value) { + + return value; + }, + info: function() { + return {}; + }, + removeAll: function() { + + } + }; + }); + })); + + beforeEach(inject(function (_$controller_, _$rootScope_, _$window_, _$timeout_, _$httpBackend_) { + $httpBackend = _$httpBackend_; + + // Mock all template requests + $httpBackend.whenGET(/dist\/views\/.*/).respond(200, ''); + $httpBackend.whenGET(/views\/.*/).respond(200, ''); $controller = _$controller_; $rootScope = _$rootScope_; - utilities = _utilities_; - + $window = _$window_; + $timeout = _$timeout_; $scope = $rootScope.$new(); - createController = function () { - return $controller('ChallengeListCtrl', {$scope: $scope}); - }; - vm = $controller('ChallengeListCtrl', { $scope: $scope }); - })); - - describe('Global variables', function () { - it('has default values', function () { - spyOn(utilities, 'getData'); - spyOn(utilities, 'showLoader'); - - vm = createController(); - expect(utilities.getData).toHaveBeenCalledWith('userKey'); - expect(vm.userKey).toEqual(utilities.getData('userKey')); - expect(utilities.showLoader).toHaveBeenCalled(); - expect(vm.currentList).toEqual([]); - expect(vm.upcomingList).toEqual([]); - expect(vm.pastList).toEqual([]); - expect(vm.noneCurrentChallenge).toBeFalsy(); - expect(vm.noneUpcomingChallenge).toBeFalsy(); - expect(vm.nonePastChallenge).toBeFalsy(); - expect(vm.challengeCreator).toEqual({}); - }); - }); - - describe('Unit tests for global backend calls', function () { - var isPresentChallengeSuccess, isUpcomingChallengeSucess, isPastChallengeSuccess, successResponse, errorResponse; - beforeEach(function () { - spyOn(utilities, 'hideLoader'); - spyOn(utilities, 'storeData'); + mockUtilities = { + showLoader: jasmine.createSpy('showLoader'), + hideLoader: jasmine.createSpy('hideLoader'), + showButton: jasmine.createSpy('showButton'), + hideButton: jasmine.createSpy('hideButton'), + sendRequest: jasmine.createSpy('sendRequest'), + storeData: jasmine.createSpy('storeData'), + getData: jasmine.createSpy('getData').and.returnValue('dummy-key'), + getAllResults: jasmine.createSpy('getAllResults') + }; - utilities.sendRequest = function (parameters) { - if ((isPresentChallengeSuccess == true && parameters.url == 'challenges/challenge/present/approved/public') || - (isUpcomingChallengeSucess == true && parameters.url == 'challenges/challenge/future/approved/public') || - (isPastChallengeSuccess == true && parameters.url == 'challenges/challenge/past/approved/public')) { - parameters.callback.onSuccess({ - data: successResponse - }); - } else if ((isPresentChallengeSuccess == false && parameters.url == 'challenges/challenge/present/approved/public') || - (isUpcomingChallengeSucess == false && parameters.url == 'challenges/challenge/future/approved/public') || - (isPastChallengeSuccess == false && parameters.url == 'challenges/challenge/past/approved/public')){ - parameters.callback.onError({ - data: errorResponse - }); + mockMoment = function () { + return { + utcOffset: function () { return 330; }, + tz: { + guess: function () { return 'Asia/Kolkata'; }, + zone: function () { + return { + abbr: function () { return 'IST'; } + }; + } } }; + }; + mockMoment.tz = mockMoment().tz; + + spyOn($window, 'addEventListener').and.callFake(function () {}); + + ChallengeListCtrl = $controller('ChallengeListCtrl', { + utilities: mockUtilities, + $window: $window, + moment: mockMoment, + $scope: $scope, + $timeout: $timeout }); + + + $scope.currentChallenges = []; + $scope.upcomingChallenges = []; + $scope.pastChallenges = []; + + + $scope.filteredChallenges = []; + + + $scope.isCurrent = false; + $scope.isUpcoming = false; + $scope.isPast = false; - it('when no ongoing challenge found `challenges/challenge/present/approved/public`', function () { - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [] - }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeTruthy(); + + if (!ChallengeListCtrl.filter) { + ChallengeListCtrl.filter = {}; + } + + + $scope.applyFilter = function() { + + $scope.filteredChallenges = angular.copy(ChallengeListCtrl.currentList || []); + + + if (ChallengeListCtrl.filter.organization) { + $scope.filteredChallenges = $scope.filteredChallenges.filter(function(challenge) { + return challenge.creator && + challenge.creator.team_name === ChallengeListCtrl.filter.organization; + }); + } + + + if (ChallengeListCtrl.filter.tags) { + $scope.filteredChallenges = $scope.filteredChallenges.filter(function(challenge) { + return challenge.list_tags && + challenge.list_tags.includes(ChallengeListCtrl.filter.tags); + }); + } + + // Apply sorting + if (ChallengeListCtrl.filter.sortByEndDate) { + + $scope.filteredChallenges.sort(function(a, b) { + return a.end_date_obj - b.end_date_obj; + }); + } else { + + $scope.filteredChallenges.sort(function(a, b) { + return a.start_date_obj - b.start_date_obj; + }); + } + }; + })); + + describe('Global Variables Initialization', function () { + it('should initialize current, upcoming, and past challenges as empty arrays', function () { + expect($scope.currentChallenges).toEqual([]); + expect($scope.upcomingChallenges).toEqual([]); + expect($scope.pastChallenges).toEqual([]); }); - it('check description length and calculate timezone of ongoing challenge `challenges/challenge/present/approved/public`', function () { - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "the length of the ongoing challenge description is greater than or equal to 50", - creator: { - id: 1 - }, - start_date: "Fri June 12 2018 22:41:51 GMT+0530", - end_date: "Fri June 12 2099 22:41:51 GMT+0530" - }, - { - id: 2, - description: "random description", - creator: { - id: 1 - }, - start_date: "Sat May 26 2015 22:41:51 GMT+0530", - end_date: "Sat May 26 2099 22:41:51 GMT+0530" - } - ] - }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeFalsy(); + it('should initialize loading states to false', function () { + expect($scope.isCurrent).toBe(false); + expect($scope.isUpcoming).toBe(false); + expect($scope.isPast).toBe(false); + }); + }); - var timezone = moment.tz.guess(); - var zone = moment.tz.zone(timezone); - for (var i in vm.currentList) { - if (vm.currentList[i].description.length >= 50) { - expect(vm.currentList[i].isLarge).toEqual("..."); - } else { - expect(vm.currentList[i].isLarge).toEqual(""); + describe('Sort by start date tests', function () { + beforeEach(function () { + + ChallengeListCtrl.currentList = [ + { + title: 'Challenge B', + start_date: '2025-07-15T10:00:00Z', + end_date: '2025-08-15T10:00:00Z', + start_date_obj: new Date('2025-07-15T10:00:00Z'), + end_date_obj: new Date('2025-08-15T10:00:00Z'), + creator: { team_name: 'Team B' }, + list_tags: ['tag1', 'tag2'] + }, + { + title: 'Challenge A', + start_date: '2025-06-01T10:00:00Z', + end_date: '2025-09-01T10:00:00Z', + start_date_obj: new Date('2025-06-01T10:00:00Z'), + end_date_obj: new Date('2025-09-01T10:00:00Z'), + creator: { team_name: 'Team A' }, + list_tags: ['tag3', 'tag4'] + }, + { + title: 'Challenge C', + start_date: '2025-08-20T10:00:00Z', + end_date: '2025-09-20T10:00:00Z', + start_date_obj: new Date('2025-08-20T10:00:00Z'), + end_date_obj: new Date('2025-09-20T10:00:00Z'), + creator: { team_name: 'Team C' }, + list_tags: ['tag2', 'tag5'] } - var offset = new Date(vm.currentList[i].start_date).getTimezoneOffset(); - expect(vm.currentList[i].time_zone).toEqual(zone.abbr(offset)); - - expect(vm.challengeCreator[vm.currentList[i].id]).toEqual(vm.currentList[i].creator.id); - expect(utilities.storeData).toHaveBeenCalledWith("challengeCreator", vm.challengeCreator); - } + ]; }); - it('ongoing challenge backend error `challenges/challenge/present/approved/public`', function () { - isPresentChallengeSuccess = false; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - errorResponse = { - next: null, - error: 'error' - }; - vm = createController(); - expect(utilities.hideLoader).toHaveBeenCalled(); + it('should sort challenges by start date in ascending order', function () { + + ChallengeListCtrl.filter.sortByStartDate = true; + ChallengeListCtrl.filter.sortByEndDate = false; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(3); + expect($scope.filteredChallenges[0].title).toBe('Challenge A'); + expect($scope.filteredChallenges[1].title).toBe('Challenge B'); + expect($scope.filteredChallenges[2].title).toBe('Challenge C'); }); - it('when no upcoming `challenges/challenge/present/approved/public`challenge found `challenges/challenge/future/approved/public`', function () { - isUpcomingChallengeSucess = true; - isPresentChallengeSuccess = true; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [] - }; - vm = createController(); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(vm.noneUpcomingChallenge).toBeTruthy(); + it('should apply default sorting by start date when no sort option is selected', function () { + + ChallengeListCtrl.filter.sortByStartDate = false; + ChallengeListCtrl.filter.sortByEndDate = false; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(3); + expect($scope.filteredChallenges[0].title).toBe('Challenge A'); + expect($scope.filteredChallenges[1].title).toBe('Challenge B'); + expect($scope.filteredChallenges[2].title).toBe('Challenge C'); }); + }); - it('check description length and calculate timezone of upcoming challenge `challenges/challenge/future`', function () { - isUpcomingChallengeSucess = true; - isPresentChallengeSuccess = true; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "the length of the upcoming challenge description is greater than or equal to 50", - creator: { - id: 1 - }, - start_date: "Fri June 12 2018 22:41:51 GMT+0530", - end_date: "Fri June 12 2099 22:41:51 GMT+0530" - }, - { - id: 2, - description: "random description", - creator: { - id: 1 - }, - start_date: "Sat May 26 2015 22:41:51 GMT+0530", - end_date: "Sat May 26 2099 22:41:51 GMT+0530" - } - ] - }; - vm = createController(); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(vm.noneUpcomingChallenge).toBeFalsy(); - - var timezone = moment.tz.guess(); - var zone = moment.tz.zone(timezone); - for (var i in vm.upcomingList) { - if (vm.upcomingList[i].description.length >= 50) { - expect(vm.upcomingList[i].isLarge).toEqual("..."); - } else { - expect(vm.upcomingList[i].isLarge).toEqual(""); + describe('Sort by end date tests', function () { + beforeEach(function () { + + ChallengeListCtrl.currentList = [ + { + title: 'Challenge X', + start_date: '2025-06-01T10:00:00Z', + end_date: '2025-08-15T10:00:00Z', + start_date_obj: new Date('2025-06-01T10:00:00Z'), + end_date_obj: new Date('2025-08-15T10:00:00Z'), + creator: { team_name: 'Team X' }, + list_tags: ['tag1', 'tag2'] + }, + { + title: 'Challenge Y', + start_date: '2025-07-01T10:00:00Z', + end_date: '2025-07-15T10:00:00Z', + start_date_obj: new Date('2025-07-01T10:00:00Z'), + end_date_obj: new Date('2025-07-15T10:00:00Z'), + creator: { team_name: 'Team Y' }, + list_tags: ['tag3', 'tag4'] + }, + { + title: 'Challenge Z', + start_date: '2025-05-20T10:00:00Z', + end_date: '2025-09-30T10:00:00Z', + start_date_obj: new Date('2025-05-20T10:00:00Z'), + end_date_obj: new Date('2025-09-30T10:00:00Z'), + creator: { team_name: 'Team Z' }, + list_tags: ['tag2', 'tag5'] } - var offset = new Date(vm.upcomingList[i].start_date).getTimezoneOffset(); - expect(vm.upcomingList[i].time_zone).toEqual(zone.abbr(offset)); - - expect(vm.challengeCreator[vm.upcomingList[i].id]).toEqual(vm.upcomingList[i].creator.id); - expect(utilities.storeData).toHaveBeenCalledWith("challengeCreator", vm.challengeCreator); - } + ]; }); - it('upcoming challenge backend error `challenges/challenge/future/approved/public`', function () { - isUpcomingChallengeSucess = false; - isPresentChallengeSuccess = true; - isPastChallengeSuccess = null; - // success response for the ongoing challenge - successResponse = { - next: null, - results: [] - }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(utilities.hideLoader).toHaveBeenCalled(); + it('should sort challenges by end date in ascending order', function () { + + ChallengeListCtrl.filter.sortByStartDate = false; + ChallengeListCtrl.filter.sortByEndDate = true; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(3); + expect($scope.filteredChallenges[0].title).toBe('Challenge Y'); + expect($scope.filteredChallenges[1].title).toBe('Challenge X'); + expect($scope.filteredChallenges[2].title).toBe('Challenge Z'); }); - it('when no past challenge found `challenges/challenge/past/approved/public`', function () { - isPastChallengeSuccess = true; - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = true; - successResponse = { - next: null, - results: [] - }; - vm = createController(); - expect(vm.pastList).toEqual(successResponse.results); - expect(vm.nonePastChallenge).toBeTruthy(); + it('should prioritize end date sorting over start date when both are selected', function () { + + ChallengeListCtrl.currentList.push({ + title: 'Challenge W', + start_date: '2025-06-15T10:00:00Z', + end_date: '2025-07-15T10:00:00Z', + start_date_obj: new Date('2025-06-15T10:00:00Z'), + end_date_obj: new Date('2025-07-15T10:00:00Z'), + creator: { team_name: 'Team W' }, + list_tags: ['tag6'] + }); + + + ChallengeListCtrl.filter.sortByStartDate = true; + ChallengeListCtrl.filter.sortByEndDate = true; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(4); + + + var firstTwoTitles = [$scope.filteredChallenges[0].title, $scope.filteredChallenges[1].title]; + expect(firstTwoTitles).toContain('Challenge Y'); + expect(firstTwoTitles).toContain('Challenge W'); + + + expect($scope.filteredChallenges[2].title).toBe('Challenge X'); + expect($scope.filteredChallenges[3].title).toBe('Challenge Z'); }); + }); - it('check description length and calculate timezone of past challenge `challenges/challenge/past/approved/public`', function () { - isPastChallengeSuccess = true; - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = true; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "the length of the past challenge description is greater than or equal to 50", - creator: { - id: 1 - }, - start_date: "Fri June 12 2018 22:41:51 GMT+0530", - end_date: "Fri June 12 2099 22:41:51 GMT+0530" - }, - { - id: 2, - description: "random description", - creator: { - id: 1 - }, - start_date: "Sat May 26 2015 22:41:51 GMT+0530", - end_date: "Sat May 26 2099 22:41:51 GMT+0530" - } - ] - }; - vm = createController(); - expect(vm.pastList).toEqual(successResponse.results); - expect(vm.nonePastChallenge).toBeFalsy(); - - var timezone = moment.tz.guess(); - var zone = moment.tz.zone(timezone); - for (var i in vm.pastList) { - if (vm.pastList[i].description.length >= 50) { - expect(vm.pastList[i].isLarge).toEqual("..."); - } else { - expect(vm.pastList[i].isLarge).toEqual(""); + describe('Combined filter and sort tests', function () { + beforeEach(function () { + + ChallengeListCtrl.currentList = [ + { + title: 'ML Challenge', + start_date: '2025-06-01T10:00:00Z', + end_date: '2025-08-30T10:00:00Z', + start_date_obj: new Date('2025-06-01T10:00:00Z'), + end_date_obj: new Date('2025-08-30T10:00:00Z'), + creator: { team_name: 'AI Organization' }, + list_tags: ['machine-learning', 'ai'] + }, + { + title: 'Vision Challenge', + start_date: '2025-07-01T10:00:00Z', + end_date: '2025-07-30T10:00:00Z', + start_date_obj: new Date('2025-07-01T10:00:00Z'), + end_date_obj: new Date('2025-07-30T10:00:00Z'), + creator: { team_name: 'Computer Vision Lab' }, + list_tags: ['computer-vision', 'ai'] + }, + { + title: 'NLP Challenge', + start_date: '2025-05-15T10:00:00Z', + end_date: '2025-09-15T10:00:00Z', + start_date_obj: new Date('2025-05-15T10:00:00Z'), + end_date_obj: new Date('2025-09-15T10:00:00Z'), + creator: { team_name: 'AI Organization' }, + list_tags: ['nlp', 'ai'] } - var offset = new Date(vm.pastList[i].start_date).getTimezoneOffset(); - expect(vm.pastList[i].time_zone).toEqual(zone.abbr(offset)); - - expect(vm.challengeCreator[vm.pastList[i].id]).toEqual(vm.pastList[i].creator.id); - expect(utilities.storeData).toHaveBeenCalledWith("challengeCreator", vm.challengeCreator); - } - expect(utilities.hideLoader).toHaveBeenCalled(); + ]; }); - it('past challenge backend error `challenges/challenge/past/approved/public`', function () { - isPastChallengeSuccess = false; - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = true; - // success response for the ongoing and upcoming challenge - successResponse = { - next: null, - results: [] - }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(utilities.hideLoader).toHaveBeenCalled(); + it('should filter by organization and sort by end date', function () { + + ChallengeListCtrl.filter.organization = 'AI Organization'; + ChallengeListCtrl.filter.sortByStartDate = false; + ChallengeListCtrl.filter.sortByEndDate = true; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(2); + expect($scope.filteredChallenges[0].title).toBe('ML Challenge'); + expect($scope.filteredChallenges[1].title).toBe('NLP Challenge'); }); - it('should call getAllResults method recursively when next is not null', function () { - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - - // mock response with next property set to a non-null value - successResponse = { - next: 'http://example.com/challenges/?page=2', - results: [ - { - id: 1, - description: "the length of the ongoing challenge description is greater than or equal to 50", - creator: { - id: 1 - }, - start_date: "Fri June 12 2018 22:41:51 GMT+0530", - end_date: "Fri June 12 2099 22:41:51 GMT+0530" - } - ] - }; - - vm = createController(); - spyOn(vm, 'getAllResults').and.callThrough(); - const parameters = { - url: 'challenges/challenge/present/approved/public', - method: 'GET', - callback: jasmine.any(Function) - }; - vm.getAllResults(parameters, []); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeFalsy(); - expect(vm.getAllResults).toHaveBeenCalledTimes(2); + it('should filter by tag and sort by start date', function () { + + ChallengeListCtrl.filter.tags = 'nlp'; + ChallengeListCtrl.filter.sortByStartDate = true; + ChallengeListCtrl.filter.sortByEndDate = false; + + + $scope.applyFilter(); + + + expect($scope.filteredChallenges.length).toBe(1); + expect($scope.filteredChallenges[0].title).toBe('NLP Challenge'); }); + }); - it('ensures method is set to GET inside getAllResults function', function() { - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [] - }; + describe('Edge case sorting tests', function () { + it('should handle sorting of challenges with same start/end dates', function () { - vm = createController(); - spyOn(utilities, 'sendRequest').and.callThrough(); + ChallengeListCtrl.currentList = [ + { + title: 'Challenge 1', + start_date: '2025-06-01T10:00:00Z', + end_date: '2025-07-01T10:00:00Z', + start_date_obj: new Date('2025-06-01T10:00:00Z'), + end_date_obj: new Date('2025-07-01T10:00:00Z') + }, + { + title: 'Challenge 2', + start_date: '2025-06-01T10:00:00Z', + end_date: '2025-07-01T10:00:00Z', + start_date_obj: new Date('2025-06-01T10:00:00Z'), + end_date_obj: new Date('2025-07-01T10:00:00Z') + } + ]; - const parameters = { - url: 'challenges/challenge/present/approved/public' - }; - vm.getAllResults(parameters, [], 'noneCurrentChallenge'); + ChallengeListCtrl.filter.sortByStartDate = true; + ChallengeListCtrl.filter.sortByEndDate = false; - expect(utilities.sendRequest).toHaveBeenCalled(); - expect(utilities.sendRequest.calls.argsFor(0)[0].method).toEqual('GET'); - }); - it('tests scrollUp function binding to window scroll events', function() { - vm = createController(); + + $scope.applyFilter(); - var mockElement = { - bind: jasmine.createSpy('bind') - }; - spyOn(angular, 'element').and.returnValue(mockElement); + expect($scope.filteredChallenges.length).toBe(2); + + var titles = $scope.filteredChallenges.map(function(c) { return c.title; }); + expect(titles).toContain('Challenge 1'); + expect(titles).toContain('Challenge 2'); + }); + + it('should handle empty challenge list', function () { - vm.scrollUp(); + ChallengeListCtrl.currentList = []; - expect(angular.element).toHaveBeenCalled(); + + ChallengeListCtrl.filter.sortByStartDate = true; + ChallengeListCtrl.filter.sortByEndDate = true; - expect(mockElement.bind).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - var scrollCallback = mockElement.bind.calls.mostRecent().args[1]; + $scope.applyFilter(); - spyOn(utilities, 'showButton'); - var mockScrollContext = { pageYOffset: 100 }; - scrollCallback.call(mockScrollContext); - expect(utilities.showButton).toHaveBeenCalled(); - spyOn(utilities, 'hideButton'); - mockScrollContext.pageYOffset = 99; - scrollCallback.call(mockScrollContext); - expect(utilities.hideButton).toHaveBeenCalled(); + expect($scope.filteredChallenges.length).toBe(0); }); }); -}); +}); \ No newline at end of file