Skip to content

Commit

Permalink
feat: add prequery search suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
katrinan029 committed Jan 30, 2024
1 parent 1541b86 commit f8dfe85
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 47 deletions.
17 changes: 15 additions & 2 deletions packages/catalog-search/src/SearchBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const SearchBoxBase = ({
const [autocompleteHits, setAutocompleteHits] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [preQueryHits, setPreQueryHits] = useState([]);

/**
* Handles when a search is submitted by adding the user's search
Expand Down Expand Up @@ -103,15 +104,23 @@ export const SearchBoxBase = ({
attributesToRetrieve: ALGOLIA_ATTRIBUTES_TO_RETRIEVE,
});
if (nbHits > 0) {
setPreQueryHits([]);
setAutocompleteHits(hits);
setShowSuggestions(true);
} else {
// If there are no results of the suggested search, hide the empty suggestion component
setShowSuggestions(false);
}
// Hide the results as soon as the user removes the entire query string, instead of waiting a second
// Display the prequery results when user clicks on search box but has not began typing
} else {
setShowSuggestions(false);
const { hits } = await index.search(query, {
filters,
attributesToHighlight: ['title'],
attributesToRetrieve: ALGOLIA_ATTRIBUTES_TO_RETRIEVE,
});
setAutocompleteHits([]);
setPreQueryHits(hits);
setShowSuggestions(true);
}
};
// Since the debounced method is called in a useEffect hook, use `useCallback` to account for repeated invoking of the
Expand Down Expand Up @@ -155,6 +164,9 @@ export const SearchBoxBase = ({
value={defaultRefinement}
onSubmit={handleSubmit}
onClear={handleClear}
onFocus={(query) => {
setSearchQuery(query);
}}
onChange={(query) => {
setSearchQuery(query);
}}
Expand All @@ -172,6 +184,7 @@ export const SearchBoxBase = ({
{ showSuggestions && (
<SearchSuggestions
enterpriseSlug={enterpriseSlug}
preQueryHits={preQueryHits}
autoCompleteHits={autocompleteHits}
handleSubmit={() => handleSubmit(searchQuery)}
handleSuggestionClickSubmit={hit => handleSuggestionSubmit(hit)}
Expand Down
67 changes: 46 additions & 21 deletions packages/catalog-search/src/SearchSuggestionItem.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import { Image } from '@edx/paragon';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';

const SearchSuggestionItem = ({
url, suggestionItemHandler, hit, disableSuggestionRedirect,
url, suggestionItemHandler, hit, disableSuggestionRedirect, isPreQuery,
}) => {
const authoringOrganization = hit.key && hit.key.split('+')[0];
// If the disable redirect bool is provided, prevent the redirect from happening and instead call the provided submit
Expand All @@ -14,28 +15,44 @@ const SearchSuggestionItem = ({
suggestionItemHandler(hit);
}
};

return (
<Link to={url} key={hit.title} className="suggestion-item" onClick={handleLinkDisable}>
<div>
{ /* eslint-disable-next-line react/no-danger, no-underscore-dangle */ }
<div dangerouslySetInnerHTML={{ __html: hit._highlightResult.title.value }} />
{
authoringOrganization && (
<div className="badge badge-light ml-3 font-weight-light authoring-org-badge">
{authoringOrganization}
<div>
{isPreQuery ? (
<Link to={url} key={hit.title} className="prequery-item pr-4 d-flex flex-column" onClick={handleLinkDisable}>
<div className="d-flex align-items-center justify-content-start">
<Image className="prequery-image mr-2" src={hit.card_image_url} />
<div className="d-flex flex-column">
{/* eslint-disable-next-line react/no-danger, no-underscore-dangle */}
<div dangerouslySetInnerHTML={{ __html: hit._highlightResult.title.value }} />
<div className="x-small d-flex flex-column">
{hit.partners[0]?.name} | {hit.learning_type}
</div>
</div>
)
}
</div>
{
hit.program_type && (
<p className="font-weight-light text-gray-400 program-type">
{hit.program_type}
</p>
)
}
</Link>
</div>
</Link>
) : (
<Link to={url} key={hit.title} className="suggestion-item" onClick={handleLinkDisable}>
<div>
{ /* eslint-disable-next-line react/no-danger, no-underscore-dangle */}
<div dangerouslySetInnerHTML={{ __html: hit._highlightResult.title.value }} />
{
authoringOrganization && (
<div className="badge badge-light ml-3 font-weight-light authoring-org-badge">
{authoringOrganization}
</div>
)
}
</div>
{
hit.program_type && (
<p className="font-weight-light text-gray-400 program-type">
{hit.program_type}
</p>
)
}
</Link>
)}
</div>
);
};

Expand All @@ -47,8 +64,16 @@ SearchSuggestionItem.propTypes = {
title: PropTypes.string,
program_type: PropTypes.string,
_highlightResult: PropTypes.shape({ title: PropTypes.shape({ value: PropTypes.string }) }),
card_image_url: PropTypes.string,
partners: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
}),
),
learning_type: PropTypes.string,
}).isRequired,
disableSuggestionRedirect: PropTypes.bool.isRequired,
isPreQuery: PropTypes.bool.isRequired,
};

SearchSuggestionItem.defaultProps = {
Expand Down
41 changes: 41 additions & 0 deletions packages/catalog-search/src/SearchSuggestions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import PropTypes from 'prop-types';
import {
MAX_NUM_SUGGESTIONS, LEARNING_TYPE_COURSE, LEARNING_TYPE_PROGRAM,
LEARNING_TYPE_EXECUTIVE_EDUCATION, COURSE_TYPE_EXECUTIVE_EDUCATION,
MAX_NUM_PRE_QUERY_SUGGESTIONS,
} from './data/constants';
import SearchSuggestionItem from './SearchSuggestionItem';

const SearchSuggestions = ({
preQueryHits,
autoCompleteHits,
enterpriseSlug,
handleSubmit,
Expand All @@ -24,9 +26,16 @@ const SearchSuggestions = ({
};
const getLinkToProgram = (program) => `/${enterpriseSlug}/program/${program.aggregation_key.split(':').pop()}`;

const preQuerySuggestions = [];
const courses = [];
const programs = [];
const execEdCourses = [];

if (preQueryHits) {
preQueryHits.forEach((hit) => {
preQuerySuggestions.push(hit);
});
}
autoCompleteHits.forEach((hit) => {
const { learning_type: learningType } = hit;
if (learningType === LEARNING_TYPE_COURSE) { courses.push(hit); }
Expand All @@ -35,6 +44,36 @@ const SearchSuggestions = ({
});
return (
<div className="suggestions" data-testid="suggestions">
{preQuerySuggestions.length > 0 && (
<div>
<div className="mb-2 ml-2 mt-1 font-weight-bold suggestions-section">
Top-rated courses
</div>
{
preQuerySuggestions.slice(0, MAX_NUM_PRE_QUERY_SUGGESTIONS)
.map((hit) => {
const getUrl = (course) => {
const { learning_type: learningType } = course;
if (learningType === LEARNING_TYPE_COURSE || learningType === LEARNING_TYPE_EXECUTIVE_EDUCATION) {
return getLinkToCourse(course);
}
return getLinkToProgram(course);
};

return (
<SearchSuggestionItem
key={hit.title}
url={getUrl(hit)}
hit={hit}
isPreQuery={preQuerySuggestions.length > 0}
disableSuggestionRedirect={disableSuggestionRedirect}
suggestionItemHandler={handleSuggestionClickSubmit}
/>
);
})
}
</div>
)}
{courses.length > 0 && (
<div>
<div className="mb-2 ml-2 mt-1 font-weight-bold suggestions-section">
Expand All @@ -47,6 +86,7 @@ const SearchSuggestions = ({
key={hit.title}
url={getLinkToCourse(hit)}
hit={hit}
isPreQuery={preQuerySuggestions.length > 0}
disableSuggestionRedirect={disableSuggestionRedirect}
suggestionItemHandler={handleSuggestionClickSubmit}
/>
Expand Down Expand Up @@ -107,6 +147,7 @@ SearchSuggestions.propTypes = {
handleSubmit: PropTypes.func,
handleSuggestionClickSubmit: PropTypes.func,
disableSuggestionRedirect: PropTypes.bool,
preQueryHits: PropTypes.arrayOf(PropTypes.shape()).isRequired,
};

SearchSuggestions.defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions packages/catalog-search/src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const NUM_CURRENT_REFINEMENTS_TO_DISPLAY = 3;
export const NUM_RESULTS_PER_PAGE = 24;

export const MAX_NUM_SUGGESTIONS = 3;
export const MAX_NUM_PRE_QUERY_SUGGESTIONS = 5;

export const NO_OPTIONS_FOUND = 'No options found.';

Expand Down
77 changes: 54 additions & 23 deletions packages/catalog-search/src/styles/_SearchSuggestions.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.suggestions{
.suggestions {
position: absolute;
background: white;
margin-top: auto;
Expand All @@ -13,53 +13,84 @@
margin: 5px 10px 5px 10px;
padding: 8px;
color: #000;
font-size: 75%;
white-space: pre-wrap;
&:hover{
color:white;
background-color:#000000 ;
text-decoration: none;
}

&:hover span {
color:white;
&:hover {
color: white;
background-color: #000000;
text-decoration: none;
}
em {
font-weight: bold;
font-style: normal;
&:hover{

&:hover span {
color: white;
}
&:not(:first-child) {
margin-left: 4px; // add space between _highlightResult.title.value hits

em {
font-weight: bold;
font-style: normal;

&:hover {
color: white;
}

&:not(:first-child) {
margin-left: 4px; // add space between _highlightResult.title.value hits
}
}
}
div{

div {
display: flex;
}
.authoring-org-badge{

.authoring-org-badge {
line-height: 1.5;
}

.program-type{
.program-type {
font-size: .9rem;
margin-bottom: 0px;
}

}

.prequery-item {
margin: 5px 10px 5px 10px;
color: #000;
white-space: pre-wrap;

&:hover {
color: white;
background-color: #000000;
text-decoration: none;
}

&:hover span {
color: white;
}
}

.prequery-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 0 !important;
}

.suggestion-heading {
color: #707070!important
color: #707070 !important
}

.view-all-btn {
color:#000;
color: #000;
border-radius: 0px;
&:hover{
color:#000;

&:hover {
color: #000;
}
}

.suggestions-section {
color: #707070;
font-size: .9rem;
}
}
18 changes: 17 additions & 1 deletion packages/catalog-search/src/tests/SearchSuggestions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const fakeSuggestionsData = {
key: 'edX+courseX',
title: 'test-course',
_highlightResult: { title: { value: 'test-<em>course</em>' } },
partners: [{
name: 'edx-partner',
}],
},
{
learning_type: 'program',
Expand All @@ -22,6 +25,9 @@ const fakeSuggestionsData = {
aggregation_key: '123:456',
authoring_organizations: [{ key: 'harvard' }],
program_type: 'xSeries',
partners: [{
name: 'harvard-partner',
}],
},
],
};
Expand Down Expand Up @@ -74,7 +80,17 @@ describe('<SeachSuggestions />', () => {
expect(screen.getByText('xSeries')).not.toBeNull();
expect(screen.getByText('View all results')).not.toBeNull();
});

test('renders only prequery suggestions', () => {
renderWithRouter(<SearchSuggestions
enterpriseSlug="test-enterprise"
autoCompleteHits={[]}
preQueryHits={fakeSuggestionsData.hits}
handleSubmit={handleSubmit}
/>);
expect(screen.getByText('Top-rated courses')).not.toBeNull();
expect(screen.queryByText('Courses')).toBeNull();
expect(screen.queryByText('Programs')).toBeNull();
});
test('renders no errors when no authoring orgs found for programs data', () => {
renderWithRouter(<SearchSuggestions
enterpriseSlug="test-enterprise"
Expand Down

0 comments on commit f8dfe85

Please sign in to comment.