Skip to content

Commit 7bf3fc7

Browse files
Ihor MasechkoIhor Masechko
authored andcommitted
fix(case-studies): search UX, history, and piece-page req handling
- Drive clear button visibility by class only; remove dead CSS/placeholder rule - Use type=text to avoid native search clear conflicting with custom clear - performSearch: use location.assign only (no pushState+reload phantom history) - handleSearchInput: close over clearButton; remove no-op focus() before navigate - SearchService: buildSearchCondition, 200-char cap, Prettier format - Chain parent beforeIndex/beforeShow so req.data is set before getFilters - Do not reassign query from filterByIndexPage (it returns undefined; fixes query.req)
1 parent 8e0a7c5 commit 7bf3fc7

6 files changed

Lines changed: 71 additions & 46 deletions

File tree

website/modules/asset/ui/src/scss/_cases.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,6 @@
178178
display: flex;
179179
}
180180

181-
.cs_search-bar-input-wrapper:focus-within .cs_search-bar-input::placeholder {
182-
color: $gray-300;
183-
}
184-
185181
// Tags styling
186182
.tags-filter {
187183
border: 1px solid $gray-border;

website/modules/case-studies-page/index.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,9 @@ const buildIndexQuery = function (self, req) {
1515
.perPage(self.perPage);
1616
self.filterByIndexPage(query, req.data.page);
1717

18-
const regexPattern = SearchService.buildSearchRegexPattern(searchTerm);
19-
if (regexPattern) {
20-
query.and({
21-
$or: [
22-
{ title: { $regex: regexPattern, $options: 'i' } },
23-
{ portfolioTitle: { $regex: regexPattern, $options: 'i' } },
24-
],
25-
});
18+
const searchCondition = SearchService.buildSearchCondition(searchTerm);
19+
if (searchCondition) {
20+
query.and(searchCondition);
2621
}
2722
return query;
2823
};
@@ -92,11 +87,19 @@ module.exports = {
9287
},
9388

9489
init(self) {
90+
const superBeforeIndex = self.beforeIndex;
9591
self.beforeIndex = async (req) => {
92+
if (superBeforeIndex) {
93+
await superBeforeIndex(req);
94+
}
9695
await self.setupIndexData(req);
9796
};
9897

98+
const superBeforeShow = self.beforeShow;
9999
self.beforeShow = async (req) => {
100+
if (superBeforeShow) {
101+
await superBeforeShow(req);
102+
}
100103
await self.setupShowData(req);
101104
};
102105
},

website/modules/case-studies-page/services/NavigationService.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,11 @@ class NavigationService {
7979
*/
8080
static applySearchFilter(filteredQuery, req) {
8181
const searchTerm = SearchService.getSearchTerm(req.query || {});
82-
const regexPattern = SearchService.buildSearchRegexPattern(searchTerm);
83-
if (!regexPattern) {
82+
const searchCondition = SearchService.buildSearchCondition(searchTerm);
83+
if (!searchCondition) {
8484
return filteredQuery;
8585
}
86-
return filteredQuery.and({
87-
$or: [
88-
{ title: { $regex: regexPattern, $options: 'i' } },
89-
{ portfolioTitle: { $regex: regexPattern, $options: 'i' } },
90-
],
91-
});
86+
return filteredQuery.and(searchCondition);
9287
}
9388

9489
/**

website/modules/case-studies-page/services/SearchService.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,68 @@
77

88
const REGEX_ESCAPE = /[$()*+.?[\\\]^{|}]/gu;
99

10+
const MAX_SEARCH_TERM_LENGTH = 200;
11+
1012
/**
1113
* Normalizes search param from query (handles missing, array, non-string)
1214
* @param {Object} queryParams - Request query object (e.g. req.query)
1315
* @returns {string} Trimmed search string, or empty string
1416
*/
15-
function getSearchTerm(queryParams) {
17+
const getSearchTerm = function (queryParams) {
1618
if (!queryParams || !queryParams.search) {
1719
return '';
1820
}
1921
const raw = queryParams.search;
20-
const value = Array.isArray(raw) ? raw[0] : raw;
22+
let value = raw;
23+
if (Array.isArray(raw)) {
24+
[value] = raw;
25+
}
2126
if (typeof value !== 'string') {
2227
return '';
2328
}
2429
return value.trim();
25-
}
30+
};
2631

2732
/**
2833
* Builds a safe MongoDB regex pattern from search term (escape + word match)
34+
* Search term is capped at MAX_SEARCH_TERM_LENGTH to avoid pathologically long patterns
2935
* @param {string} searchTerm - User search string
3036
* @returns {string|null} Pattern for $regex, or null if no search
3137
*/
32-
function buildSearchRegexPattern(searchTerm) {
38+
const buildSearchRegexPattern = function (searchTerm) {
3339
if (!searchTerm || !searchTerm.trim()) {
3440
return null;
3541
}
36-
const escaped = searchTerm.trim().replace(REGEX_ESCAPE, '\\$&');
42+
const trimmed = searchTerm.trim();
43+
if (!trimmed) {
44+
return null;
45+
}
46+
let capped = trimmed;
47+
if (trimmed.length > MAX_SEARCH_TERM_LENGTH) {
48+
capped = trimmed.slice(0, MAX_SEARCH_TERM_LENGTH);
49+
}
50+
const escaped = capped.replace(REGEX_ESCAPE, '\\$&');
3751
return escaped.split(/\s+/u).join('.*');
38-
}
52+
};
53+
54+
/**
55+
* Builds MongoDB $or condition for case study search (single source of truth for searchable fields)
56+
* @param {string} searchTerm - User search string
57+
* @returns {Object|null} Condition to pass to query.and(), or null if no search
58+
*/
59+
const buildSearchCondition = function (searchTerm) {
60+
const regexPattern = buildSearchRegexPattern(searchTerm);
61+
if (!regexPattern) {
62+
return null;
63+
}
64+
const regexOpts = { $regex: regexPattern, $options: 'i' };
65+
return {
66+
$or: [{ title: regexOpts }, { portfolioTitle: regexOpts }],
67+
};
68+
};
3969

4070
module.exports = {
41-
getSearchTerm,
71+
buildSearchCondition,
4272
buildSearchRegexPattern,
73+
getSearchTerm,
4374
};

website/modules/case-studies-page/views/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
aria-hidden="true"
3030
/>
3131
<input
32-
type="search"
32+
type="text"
3333
class="cs_search-bar-input"
3434
id="case-studies-search"
3535
name="search"

website/public/js/modules/case-studies-page/search-handler.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,25 @@
2929
return newUrl;
3030
}
3131

32-
// Update URL and reload page
32+
// Navigate to URL with updated search (single history entry; Back works as expected)
3333
function performSearch(searchValue) {
3434
const newUrl = buildSearchUrl(searchValue);
35-
history.pushState({ clientSideFilter: true, url: newUrl }, '', newUrl);
36-
window.location.reload();
35+
window.location.assign(newUrl);
3736
}
3837

39-
// Update clear button visibility
38+
const VISIBLE_CLASS = 'cs_search-bar-clear--visible';
39+
40+
// Sync clear button visibility via CSS class (no inline styles; CSS rules control display)
4041
function updateClearButtonVisibility(searchInput, clearButton) {
41-
if (searchInput && clearButton) {
42-
if (searchInput.value && searchInput.value.trim()) {
43-
clearButton.style.display = 'flex';
44-
} else {
45-
clearButton.style.display = 'none';
46-
}
42+
if (!searchInput || !clearButton) {
43+
return;
44+
}
45+
const hasValue = searchInput.value && searchInput.value.trim();
46+
if (hasValue) {
47+
clearButton.classList.add(VISIBLE_CLASS);
48+
} else {
49+
clearButton.classList.remove(VISIBLE_CLASS);
4750
}
48-
}
49-
50-
// Handle search input (only update clear button; search runs on Enter)
51-
function handleSearchInput(event) {
52-
const clearButton = document.querySelector('.cs_search-bar-clear');
53-
updateClearButtonVisibility(event.target, clearButton);
5451
}
5552

5653
// Handle clear button click
@@ -63,7 +60,6 @@
6360
if (searchInput) {
6461
searchInput.value = '';
6562
updateClearButtonVisibility(searchInput, clearButton);
66-
searchInput.focus();
6763
performSearch('');
6864
}
6965
}
@@ -97,7 +93,11 @@
9793
return;
9894
}
9995

100-
// Add input event listener
96+
function handleSearchInput(event) {
97+
updateClearButtonVisibility(event.target, clearButton);
98+
}
99+
100+
// Add input event listener (handler closes over clearButton; no live query per keystroke)
101101
searchInput.addEventListener('input', handleSearchInput);
102102

103103
// Add form submit handler

0 commit comments

Comments
 (0)