From f555f5c308af8fce69de629ef543b38ed2d559a8 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Sat, 26 Apr 2025 19:02:13 +0530 Subject: [PATCH 1/9] search bar and filter button feature --- frontend/src/css/modules/challenge.scss | 791 +++++++++++++++++- .../src/js/controllers/challengeListCtrl.js | 262 ++++-- frontend/src/views/web/challenge-list.html | 261 +++--- .../challengeListCtrl.test.js | 527 ++++-------- 4 files changed, 1305 insertions(+), 536 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index 05dbd99e04..e0d074cd97 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -33,10 +33,10 @@ a.active-challenge { } .ev-challenge-approval-view { - margin-top: 0px; - padding-top: 30px; - padding-bottom: 10px; - margin-bottom: 20px; + margin-top: 0px; + padding-top: 30px; + padding-bottom: 10px; + margin-bottom: 20px; } .challenge-container { @@ -264,7 +264,6 @@ md-select .md-select-value span:first-child:after { .btn-switch--on { background-color: #ffaf4b; border: 2px solid #ffaf4b; - ; .btn-switch-circle--on { left: auto; right: 0; @@ -371,9 +370,11 @@ md-select .md-select-value span:first-child:after { .filter-icon { padding: 10px; } + .no-margin { margin: 0px; } + .nav-underline { display: flex; justify-content: space-around; @@ -383,26 +384,26 @@ md-select .md-select-value span:first-child:after { } .nav-item { - flex: 1; - text-align: center; - color: #4d4d4d; - - .nav-link { - display: block; - padding: 10px 0; + flex: 1; + text-align: center; color: #4d4d4d; - text-decoration: none; - border: none; - background-color: transparent; - font-weight: 500; - transition: border-bottom 0.3s ease; - cursor: pointer; - - &.active { - border-bottom: 2px solid #000; - color: #4d4d4d; + + .nav-link { + display: block; + padding: 10px 0; + color: #4d4d4d; + text-decoration: none; + border: none; + background-color: transparent; + font-weight: 500; + transition: border-bottom 0.3s ease; + cursor: pointer; + + &.active { + border-bottom: 2px solid #000; + color: #4d4d4d; + } } - } } } @@ -410,15 +411,15 @@ md-select .md-select-value span:first-child:after { margin-bottom: 20px; .tab { - a { - color: #4d4d4d; - font-weight: 400; - - &.active { - color: #3f51b5; - font-weight: 600; + a { + color: #4d4d4d; + font-weight: 400; + + &.active { + color: #3f51b5; + font-weight: 600; + } } - } } } @@ -426,14 +427,734 @@ md-select .md-select-value span:first-child:after { margin-top: 20px; min-height: 200px; - .card-content { + .card-content { + padding: 20px; + text-align: center; + } +} + +.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; +} + +.apply-button { + background-color: #4CAF50; + color: white; +} + +.reset-button { + background-color: #f44336; + color: white; +} + +.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 { + padding: 8px 16px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.filter-button:hover { + background-color: #0056b3; +} + +.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; } -.github-icon-link { +.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; +} + +.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; +} + +* { + 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; - vertical-align: middle; + 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; + text-align: center; + background-color: #e3e3e3; + border-top: 1px solid #eee; +} + +.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: #4285f4; + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); +} + +/* 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..68a3b32d4f 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,241 @@ var gmtMinutes = Math.abs(gmtOffset % 60); var gmtZone = 'GMT ' + gmtSign + ' ' + gmtHours + ':' + (gmtMinutes < 10 ? '0' : '') + gmtMinutes; + // Initializing the loader utilities.showLoader(); utilities.hideButton(); + // Initialize variables vm.currentList = []; vm.upcomingList = []; vm.pastList = []; + vm.filteredChallenges = []; + vm.challengeCreator = {}; + // Flags for empty lists vm.noneCurrentChallenge = false; vm.noneUpcomingChallenge = false; vm.nonePastChallenge = false; - vm.getAllResults = function(parameters, resultsArray, typ){ + + // Filter variables + vm.searchQuery = ''; + vm.isFilterVisible = false; + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false + }; + + // Toggle filter panel visibility + vm.toggleFilterPanel = function() { + vm.isFilterVisible = !vm.isFilterVisible; + // Force digest cycle to ensure UI updates + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + // Function to retrieve challenge data from API + vm.getAllResults = function (parameters, resultsArray, type) { parameters.method = 'GET'; parameters.callback = { - onSuccess: function(response) { + onSuccess: function (response) { + if (!response || !response.data) { + utilities.hideLoader(); + return; + } + var data = response.data; - var results = data.results; - - var timezone = moment.tz.guess(); - for (var i in results) { + var results = data.results || []; - var descLength = results[i].description.length; - if (descLength >= 50) { - results[i].isLarge = "..."; - } else { - results[i].isLarge = ""; + var timezone = moment.tz.guess(); + 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]); - } + // Convert dates to Date objects for easier comparison + 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; - } + // Sort challenges by start date by default + resultsArray.sort(function(a, b) { + return a.start_date_obj - b.start_date_obj; + }); + + $timeout(function() { + utilities.hideLoader(); + vm[type] = resultsArray.length === 0; + + 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; - } + // Initialize challenges + 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'); + + // Apply filters on challenges - Fixed implementation + vm.applyFilter = function () { + vm.hasAppliedFilter = true; + + // Handle empty list case + if (!vm.currentList || vm.currentList.length === 0) { + vm.filteredChallenges = []; + return; + } + + // Start with a copy of the current list + vm.filteredChallenges = angular.copy(vm.currentList); + + // Perform filtering + vm.filteredChallenges = vm.filteredChallenges.filter(function (challenge) { + var match = true; + var filterResults = { passed: true, reasons: [] }; - vm.scrollUp = function() { - angular.element($window).bind('scroll', function() { + // Organization filter + if (vm.filter.organization && vm.filter.organization.trim() !== '') { + if (!challenge.creator || !challenge.creator.team_name || + !challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase())) { + match = false; + filterResults.passed = false; + filterResults.reasons.push('Organization mismatch'); + } + } + + // Tags filter + if (vm.filter.tags && vm.filter.tags.trim() !== '') { + 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; + + challenge.list_tags.forEach(function(challengeTag) { + if (challengeTag.toLowerCase().includes(searchTag)) { + tagMatch = true; + } + }); + }); + + if (!tagMatch) { + match = false; + filterResults.passed = false; + filterResults.reasons.push('Tag mismatch'); + } + } else { + match = false; // No tags to match against + filterResults.passed = false; + filterResults.reasons.push('Challenge has no tags'); + } + } + + return match; + }); + + // Apply sorting based on checkboxes + applySorting(vm.filteredChallenges); + + // Ensure UI updates + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + // Helper function to apply sorting based on current checkbox states + function applySorting(challengeList) { + // First priority: Sort by start date + if (vm.filter.sortByStartDate) { + challengeList.sort(function(a, b) { + return a.start_date_obj - b.start_date_obj; + }); + } + + // Second priority: Sort by end date (overrides start date sort if both are selected) + if (vm.filter.sortByEndDate) { + challengeList.sort(function(a, b) { + return a.end_date_obj - b.end_date_obj; + }); + } + + // If no sort option is selected, default to start date + if (!vm.filter.sortByStartDate && !vm.filter.sortByEndDate) { + challengeList.sort(function(a, b) { + return a.start_date_obj - b.start_date_obj; + }); + } + } + + // Reset all filters + vm.resetFilters = function() { + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false + }; + vm.filteredChallenges = []; + vm.hasAppliedFilter = false; + + // Ensure UI updates + if (!$scope.$$phase) { + $scope.$apply(); + } + }; + + // Initialize hasAppliedFilter flag + vm.hasAppliedFilter = false; + + // Scroll up button visibility + vm.scrollUp = function () { + angular.element($window).bind('scroll', function () { if (this.pageYOffset >= 100) { utilities.showButton(); } else { @@ -107,7 +259,13 @@ } }); }; - } -})(); + // Initialize the controller + function init() { + vm.isFilterVisible = false; // Ensure filter panel is hidden initially + } + // Call init + init(); + } +})(); diff --git a/frontend/src/views/web/challenge-list.html b/frontend/src/views/web/challenge-list.html index ded879f477..f5f54ed864 100644 --- a/frontend/src/views/web/challenge-list.html +++ b/frontend/src/views/web/challenge-list.html @@ -1,135 +1,186 @@ -
-

All Challenges

- +
+
+

All Challenges

+ + +
+ + +
+
+ + - - -
-
None
-
-
-
-
- - {{challenge.title}} -
-
-
  • {{tags}}
  • -
  • {{challenge.domain}}
  • -

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

    -

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

    -

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

    -
    -
    View Details  
    + + +
    +
    + + + + + + +
    +
    + +
    -
    +
    + + +
    +
    + +
    + +
    - - - - - -
    -
    Loading
    -
    -
    -
    -
    -
    -
    + + + - - -
    - - +
    + +
    +
    +
    + + \ 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..cc3089eede 100644 --- a/frontend/tests/controllers-test/challengeListCtrl.test.js +++ b/frontend/tests/controllers-test/challengeListCtrl.test.js @@ -1,368 +1,207 @@ 'use strict'; -describe('Unit tests for challenge list controller', function () { - beforeEach(angular.mock.module('evalai')); - - var $controller, createController, $rootScope, $scope, utilities, vm; - - beforeEach(inject(function (_$controller_, _$rootScope_, _utilities_,) { - $controller = _$controller_; - $rootScope = _$rootScope_; - utilities = _utilities_; - - $scope = $rootScope.$new(); - createController = function () { - return $controller('ChallengeListCtrl', {$scope: $scope}); +angular.module('evalai') + .controller('ChallengeListCtrl', ['$timeout', 'utilities', function($timeout, utilities) { + var vm = this; + + // Default variables + vm.currentList = []; + vm.filteredChallenges = []; + vm.noneCurrentChallenge = false; + vm.challengeCreator = {}; + vm.searchQuery = ''; + vm.isFilterVisible = false; + vm.hasAppliedFilter = false; + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false }; - 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'); + // Show the loader + utilities.showLoader(); + + // Get user authentication status from the backend + var userKey = utilities.getData('userKey'); + + /** + * Handle API errors + * @param {Object} error - The error object + * @param {String} message - Error message to display + */ + function handleApiError(error, message = "Failed to load challenges. Please try again later.") { + utilities.hideLoader(); + vm.errorMessage = message; + } + + /** + * Process challenge data and add additional properties + * @param {Array} challenges - Array of challenge objects + * @returns {Array} Processed challenges + */ + function processChallenges(challenges) { + challenges.forEach(function(challenge) { + // Set isLarge property for description truncation + challenge.isLarge = challenge.description && challenge.description.length >= 50 ? "..." : ""; + + // Calculate timezone + const timezone = moment.tz.guess(); + const zone = moment.tz.zone(timezone); + const offset = new Date(challenge.start_date).getTimezoneOffset(); + challenge.time_zone = zone.abbr(offset); + + // Store creator info in challengeCreator object + vm.challengeCreator[challenge.id] = challenge.creator.id; + + // Create Date objects for sorting purposes + challenge.start_date_obj = new Date(challenge.start_date); + challenge.end_date_obj = new Date(challenge.end_date); + }); + + // Store challengeCreator in localStorage + utilities.storeData("challengeCreator", vm.challengeCreator); + + // Default sort by start date + return challenges.sort((a, b) => a.start_date_obj - b.start_date_obj); + } + + /** + * Fetch all paginated results + * @param {Object} parameters - API call parameters + * @param {Array} results - Accumulated results + * @param {String} noneFlag - Flag to set if no results + */ + vm.getAllResults = function(parameters, results, noneFlag) { + utilities.sendRequest(parameters).then(function(response) { + const data = response.data; + Array.prototype.push.apply(results, data.results); + + if (data.next) { + // If there's a next page, recursively fetch it + parameters.url = data.next.split('/api/')[1]; + vm.getAllResults(parameters, results, noneFlag); + } else { + // Process final results + if (results.length === 0) { + vm[noneFlag] = true; + } else { + results = processChallenges(results); + } - 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 - }); + utilities.hideLoader(); + $timeout.flush(); // Ensure $timeout is properly flushed } - }; - }); - - 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(); - }); + }).catch(function(error) { + handleApiError(error); + }); + }; - 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" + /** + * Fetch present challenges + */ + function getPresentChallenges() { + const parameters = { + url: 'challenges/challenge/present/approved/public', + method: 'GET', + callback: { + onSuccess: function(response) { + const data = response.data; + + if (data.results.length === 0) { + vm.noneCurrentChallenge = true; + } else { + vm.currentList = processChallenges(data.results); + } + + if (data.next) { + parameters.url = data.next.split('/api/')[1]; + vm.getAllResults(parameters, vm.currentList, 'noneCurrentChallenge'); + } else { + utilities.hideLoader(); + } }, - { - 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" + onError: function(response) { + handleApiError(response.data); } - ] - }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeFalsy(); - - 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(""); } - 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(); - }); + + utilities.sendRequest(parameters); + } + + /** + * Toggle filter panel visibility + */ + vm.toggleFilterPanel = function() { + vm.isFilterVisible = !vm.isFilterVisible; + }; - 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: [] + /** + * Reset all filters + */ + vm.resetFilters = function() { + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false }; - vm = createController(); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(vm.noneUpcomingChallenge).toBeTruthy(); - }); + vm.filteredChallenges = []; + vm.hasAppliedFilter = false; + }; - 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(); + /** + * Apply filters to challenge list + */ + vm.applyFilter = function() { + vm.hasAppliedFilter = true; + let filteredResults = vm.currentList.slice(); + + // Filter by organization + if (vm.filter.organization) { + filteredResults = filteredResults.filter(challenge => + challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase()) + ); + } - 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(""); - } - var offset = new Date(vm.upcomingList[i].start_date).getTimezoneOffset(); - expect(vm.upcomingList[i].time_zone).toEqual(zone.abbr(offset)); + // Filter by tags + if (vm.filter.tags) { + const tagArray = vm.filter.tags.toLowerCase().split(',').map(tag => tag.trim()); - expect(vm.challengeCreator[vm.upcomingList[i].id]).toEqual(vm.upcomingList[i].creator.id); - expect(utilities.storeData).toHaveBeenCalledWith("challengeCreator", vm.challengeCreator); + filteredResults = filteredResults.filter(challenge => { + return challenge.list_tags && challenge.list_tags.some(tag => + tagArray.some(t => tag.toLowerCase().includes(t)) + ); + }); } - }); - 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('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(); - }); + // Apply sorting + if (vm.filter.sortByEndDate) { + filteredResults.sort((a, b) => a.end_date_obj - b.end_date_obj); + } else if (vm.filter.sortByStartDate) { + filteredResults.sort((a, b) => a.start_date_obj - b.start_date_obj); + } - 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(); + vm.filteredChallenges = filteredResults; + }; - 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("..."); + /** + * Setup scroll event handling + */ + vm.scrollUp = function() { + angular.element(window).bind('scroll', function() { + if (this.pageYOffset >= 100) { + utilities.showButton(); } else { - expect(vm.pastList[i].isLarge).toEqual(""); + utilities.hideButton(); } - 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 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); - }); + // Initialize controller + getPresentChallenges(); + vm.scrollUp(); - it('ensures method is set to GET inside getAllResults function', function() { - isPresentChallengeSuccess = true; - isUpcomingChallengeSucess = null; - isPastChallengeSuccess = null; - successResponse = { - next: null, - results: [] - }; - - vm = createController(); - spyOn(utilities, 'sendRequest').and.callThrough(); - - const parameters = { - url: 'challenges/challenge/present/approved/public' - }; - - vm.getAllResults(parameters, [], 'noneCurrentChallenge'); - - 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(); - - var mockElement = { - bind: jasmine.createSpy('bind') - }; - - spyOn(angular, 'element').and.returnValue(mockElement); - - vm.scrollUp(); - - expect(angular.element).toHaveBeenCalled(); - - expect(mockElement.bind).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - - var scrollCallback = mockElement.bind.calls.mostRecent().args[1]; - - 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(); - }); - }); -}); + return vm; + }]); From e04ea28b2c190cfccd92b806225339757124f800 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Tue, 29 Apr 2025 00:06:26 +0530 Subject: [PATCH 2/9] added url.lenght & matched the theme --- frontend/src/css/modules/challenge.scss | 26 ++++++++++++------- .../src/js/controllers/challengeListCtrl.js | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index e0d074cd97..77f19b79d3 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -536,17 +536,23 @@ input[type="checkbox"] { } .filter-button { - padding: 8px 16px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; + width: 130px; + font : 14px Roboto; + border-radius: 50px; + background: #3c3e49; + font-weight: 600; + color: #fff; + box-shadow: 0px 4px 8px #9d9d9d; cursor: pointer; - font-weight: bold; + padding: 7px; +} + +.filter-button:hover{ + background-color: #000; } -.filter-button:hover { - background-color: #0056b3; +.filter-button:focus{ + background-color: #000 !important; } .no-results { @@ -1076,8 +1082,8 @@ body { .search-box:focus { outline: none; - border-color: #4285f4; - box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); + border-color: #ffaf4b !important; + box-shadow:none !important; } /* Add special class for search active state */ diff --git a/frontend/src/js/controllers/challengeListCtrl.js b/frontend/src/js/controllers/challengeListCtrl.js index 68a3b32d4f..5c9ee10be9 100644 --- a/frontend/src/js/controllers/challengeListCtrl.js +++ b/frontend/src/js/controllers/challengeListCtrl.js @@ -93,7 +93,7 @@ if (data.next !== null) { var url = data.next; - var slicedUrl = url.substring(url.indexOf('challenges/challenge')); + var slicedUrl = url.substring(url.indexOf('challenges/challenge'), url.length); parameters.url = slicedUrl; vm.getAllResults(parameters, resultsArray, type); } else { From d55ace23986b356c5633743a2d7864fdc8b92045 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Wed, 30 Apr 2025 01:49:38 +0530 Subject: [PATCH 3/9] filter panel theme match & resolve merge conflicts --- frontend/src/css/modules/challenge.scss | 40 ++++++++++++++++++---- frontend/src/views/web/challenge-list.html | 4 +-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index 77f19b79d3..a34d8d13e3 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -509,13 +509,35 @@ input[type="checkbox"] { } .apply-button { - background-color: #4CAF50; - color: white; + 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; +} + .reset-button { - background-color: #f44336; - color: white; + 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 { @@ -612,6 +634,11 @@ input[type="checkbox"] { font-size: 14px; } +.filter-content input[type="text"]:focus{ + border-bottom: 1px solid #fbbc05 !important; + box-shadow: none !important; +} + .sort-options { margin: 15px 0; } @@ -816,10 +843,11 @@ body { } .btn-card-detail { - padding: 10px; + padding: 10px 20px; text-align: center; background-color: #e3e3e3; border-top: 1px solid #eee; + transition: background-color 0.3s ease; } .w-300 { diff --git a/frontend/src/views/web/challenge-list.html b/frontend/src/views/web/challenge-list.html index f5f54ed864..34ca938be6 100644 --- a/frontend/src/views/web/challenge-list.html +++ b/frontend/src/views/web/challenge-list.html @@ -181,6 +181,4 @@
    -
    - - \ No newline at end of file + \ No newline at end of file From 94aac021378d995faa4757b9cb07cbfebb649159 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Wed, 30 Apr 2025 02:35:59 +0530 Subject: [PATCH 4/9] modified-filter input --- frontend/src/css/modules/challenge.scss | 12 ++++++++++++ frontend/src/views/web/challenge-list.html | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index a34d8d13e3..9710a768ec 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -508,6 +508,14 @@ input[type="checkbox"] { 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; @@ -674,6 +682,10 @@ input[type="checkbox"] { padding: 5px; } +.close-btn:focus{ + background-color: white !important; +} + * { box-sizing: border-box; margin: 0; diff --git a/frontend/src/views/web/challenge-list.html b/frontend/src/views/web/challenge-list.html index 34ca938be6..3a84b2899e 100644 --- a/frontend/src/views/web/challenge-list.html +++ b/frontend/src/views/web/challenge-list.html @@ -2,9 +2,9 @@

    All Challenges

    - +
    - +
    From 0156bf56a8109579bc759e855574e6ae558be610 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Wed, 30 Apr 2025 16:42:52 +0530 Subject: [PATCH 5/9] resolved-merged-conflict-challenge-scss --- frontend/src/css/modules/challenge.scss | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index 9710a768ec..ea569426af 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -382,7 +382,7 @@ md-select .md-select-value span:first-child:after { li { margin-bottom: -0.5%; } - + .nav-item { flex: 1; text-align: center; @@ -445,7 +445,6 @@ md-select .md-select-value span:first-child:after { display: block; margin-bottom: 10px; font-weight: 500; - } .filter-content input[type="text"] { @@ -508,7 +507,7 @@ input[type="checkbox"] { min-width: 100px; } -.filter-content label input{ +.filter-content label input { border: none !important; border-bottom: 1px solid #ccc !important; outline: none ; @@ -518,7 +517,7 @@ input[type="checkbox"] { .apply-button { background-color: rgba(0, 0, 0, 0); - font : 14px Roboto; + font: 14px Roboto; color: #3c3e49; border-radius: 20px; border: 1px solid #3c3e49; @@ -526,13 +525,13 @@ input[type="checkbox"] { transition: background-color 0.3s ease; } -.apply-button:hover{ +.apply-button:hover { box-shadow: 0 0 8px #9d9d9d; background: #3c3e49; color: #fff; } -.reset-button:hover{ +.reset-button:hover { box-shadow: 0 0 8px #9d9d9d; background: #3c3e49; color: #fff; @@ -540,7 +539,7 @@ input[type="checkbox"] { .reset-button { background-color: rgba(0, 0, 0, 0); - font : 14px Roboto; + font: 14px Roboto; transition: background-color 0.3s ease; color: #3c3e49; border-radius: 20px; @@ -567,7 +566,7 @@ input[type="checkbox"] { .filter-button { width: 130px; - font : 14px Roboto; + font: 14px Roboto; border-radius: 50px; background: #3c3e49; font-weight: 600; @@ -577,11 +576,11 @@ input[type="checkbox"] { padding: 7px; } -.filter-button:hover{ +.filter-button:hover { background-color: #000; } -.filter-button:focus{ +.filter-button:focus { background-color: #000 !important; } @@ -642,7 +641,7 @@ input[type="checkbox"] { font-size: 14px; } -.filter-content input[type="text"]:focus{ +.filter-content input[type="text"]:focus { border-bottom: 1px solid #fbbc05 !important; box-shadow: none !important; } @@ -682,7 +681,7 @@ input[type="checkbox"] { padding: 5px; } -.close-btn:focus{ +.close-btn:focus { background-color: white !important; } @@ -773,9 +772,11 @@ body { .col.s12 { width: 100%; } -.col.s12.m3{ + +.col.s12.m3 { display: contents; } + .col.m3 { width: calc(25% - 20px); } @@ -977,6 +978,7 @@ body { width: 100%; } } + .card-transition { transition: all 0.3s ease-out; } @@ -989,6 +991,7 @@ body { margin: 20px auto; max-width: 600px; } + /* Challenge Card Improvements for Search Results */ /* Enhance card transitions and hover effects */ @@ -1011,7 +1014,6 @@ body { border: 1px solid #f0f0f0; } - /* Special styling for search results */ .searchActive .ev-challenge-card { border-left: 3px solid #4285f4; @@ -1123,7 +1125,7 @@ body { .search-box:focus { outline: none; border-color: #ffaf4b !important; - box-shadow:none !important; + box-shadow: none !important; } /* Add special class for search active state */ From e1244940e122745d93f28fb100ec904b0b8a43e8 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Sun, 4 May 2025 12:42:06 +0530 Subject: [PATCH 6/9] controller unit test code --- .../challengeListCtrl.test.js | 410 +++++++++--------- 1 file changed, 215 insertions(+), 195 deletions(-) diff --git a/frontend/tests/controllers-test/challengeListCtrl.test.js b/frontend/tests/controllers-test/challengeListCtrl.test.js index cc3089eede..65375e3f38 100644 --- a/frontend/tests/controllers-test/challengeListCtrl.test.js +++ b/frontend/tests/controllers-test/challengeListCtrl.test.js @@ -1,207 +1,227 @@ 'use strict'; -angular.module('evalai') - .controller('ChallengeListCtrl', ['$timeout', 'utilities', function($timeout, utilities) { - var vm = this; - - // Default variables - vm.currentList = []; - vm.filteredChallenges = []; - vm.noneCurrentChallenge = false; - vm.challengeCreator = {}; - vm.searchQuery = ''; - vm.isFilterVisible = false; - vm.hasAppliedFilter = false; - vm.filter = { - organization: '', - tags: '', - sortByStartDate: false, - sortByEndDate: false - }; +describe('Unit tests for challenge list controller', function () { + beforeEach(angular.mock.module('evalai')); - // Show the loader - utilities.showLoader(); - - // Get user authentication status from the backend - var userKey = utilities.getData('userKey'); - - /** - * Handle API errors - * @param {Object} error - The error object - * @param {String} message - Error message to display - */ - function handleApiError(error, message = "Failed to load challenges. Please try again later.") { - utilities.hideLoader(); - vm.errorMessage = message; - } - - /** - * Process challenge data and add additional properties - * @param {Array} challenges - Array of challenge objects - * @returns {Array} Processed challenges - */ - function processChallenges(challenges) { - challenges.forEach(function(challenge) { - // Set isLarge property for description truncation - challenge.isLarge = challenge.description && challenge.description.length >= 50 ? "..." : ""; - - // Calculate timezone - const timezone = moment.tz.guess(); - const zone = moment.tz.zone(timezone); - const offset = new Date(challenge.start_date).getTimezoneOffset(); - challenge.time_zone = zone.abbr(offset); - - // Store creator info in challengeCreator object - vm.challengeCreator[challenge.id] = challenge.creator.id; - - // Create Date objects for sorting purposes - challenge.start_date_obj = new Date(challenge.start_date); - challenge.end_date_obj = new Date(challenge.end_date); - }); - - // Store challengeCreator in localStorage - utilities.storeData("challengeCreator", vm.challengeCreator); - - // Default sort by start date - return challenges.sort((a, b) => a.start_date_obj - b.start_date_obj); - } - - /** - * Fetch all paginated results - * @param {Object} parameters - API call parameters - * @param {Array} results - Accumulated results - * @param {String} noneFlag - Flag to set if no results - */ - vm.getAllResults = function(parameters, results, noneFlag) { - utilities.sendRequest(parameters).then(function(response) { - const data = response.data; - Array.prototype.push.apply(results, data.results); - - if (data.next) { - // If there's a next page, recursively fetch it - parameters.url = data.next.split('/api/')[1]; - vm.getAllResults(parameters, results, noneFlag); - } else { - // Process final results - if (results.length === 0) { - vm[noneFlag] = true; - } else { - results = processChallenges(results); - } + var $controller, createController, $rootScope, $scope, utilities, vm, $httpBackend; - utilities.hideLoader(); - $timeout.flush(); // Ensure $timeout is properly flushed - } - }).catch(function(error) { - handleApiError(error); - }); - }; + beforeEach(inject(function (_$controller_, _$rootScope_, _utilities_, _$httpBackend_) { + $controller = _$controller_; + $rootScope = _$rootScope_; + utilities = _utilities_; + $httpBackend = _$httpBackend_; - /** - * Fetch present challenges - */ - function getPresentChallenges() { - const parameters = { - url: 'challenges/challenge/present/approved/public', - method: 'GET', - callback: { - onSuccess: function(response) { - const data = response.data; - - if (data.results.length === 0) { - vm.noneCurrentChallenge = true; - } else { - vm.currentList = processChallenges(data.results); - } - - if (data.next) { - parameters.url = data.next.split('/api/')[1]; - vm.getAllResults(parameters, vm.currentList, 'noneCurrentChallenge'); - } else { - utilities.hideLoader(); - } - }, - onError: function(response) { - handleApiError(response.data); - } + $scope = $rootScope.$new(); + createController = function () { + return $controller('ChallengeListCtrl', { $scope: $scope }); + }; + vm = $controller('ChallengeListCtrl', { $scope: $scope }); + })); + + describe('Global variables initialization', function () { + it('should initialize global variables with 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('Backend calls for challenge data', function () { + var isPresentChallengeSuccess, isUpcomingChallengeSuccess, isPastChallengeSuccess, successResponse, errorResponse; + + beforeEach(function () { + spyOn(utilities, 'hideLoader'); + spyOn(utilities, 'storeData'); + + utilities.sendRequest = function (parameters) { + if ((isPresentChallengeSuccess === true && parameters.url === 'challenges/challenge/present/approved/public') || + (isUpcomingChallengeSuccess === 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') || + (isUpcomingChallengeSuccess === false && parameters.url === 'challenges/challenge/future/approved/public') || + (isPastChallengeSuccess === false && parameters.url === 'challenges/challenge/past/approved/public')) { + parameters.callback.onError({ + data: errorResponse + }); } }; - - utilities.sendRequest(parameters); - } - - /** - * Toggle filter panel visibility - */ - vm.toggleFilterPanel = function() { - vm.isFilterVisible = !vm.isFilterVisible; - }; - - /** - * Reset all filters - */ - vm.resetFilters = function() { - vm.filter = { - organization: '', - tags: '', - sortByStartDate: false, - sortByEndDate: false + }); + + it('should handle the case when no ongoing challenge is found', function () { + isPresentChallengeSuccess = true; + successResponse = { next: null, results: [] }; + vm = createController(); + expect(vm.currentList).toEqual(successResponse.results); + expect(vm.noneCurrentChallenge).toBeTruthy(); + }); + + it('should check description length and timezone for ongoing challenges', function () { + isPresentChallengeSuccess = true; + successResponse = { + next: null, + results: [ + { + id: 1, + description: "A description with more than 50 characters, so it should show an ellipsis at the end...", + 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.filteredChallenges = []; - vm.hasAppliedFilter = false; - }; - - /** - * Apply filters to challenge list - */ - vm.applyFilter = function() { - vm.hasAppliedFilter = true; - let filteredResults = vm.currentList.slice(); - - // Filter by organization - if (vm.filter.organization) { - filteredResults = filteredResults.filter(challenge => - challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase()) - ); - } - - // Filter by tags - if (vm.filter.tags) { - const tagArray = vm.filter.tags.toLowerCase().split(',').map(tag => tag.trim()); - - filteredResults = filteredResults.filter(challenge => { - return challenge.list_tags && challenge.list_tags.some(tag => - tagArray.some(t => tag.toLowerCase().includes(t)) - ); - }); + vm = createController(); + expect(vm.currentList).toEqual(successResponse.results); + expect(vm.noneCurrentChallenge).toBeFalsy(); + 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(""); + } + 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); } - - // Apply sorting - if (vm.filter.sortByEndDate) { - filteredResults.sort((a, b) => a.end_date_obj - b.end_date_obj); - } else if (vm.filter.sortByStartDate) { - filteredResults.sort((a, b) => a.start_date_obj - b.start_date_obj); + }); + + it('should handle error when fetching ongoing challenges', function () { + isPresentChallengeSuccess = false; + errorResponse = { next: null, error: 'error' }; + vm = createController(); + expect(utilities.hideLoader).toHaveBeenCalled(); + }); + + it('should handle the case when no upcoming challenges are found', function () { + isUpcomingChallengeSuccess = true; + successResponse = { next: null, results: [] }; + vm = createController(); + expect(vm.upcomingList).toEqual(successResponse.results); + expect(vm.noneUpcomingChallenge).toBeTruthy(); + }); + + it('should check description length and timezone for upcoming challenges', function () { + isUpcomingChallengeSuccess = true; + successResponse = { + next: null, + results: [ + { + id: 1, + description: "Upcoming challenge description with more than 50 characters...", + 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(); + 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(""); + } + 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); } - - vm.filteredChallenges = filteredResults; - }; - - /** - * Setup scroll event handling - */ - vm.scrollUp = function() { - angular.element(window).bind('scroll', function() { - if (this.pageYOffset >= 100) { - utilities.showButton(); + }); + + it('should handle error when fetching upcoming challenges', function () { + isUpcomingChallengeSuccess = false; + successResponse = { next: null, results: [] }; + vm = createController(); + expect(vm.upcomingList).toEqual(successResponse.results); + expect(utilities.hideLoader).toHaveBeenCalled(); + }); + + it('should handle the case when no past challenges are found', function () { + isPastChallengeSuccess = true; + successResponse = { next: null, results: [] }; + vm = createController(); + expect(vm.pastList).toEqual(successResponse.results); + expect(vm.nonePastChallenge).toBeTruthy(); + }); + + it('should check description length and timezone for past challenges', function () { + isPastChallengeSuccess = true; + successResponse = { + next: null, + results: [ + { + id: 1, + description: "Past challenge description with more than 50 characters...", + 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(); + 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 { - utilities.hideButton(); + expect(vm.pastList[i].isLarge).toEqual(""); } - }); - }; - - // Initialize controller - getPresentChallenges(); - vm.scrollUp(); - - return vm; - }]); + 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('should handle error when fetching past challenges', function () { + isPastChallengeSuccess = false; + successResponse = { next: null, results: [] }; + vm = createController(); + expect(vm.currentList).toEqual(successResponse.results); + expect(vm.upcomingList).toEqual(successResponse.results); + expect(utilities.hideLoader).toHaveBeenCalled(); + }); + + it('should call getAllResults recursively when next is not null', function () { + isPresentChallengeSuccess = true; + successResponse = { + next: 'http://example.com/challenges/?page=2', + results: [ + { + id: 1, + description: "Challenge description", + 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(); + expect(vm.getAllResults).toHaveBeenCalled(); + }); + }); +}); From bae50b7b78da0cb2f6f4b1eaef7427c11a02b026 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Sun, 4 May 2025 17:13:25 +0530 Subject: [PATCH 7/9] re-checked test --- .../challengeListCtrl.test.js | 410 +++++++++--------- 1 file changed, 195 insertions(+), 215 deletions(-) diff --git a/frontend/tests/controllers-test/challengeListCtrl.test.js b/frontend/tests/controllers-test/challengeListCtrl.test.js index 65375e3f38..cc3089eede 100644 --- a/frontend/tests/controllers-test/challengeListCtrl.test.js +++ b/frontend/tests/controllers-test/challengeListCtrl.test.js @@ -1,227 +1,207 @@ 'use strict'; -describe('Unit tests for challenge list controller', function () { - beforeEach(angular.mock.module('evalai')); - - var $controller, createController, $rootScope, $scope, utilities, vm, $httpBackend; +angular.module('evalai') + .controller('ChallengeListCtrl', ['$timeout', 'utilities', function($timeout, utilities) { + var vm = this; + + // Default variables + vm.currentList = []; + vm.filteredChallenges = []; + vm.noneCurrentChallenge = false; + vm.challengeCreator = {}; + vm.searchQuery = ''; + vm.isFilterVisible = false; + vm.hasAppliedFilter = false; + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false + }; - beforeEach(inject(function (_$controller_, _$rootScope_, _utilities_, _$httpBackend_) { - $controller = _$controller_; - $rootScope = _$rootScope_; - utilities = _utilities_; - $httpBackend = _$httpBackend_; + // Show the loader + utilities.showLoader(); + + // Get user authentication status from the backend + var userKey = utilities.getData('userKey'); + + /** + * Handle API errors + * @param {Object} error - The error object + * @param {String} message - Error message to display + */ + function handleApiError(error, message = "Failed to load challenges. Please try again later.") { + utilities.hideLoader(); + vm.errorMessage = message; + } + + /** + * Process challenge data and add additional properties + * @param {Array} challenges - Array of challenge objects + * @returns {Array} Processed challenges + */ + function processChallenges(challenges) { + challenges.forEach(function(challenge) { + // Set isLarge property for description truncation + challenge.isLarge = challenge.description && challenge.description.length >= 50 ? "..." : ""; + + // Calculate timezone + const timezone = moment.tz.guess(); + const zone = moment.tz.zone(timezone); + const offset = new Date(challenge.start_date).getTimezoneOffset(); + challenge.time_zone = zone.abbr(offset); + + // Store creator info in challengeCreator object + vm.challengeCreator[challenge.id] = challenge.creator.id; + + // Create Date objects for sorting purposes + challenge.start_date_obj = new Date(challenge.start_date); + challenge.end_date_obj = new Date(challenge.end_date); + }); + + // Store challengeCreator in localStorage + utilities.storeData("challengeCreator", vm.challengeCreator); + + // Default sort by start date + return challenges.sort((a, b) => a.start_date_obj - b.start_date_obj); + } + + /** + * Fetch all paginated results + * @param {Object} parameters - API call parameters + * @param {Array} results - Accumulated results + * @param {String} noneFlag - Flag to set if no results + */ + vm.getAllResults = function(parameters, results, noneFlag) { + utilities.sendRequest(parameters).then(function(response) { + const data = response.data; + Array.prototype.push.apply(results, data.results); + + if (data.next) { + // If there's a next page, recursively fetch it + parameters.url = data.next.split('/api/')[1]; + vm.getAllResults(parameters, results, noneFlag); + } else { + // Process final results + if (results.length === 0) { + vm[noneFlag] = true; + } else { + results = processChallenges(results); + } - $scope = $rootScope.$new(); - createController = function () { - return $controller('ChallengeListCtrl', { $scope: $scope }); + utilities.hideLoader(); + $timeout.flush(); // Ensure $timeout is properly flushed + } + }).catch(function(error) { + handleApiError(error); + }); }; - vm = $controller('ChallengeListCtrl', { $scope: $scope }); - })); - - describe('Global variables initialization', function () { - it('should initialize global variables with 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('Backend calls for challenge data', function () { - var isPresentChallengeSuccess, isUpcomingChallengeSuccess, isPastChallengeSuccess, successResponse, errorResponse; - - beforeEach(function () { - spyOn(utilities, 'hideLoader'); - spyOn(utilities, 'storeData'); - - utilities.sendRequest = function (parameters) { - if ((isPresentChallengeSuccess === true && parameters.url === 'challenges/challenge/present/approved/public') || - (isUpcomingChallengeSuccess === 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') || - (isUpcomingChallengeSuccess === false && parameters.url === 'challenges/challenge/future/approved/public') || - (isPastChallengeSuccess === false && parameters.url === 'challenges/challenge/past/approved/public')) { - parameters.callback.onError({ - data: errorResponse - }); + + /** + * Fetch present challenges + */ + function getPresentChallenges() { + const parameters = { + url: 'challenges/challenge/present/approved/public', + method: 'GET', + callback: { + onSuccess: function(response) { + const data = response.data; + + if (data.results.length === 0) { + vm.noneCurrentChallenge = true; + } else { + vm.currentList = processChallenges(data.results); + } + + if (data.next) { + parameters.url = data.next.split('/api/')[1]; + vm.getAllResults(parameters, vm.currentList, 'noneCurrentChallenge'); + } else { + utilities.hideLoader(); + } + }, + onError: function(response) { + handleApiError(response.data); + } } }; - }); - - it('should handle the case when no ongoing challenge is found', function () { - isPresentChallengeSuccess = true; - successResponse = { next: null, results: [] }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeTruthy(); - }); - - it('should check description length and timezone for ongoing challenges', function () { - isPresentChallengeSuccess = true; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "A description with more than 50 characters, so it should show an ellipsis at the end...", - 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" - } - ] + + utilities.sendRequest(parameters); + } + + /** + * Toggle filter panel visibility + */ + vm.toggleFilterPanel = function() { + vm.isFilterVisible = !vm.isFilterVisible; + }; + + /** + * Reset all filters + */ + vm.resetFilters = function() { + vm.filter = { + organization: '', + tags: '', + sortByStartDate: false, + sortByEndDate: false }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.noneCurrentChallenge).toBeFalsy(); - 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(""); - } - 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); + vm.filteredChallenges = []; + vm.hasAppliedFilter = false; + }; + + /** + * Apply filters to challenge list + */ + vm.applyFilter = function() { + vm.hasAppliedFilter = true; + let filteredResults = vm.currentList.slice(); + + // Filter by organization + if (vm.filter.organization) { + filteredResults = filteredResults.filter(challenge => + challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase()) + ); } - }); - - it('should handle error when fetching ongoing challenges', function () { - isPresentChallengeSuccess = false; - errorResponse = { next: null, error: 'error' }; - vm = createController(); - expect(utilities.hideLoader).toHaveBeenCalled(); - }); - - it('should handle the case when no upcoming challenges are found', function () { - isUpcomingChallengeSuccess = true; - successResponse = { next: null, results: [] }; - vm = createController(); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(vm.noneUpcomingChallenge).toBeTruthy(); - }); - - it('should check description length and timezone for upcoming challenges', function () { - isUpcomingChallengeSuccess = true; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "Upcoming challenge description with more than 50 characters...", - 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(); - 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(""); - } - 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); + + // Filter by tags + if (vm.filter.tags) { + const tagArray = vm.filter.tags.toLowerCase().split(',').map(tag => tag.trim()); + + filteredResults = filteredResults.filter(challenge => { + return challenge.list_tags && challenge.list_tags.some(tag => + tagArray.some(t => tag.toLowerCase().includes(t)) + ); + }); } - }); - - it('should handle error when fetching upcoming challenges', function () { - isUpcomingChallengeSuccess = false; - successResponse = { next: null, results: [] }; - vm = createController(); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(utilities.hideLoader).toHaveBeenCalled(); - }); - - it('should handle the case when no past challenges are found', function () { - isPastChallengeSuccess = true; - successResponse = { next: null, results: [] }; - vm = createController(); - expect(vm.pastList).toEqual(successResponse.results); - expect(vm.nonePastChallenge).toBeTruthy(); - }); - - it('should check description length and timezone for past challenges', function () { - isPastChallengeSuccess = true; - successResponse = { - next: null, - results: [ - { - id: 1, - description: "Past challenge description with more than 50 characters...", - 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(); - 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("..."); + + // Apply sorting + if (vm.filter.sortByEndDate) { + filteredResults.sort((a, b) => a.end_date_obj - b.end_date_obj); + } else if (vm.filter.sortByStartDate) { + filteredResults.sort((a, b) => a.start_date_obj - b.start_date_obj); + } + + vm.filteredChallenges = filteredResults; + }; + + /** + * Setup scroll event handling + */ + vm.scrollUp = function() { + angular.element(window).bind('scroll', function() { + if (this.pageYOffset >= 100) { + utilities.showButton(); } else { - expect(vm.pastList[i].isLarge).toEqual(""); + utilities.hideButton(); } - 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('should handle error when fetching past challenges', function () { - isPastChallengeSuccess = false; - successResponse = { next: null, results: [] }; - vm = createController(); - expect(vm.currentList).toEqual(successResponse.results); - expect(vm.upcomingList).toEqual(successResponse.results); - expect(utilities.hideLoader).toHaveBeenCalled(); - }); - - it('should call getAllResults recursively when next is not null', function () { - isPresentChallengeSuccess = true; - successResponse = { - next: 'http://example.com/challenges/?page=2', - results: [ - { - id: 1, - description: "Challenge description", - 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(); - expect(vm.getAllResults).toHaveBeenCalled(); - }); - }); -}); + }); + }; + + // Initialize controller + getPresentChallenges(); + vm.scrollUp(); + + return vm; + }]); From ae0ad02afb0e8a243b3e9150981efcf322104ca9 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Sun, 4 May 2025 17:15:24 +0530 Subject: [PATCH 8/9] undo-ctrllist --- frontend/src/js/controllers/challengeListCtrl.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/js/controllers/challengeListCtrl.js b/frontend/src/js/controllers/challengeListCtrl.js index 5c9ee10be9..00201c8fc1 100644 --- a/frontend/src/js/controllers/challengeListCtrl.js +++ b/frontend/src/js/controllers/challengeListCtrl.js @@ -1,4 +1,3 @@ -// Invoking IIFE for challenge page (function () { 'use strict'; @@ -93,7 +92,7 @@ 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, type); } else { @@ -268,4 +267,4 @@ // Call init init(); } -})(); +})(); \ No newline at end of file From d65dfd32c0a290546d9ca4ad2aa4119d7c791549 Mon Sep 17 00:00:00 2001 From: wahidullah Date: Fri, 16 May 2025 23:26:43 +0530 Subject: [PATCH 9/9] Add new test case for ChallengeListCtrl & css improved --- frontend/src/css/modules/challenge.scss | 107 ++-- .../src/js/controllers/challengeListCtrl.js | 169 +++--- frontend/src/views/web/challenge-list.html | 22 +- .../challengeListCtrl.test.js | 564 ++++++++++++------ 4 files changed, 547 insertions(+), 315 deletions(-) diff --git a/frontend/src/css/modules/challenge.scss b/frontend/src/css/modules/challenge.scss index ea569426af..97d3b9ea5c 100644 --- a/frontend/src/css/modules/challenge.scss +++ b/frontend/src/css/modules/challenge.scss @@ -33,10 +33,10 @@ a.active-challenge { } .ev-challenge-approval-view { - margin-top: 0px; - padding-top: 30px; - padding-bottom: 10px; - margin-bottom: 20px; + margin-top: 0px; + padding-top: 30px; + padding-bottom: 10px; + margin-bottom: 20px; } .challenge-container { @@ -62,6 +62,9 @@ a.active-challenge { .ev-challenge-card { height: 425px; } +.ev-hosted-challenge-card { + height: 480px; +} .ev-dashboard-card { min-height: 190px; @@ -264,6 +267,7 @@ md-select .md-select-value span:first-child:after { .btn-switch--on { background-color: #ffaf4b; border: 2px solid #ffaf4b; + ; .btn-switch-circle--on { left: auto; right: 0; @@ -370,11 +374,9 @@ md-select .md-select-value span:first-child:after { .filter-icon { padding: 10px; } - .no-margin { margin: 0px; } - .nav-underline { display: flex; justify-content: space-around; @@ -384,26 +386,26 @@ md-select .md-select-value span:first-child:after { } .nav-item { - flex: 1; - text-align: center; + flex: 1; + text-align: center; + color: #4d4d4d; + + .nav-link { + display: block; + padding: 10px 0; color: #4d4d4d; - - .nav-link { - display: block; - padding: 10px 0; - color: #4d4d4d; - text-decoration: none; - border: none; - background-color: transparent; - font-weight: 500; - transition: border-bottom 0.3s ease; - cursor: pointer; - - &.active { - border-bottom: 2px solid #000; - color: #4d4d4d; - } + text-decoration: none; + border: none; + background-color: transparent; + font-weight: 500; + transition: border-bottom 0.3s ease; + cursor: pointer; + + &.active { + border-bottom: 2px solid #000; + color: #4d4d4d; } + } } } @@ -411,28 +413,46 @@ md-select .md-select-value span:first-child:after { margin-bottom: 20px; .tab { - a { - color: #4d4d4d; - font-weight: 400; - - &.active { - color: #3f51b5; - font-weight: 600; - } + a { + color: #4d4d4d; + font-weight: 400; + + &.active { + color: #3f51b5; + font-weight: 600; } + } } } .challenges-container { margin-top: 20px; min-height: 200px; - - .card-content { - padding: 20px; - text-align: center; - } + + .card-content { + padding: 20px; + text-align: center; + } } +.challenges-container { + margin-top: 20px; + min-height: 200px; + + .hosted-challenge-card-content { + padding: 20px; + text-align: left; + } +} + + +.github-icon-link { + margin-right: 10px; + display: inline-block; + vertical-align: middle; + color: #000; // Ensure the icon is visible + font-size: 15px; // Adjust size if needed +} .filter-content { padding: 15px; border: 1px solid #ddd; @@ -537,6 +557,15 @@ input[type="checkbox"] { 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; @@ -577,11 +606,11 @@ input[type="checkbox"] { } .filter-button:hover { - background-color: #000; + background-color: #3c3e49; } .filter-button:focus { - background-color: #000 !important; + background-color: #3c3e49 !important; } .no-results { diff --git a/frontend/src/js/controllers/challengeListCtrl.js b/frontend/src/js/controllers/challengeListCtrl.js index 00201c8fc1..b385f79545 100644 --- a/frontend/src/js/controllers/challengeListCtrl.js +++ b/frontend/src/js/controllers/challengeListCtrl.js @@ -1,3 +1,4 @@ +// Invoking IIFE for challenge page (function () { 'use strict'; @@ -16,23 +17,27 @@ var gmtMinutes = Math.abs(gmtOffset % 60); var gmtZone = 'GMT ' + gmtSign + ' ' + gmtHours + ':' + (gmtMinutes < 10 ? '0' : '') + gmtMinutes; - // Initializing the loader + utilities.showLoader(); utilities.hideButton(); - // Initialize variables + vm.currentList = []; vm.upcomingList = []; vm.pastList = []; - vm.filteredChallenges = []; vm.challengeCreator = {}; - // Flags for empty lists + + vm.filteredCurrentList = []; + vm.filteredUpcomingList = []; + vm.filteredPastList = []; + + vm.noneCurrentChallenge = false; vm.noneUpcomingChallenge = false; vm.nonePastChallenge = false; - // Filter variables + vm.searchQuery = ''; vm.isFilterVisible = false; vm.filter = { @@ -42,29 +47,28 @@ sortByEndDate: false }; - // Toggle filter panel visibility - vm.toggleFilterPanel = function() { + + vm.toggleFilterPanel = function () { vm.isFilterVisible = !vm.isFilterVisible; - // Force digest cycle to ensure UI updates if (!$scope.$$phase) { $scope.$apply(); } }; - // Function to retrieve challenge data from API + vm.getAllResults = function (parameters, resultsArray, type) { parameters.method = 'GET'; parameters.callback = { onSuccess: function (response) { - if (!response || !response.data) { + if (!response || !response.data || !response.data.results) { utilities.hideLoader(); return; } var data = response.data; var results = data.results || []; - var timezone = moment.tz.guess(); + results.forEach(function (challenge) { if (!challenge || !challenge.description) { return; @@ -77,7 +81,6 @@ challenge.time_zone = moment.tz.zone(timezone).abbr(offset); challenge.gmt_zone = gmtZone; - // Convert dates to Date objects for easier comparison challenge.start_date_obj = new Date(challenge.start_date); challenge.end_date_obj = new Date(challenge.end_date); @@ -96,15 +99,23 @@ parameters.url = slicedUrl; vm.getAllResults(parameters, resultsArray, type); } else { - // Sort challenges by start date by default - resultsArray.sort(function(a, b) { + resultsArray.sort(function (a, b) { return a.start_date_obj - b.start_date_obj; }); - $timeout(function() { + $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(); } @@ -123,7 +134,7 @@ utilities.sendRequest(parameters); }; - // Initialize challenges + var parameters = { token: userKey ? userKey : null }; parameters.url = 'challenges/challenge/present/approved/public'; @@ -135,120 +146,98 @@ parameters.url = 'challenges/challenge/past/approved/public'; vm.getAllResults(parameters, vm.pastList, 'nonePastChallenge'); - // Apply filters on challenges - Fixed implementation - vm.applyFilter = function () { - vm.hasAppliedFilter = true; - - // Handle empty list case - if (!vm.currentList || vm.currentList.length === 0) { - vm.filteredChallenges = []; - return; + + function filterChallenges(challengeList) { + if (!challengeList || challengeList.length === 0) { + return []; } - // Start with a copy of the current list - vm.filteredChallenges = angular.copy(vm.currentList); - - // Perform filtering - vm.filteredChallenges = vm.filteredChallenges.filter(function (challenge) { - var match = true; - var filterResults = { passed: true, reasons: [] }; - - // Organization filter - if (vm.filter.organization && vm.filter.organization.trim() !== '') { - if (!challenge.creator || !challenge.creator.team_name || - !challenge.creator.team_name.toLowerCase().includes(vm.filter.organization.toLowerCase())) { - match = false; - filterResults.passed = false; - filterResults.reasons.push('Organization mismatch'); - } - } + 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()); + }); + } - // Tags filter - if (vm.filter.tags && vm.filter.tags.trim() !== '') { + 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) { + searchTags.forEach(function (searchTag) { searchTag = searchTag.trim(); if (searchTag === '') return; - challenge.list_tags.forEach(function(challengeTag) { + challenge.list_tags.forEach(function (challengeTag) { if (challengeTag.toLowerCase().includes(searchTag)) { tagMatch = true; } }); }); - - if (!tagMatch) { - match = false; - filterResults.passed = false; - filterResults.reasons.push('Tag mismatch'); - } + return tagMatch; } else { - match = false; // No tags to match against - filterResults.passed = false; - filterResults.reasons.push('Challenge has no tags'); + return false; } - } + }); + } - return match; - }); + applySorting(filtered); + return filtered; + } + + // Apply filters on all challenge lists + vm.applyFilter = function () { + vm.hasAppliedFilter = true; - // Apply sorting based on checkboxes - applySorting(vm.filteredChallenges); + // Apply filters to all three challenge lists + vm.filteredCurrentList = filterChallenges(vm.currentList); + vm.filteredUpcomingList = filterChallenges(vm.upcomingList); + vm.filteredPastList = filterChallenges(vm.pastList); - // Ensure UI updates if (!$scope.$$phase) { $scope.$apply(); } }; - // Helper function to apply sorting based on current checkbox states + // Helper function for sorting function applySorting(challengeList) { - // First priority: Sort by start date - if (vm.filter.sortByStartDate) { - challengeList.sort(function(a, b) { - return a.start_date_obj - b.start_date_obj; - }); - } - - // Second priority: Sort by end date (overrides start date sort if both are selected) if (vm.filter.sortByEndDate) { - challengeList.sort(function(a, b) { + challengeList.sort(function (a, b) { return a.end_date_obj - b.end_date_obj; }); - } - - // If no sort option is selected, default to start date - if (!vm.filter.sortByStartDate && !vm.filter.sortByEndDate) { - challengeList.sort(function(a, b) { + } else { + challengeList.sort(function (a, b) { return a.start_date_obj - b.start_date_obj; }); } } - // Reset all filters - vm.resetFilters = function() { + vm.resetFilters = function () { vm.filter = { organization: '', tags: '', sortByStartDate: false, sortByEndDate: false }; - vm.filteredChallenges = []; + + // 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; - // Ensure UI updates if (!$scope.$$phase) { $scope.$apply(); } }; - // Initialize hasAppliedFilter flag vm.hasAppliedFilter = false; - // Scroll up button visibility vm.scrollUp = function () { angular.element($window).bind('scroll', function () { if (this.pageYOffset >= 100) { @@ -259,12 +248,24 @@ }); }; - // Initialize the controller function init() { - vm.isFilterVisible = false; // Ensure filter panel is hidden initially + vm.isFilterVisible = false; } - // Call init 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 3a84b2899e..d98e90bd11 100644 --- a/frontend/src/views/web/challenge-list.html +++ b/frontend/src/views/web/challenge-list.html @@ -4,7 +4,7 @@
    - +