Skip to content

Commit 999eb54

Browse files
authored
[995] feat: add Partner filter to case studies page (#251)
feat: add Partner filter to case studies page - Add Partner filter option in sidebar after Industry, Case Type, and Technology - Implement backend filtering logic for business-partner relationships - Update NavigationService, UrlService, and TagCountService to support partner filtering - Add partner filter UI matching existing filter patterns - Support multi-select and combination with other filters
1 parent aecf1c3 commit 999eb54

6 files changed

Lines changed: 155 additions & 62 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
{ name: 'industry' },
1414
{ name: 'stack' },
1515
{ name: 'caseStudyType' },
16+
{ name: 'partner' },
1617
],
1718
pieces: 'case-studies',
1819
piecesFiltersUrl: '/case-studies',
@@ -59,6 +60,7 @@ module.exports = {
5960
industry: {},
6061
stack: {},
6162
caseStudyType: {},
63+
partner: {},
6264
});
6365
}
6466
},

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

Lines changed: 81 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,63 @@
99
*/
1010
class NavigationService {
1111
/**
12-
* Converts tag slugs to IDs
12+
* Converts slugs to document IDs for a given piece module
1313
* @param {Object} apos - ApostropheCMS instance
1414
* @param {Object} req - Request object
15-
* @param {Array} slugs - Array of tag slugs
16-
* @returns {Promise<Array>} Promise resolving to array of tag IDs
15+
* @param {Array|string} slugs - Array of slugs or single slug (Express may pass string for one query param)
16+
* @param {string} moduleKey - Key of the piece module in apos.modules (e.g. 'cases-tags', 'business-partner')
17+
* @returns {Promise<Array>} Promise resolving to array of document IDs
1718
*/
18-
static async convertSlugsToIds(apos, req, slugs) {
19-
const tagPromises = slugs.map(async (slug) => {
20-
const results = await apos.modules['cases-tags']
19+
static async convertSlugsToIdsForModule(apos, req, slugs, moduleKey) {
20+
let slugList = [];
21+
if (Array.isArray(slugs)) {
22+
slugList = slugs;
23+
} else if (slugs) {
24+
slugList = [slugs];
25+
}
26+
const docPromises = slugList.map(async (slug) => {
27+
const results = await apos.modules[moduleKey]
2128
.find(req, { slug })
2229
.toArray();
2330
if (results.length > 0) {
2431
return results[0];
2532
}
2633
return null;
2734
});
28-
const tags = await Promise.all(tagPromises);
29-
return tags.filter((tag) => tag).map((tag) => tag.aposDocId);
35+
const docs = await Promise.all(docPromises);
36+
return docs.filter((doc) => doc).map((doc) => doc.aposDocId);
37+
}
38+
39+
/**
40+
* Converts tag slugs to IDs (cases-tags module)
41+
* @param {Object} apos - ApostropheCMS instance
42+
* @param {Object} req - Request object
43+
* @param {Array} slugs - Array of tag slugs
44+
* @returns {Promise<Array>} Promise resolving to array of tag IDs
45+
*/
46+
static convertSlugsToIds(apos, req, slugs) {
47+
return NavigationService.convertSlugsToIdsForModule(
48+
apos,
49+
req,
50+
slugs,
51+
'cases-tags',
52+
);
53+
}
54+
55+
/**
56+
* Converts business partner slugs to IDs
57+
* @param {Object} apos - ApostropheCMS instance
58+
* @param {Object} req - Request object
59+
* @param {Array} slugs - Array of partner slugs
60+
* @returns {Promise<Array>} Promise resolving to array of partner IDs
61+
*/
62+
static convertPartnerSlugsToIds(apos, req, slugs) {
63+
return NavigationService.convertSlugsToIdsForModule(
64+
apos,
65+
req,
66+
slugs,
67+
'business-partner',
68+
);
3069
}
3170

3271
/**
@@ -37,45 +76,45 @@ class NavigationService {
3776
* @returns {Promise<Object>} Promise resolving to modified query object
3877
*/
3978
static async applyFiltersToQuery(query, req, apos) {
79+
const filterConfigs = [
80+
{
81+
param: 'industry',
82+
convert: NavigationService.convertSlugsToIds,
83+
field: 'industryIds',
84+
},
85+
{
86+
param: 'stack',
87+
convert: NavigationService.convertSlugsToIds,
88+
field: 'stackIds',
89+
},
90+
{
91+
param: 'caseStudyType',
92+
convert: NavigationService.convertSlugsToIds,
93+
field: 'caseStudyTypeIds',
94+
},
95+
{
96+
param: 'partner',
97+
convert: NavigationService.convertPartnerSlugsToIds,
98+
field: 'partnerIds',
99+
},
100+
];
101+
const idArrays = await Promise.all(
102+
filterConfigs.map((config) => {
103+
if (req.query[config.param]?.length) {
104+
return config.convert(apos, req, req.query[config.param]);
105+
}
106+
return Promise.resolve([]);
107+
}),
108+
);
40109
let filteredQuery = query;
41-
42-
if (req.query.industry && req.query.industry.length > 0) {
43-
const industryIds = await NavigationService.convertSlugsToIds(
44-
apos,
45-
req,
46-
req.query.industry,
47-
);
48-
if (industryIds.length > 0) {
110+
for (let index = 0; index < filterConfigs.length; index += 1) {
111+
const ids = idArrays[index];
112+
if (ids.length > 0) {
49113
filteredQuery = filteredQuery.and({
50-
industryIds: { $in: industryIds },
114+
[filterConfigs[index].field]: { $in: ids },
51115
});
52116
}
53117
}
54-
55-
if (req.query.stack && req.query.stack.length > 0) {
56-
const stackIds = await NavigationService.convertSlugsToIds(
57-
apos,
58-
req,
59-
req.query.stack,
60-
);
61-
if (stackIds.length > 0) {
62-
filteredQuery = filteredQuery.and({ stackIds: { $in: stackIds } });
63-
}
64-
}
65-
66-
if (req.query.caseStudyType && req.query.caseStudyType.length > 0) {
67-
const caseStudyTypeIds = await NavigationService.convertSlugsToIds(
68-
apos,
69-
req,
70-
req.query.caseStudyType,
71-
);
72-
if (caseStudyTypeIds.length > 0) {
73-
filteredQuery = filteredQuery.and({
74-
caseStudyTypeIds: { $in: caseStudyTypeIds },
75-
});
76-
}
77-
}
78-
79118
return filteredQuery;
80119
}
81120

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

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
*/
1010
class TagCountService {
1111
/**
12-
* Creates a mapping from tag IDs to tag slugs
13-
* @param {Array} casesTags - Array of tag objects
14-
* @returns {Object} Map of tag ID to slug
12+
* Creates a mapping from document ID to slug for any array of docs with aposDocId and slug
13+
* @param {Array} docs - Array of document objects with aposDocId and slug
14+
* @returns {Object} Map of document ID to slug
1515
*/
16-
static createTagMap(casesTags) {
17-
const tagMap = {};
18-
casesTags.forEach((tag) => {
19-
tagMap[tag.aposDocId] = tag.slug;
16+
static createIdToSlugMap(docs) {
17+
const map = {};
18+
docs.forEach((doc) => {
19+
map[doc.aposDocId] = doc.slug;
2020
});
21-
return tagMap;
21+
return map;
2222
}
2323

2424
/**
@@ -47,21 +47,25 @@ class TagCountService {
4747
* @param {Object} req - ApostropheCMS request object
4848
* @param {Object} aposModules - ApostropheCMS modules
4949
* @param {Object} options - Module options
50-
* @returns {Promise<Array>} Promise resolving to [caseStudies, casesTags] arrays
50+
* @returns {Promise<Array>} Promise resolving to [caseStudies, casesTags, businessPartners] arrays
5151
*/
5252
static async fetchCaseStudiesAndTags(req, aposModules, options) {
53-
const caseStudies = await aposModules[options.pieces].find(req).toArray();
54-
const casesTags = await aposModules['cases-tags'].find(req).toArray();
55-
return [caseStudies, casesTags];
53+
const [caseStudies, casesTags, businessPartners] = await Promise.all([
54+
aposModules[options.pieces].find(req).toArray(),
55+
aposModules['cases-tags'].find(req).toArray(),
56+
aposModules['business-partner'].find(req).toArray(),
57+
]);
58+
return [caseStudies, casesTags, businessPartners];
5659
}
5760

5861
/**
5962
* Processes case studies to count tags by type
6063
* @param {Array} caseStudies - Array of case study objects
6164
* @param {Object} tagMap - Map of tag ID to slug
65+
* @param {Object} partnerMap - Map of partner ID to slug
6266
* @param {Object} tagCounts - Object to store all tag counts
6367
*/
64-
static processCaseStudies(caseStudies, tagMap, tagCounts) {
68+
static processCaseStudies(caseStudies, tagMap, partnerMap, tagCounts) {
6569
caseStudies.forEach((study) => {
6670
TagCountService.countTagsOfType(
6771
study.industryIds,
@@ -76,6 +80,12 @@ class TagCountService {
7680
tagMap,
7781
tagCounts.caseStudyType,
7882
);
83+
84+
TagCountService.countTagsOfType(
85+
study.partnerIds,
86+
partnerMap,
87+
tagCounts.partner,
88+
);
7989
});
8090
}
8191

@@ -91,14 +101,21 @@ class TagCountService {
91101
industry: {},
92102
stack: {},
93103
caseStudyType: {},
104+
partner: {},
94105
};
95106

96-
const [caseStudies, casesTags] =
107+
const [caseStudies, casesTags, businessPartners] =
97108
await TagCountService.fetchCaseStudiesAndTags(req, aposModules, options);
98109

99-
const tagMap = TagCountService.createTagMap(casesTags);
110+
const tagMap = TagCountService.createIdToSlugMap(casesTags);
111+
const partnerMap = TagCountService.createIdToSlugMap(businessPartners);
100112

101-
TagCountService.processCaseStudies(caseStudies, tagMap, tagCounts);
113+
TagCountService.processCaseStudies(
114+
caseStudies,
115+
tagMap,
116+
partnerMap,
117+
tagCounts,
118+
);
102119

103120
return tagCounts;
104121
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ class UrlService {
4141
});
4242
}
4343

44+
// Add partner parameters
45+
if (query.partner) {
46+
const partners = UrlService.ensureArray(query.partner);
47+
partners.forEach((partner, index) => {
48+
params.append(`partner[${index}]`, partner);
49+
});
50+
}
51+
4452
// Add search parameter
4553
if (query.search) {
4654
params.append('search', query.search);

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<div class="cs_filter-info">
2020
<div class="filters-cta">Filter Case Studies</div>
2121
{% set hasActiveFilters = data.query.industry or data.query.stack or
22-
data.query.caseStudyType %} {% if hasActiveFilters %}
22+
data.query.caseStudyType or data.query.partner %} {% if hasActiveFilters %}
2323
<p class="items-count">
2424
{{ data.totalPieces }} Ite{% if data.totalPieces == 1 %}m{% else %}ms{%
2525
endif %} Found
@@ -32,7 +32,7 @@
3232
<div class="clear-all">
3333
<a
3434
class="clear-all-link"
35-
href="{{ data.url | build({ industry: null, stack: null, caseStudyType: null }) }}#filter"
35+
href="{{ data.url | build({ industry: null, stack: null, caseStudyType: null, partner: null }) }}#filter"
3636
aria-label="Clear all filters"
3737
>
3838
<span class="clear-all-content">
@@ -46,7 +46,7 @@
4646

4747
<div class="selected-tags">
4848
<ul class="selected-tags-list">
49-
{% for filterType in ['industry', 'stack', 'caseStudyType'] %} {% if
49+
{% for filterType in ['industry', 'stack', 'caseStudyType', 'partner'] %} {% if
5050
data.query[filterType] %} {% for tag in data.piecesFilters[filterType]
5151
%} {# Check if tag is selected (handle both array and string) #} {%
5252
set isTagSelected = false %} {% if data.query[filterType].length is
@@ -79,6 +79,14 @@
7979
>
8080
<img src="/images/close.svg" alt="Close Icon" />
8181
</a>
82+
{% elif filterType == 'partner' %}
83+
<a
84+
class="remove-tag"
85+
href="{{ data.url | build({}, { partner: { $pull: tag.value } }) }}#filter"
86+
aria-label="Remove partner tag {{ tag.label }}"
87+
>
88+
<img src="/images/close.svg" alt="Close Icon" />
89+
</a>
8290
{% endif %}
8391
</li>
8492
{% endif %} {% endfor %} {% endif %} {% endfor %}
@@ -91,7 +99,7 @@
9199
<div class="tags-filter">
92100
{# Display all filter types #} {% for filterType, filterLabel in [
93101
['industry', 'Industry'], ['caseStudyType', 'Case Type'], ['stack',
94-
'Technology'] ] %} {% set tags = data.piecesFilters[filterType] %} {% if
102+
'Technology'], ['partner', 'Partner'] ] %} {% set tags = data.piecesFilters[filterType] %} {% if
95103
tags and tags.length %}
96104
<div class="filter-section">
97105
<!-- prettier-ignore -->
@@ -175,6 +183,14 @@
175183
<span class="tag-label">{{ tag.label }}</span>
176184
<span class="tag-count">[ {{ count }} ]</span>
177185
</a>
186+
{% elif filterType == 'partner' %}
187+
<a
188+
class="tag-link"
189+
href="{{ data.url | build({}, { partner: { $pull: tag.value } }) }}#filter"
190+
>
191+
<span class="tag-label">{{ tag.label }}</span>
192+
<span class="tag-count">[ {{ count }} ]</span>
193+
</a>
178194
{% endif %}
179195
</li>
180196
{% else %}
@@ -206,6 +222,14 @@
206222
<span class="tag-label">{{ tag.label }}</span>
207223
<span class="tag-count">[ {{ count }} ]</span>
208224
</a>
225+
{% elif filterType == 'partner' %}
226+
<a
227+
class="tag-link"
228+
href="{{ data.url | build({}, { partner: { $addToSet: tag.value } }) }}#filter"
229+
>
230+
<span class="tag-label">{{ tag.label }}</span>
231+
<span class="tag-count">[ {{ count }} ]</span>
232+
</a>
209233
{% endif %}
210234
</li>
211235
{% endif %} {% endfor %}

website/modules/case-studies/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ module.exports = {
199199
_stack: {
200200
label: 'Stack',
201201
},
202+
_partner: {
203+
label: 'Partner',
204+
},
202205
},
203206
},
204207
helpers() {

0 commit comments

Comments
 (0)