From d87f936deee03b37802f5cb355996cc5270b144c Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 16:16:12 +0300 Subject: [PATCH 01/11] Add case study pagination and navigation - Extract tag counting logic into TagCountService - Add NavigationService for previous/next case study navigation - Implement previous/next buttons on case study detail pages - Add back to all projects link with preserved filter state - Preserve query parameters across navigation links - Add CSS styling for navigation controls --- website/modules/asset/ui/src/scss/_cases.scss | 56 ++++ website/modules/case-studies-page/index.js | 89 +++--- .../services/NavigationService.js | 91 ++++++ .../services/TagCountService.js | 79 +++++ .../case-studies-page/views/index.html | 17 +- .../modules/case-studies-page/views/show.html | 269 +++++++++++------- 6 files changed, 456 insertions(+), 145 deletions(-) create mode 100644 website/modules/case-studies-page/services/NavigationService.js create mode 100644 website/modules/case-studies-page/services/TagCountService.js diff --git a/website/modules/asset/ui/src/scss/_cases.scss b/website/modules/asset/ui/src/scss/_cases.scss index 06241388..2ca35c47 100644 --- a/website/modules/asset/ui/src/scss/_cases.scss +++ b/website/modules/asset/ui/src/scss/_cases.scss @@ -796,6 +796,62 @@ } } +// Case Study Navigation +.cs_navigation { + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + gap: 44px; + position: absolute; + width: 261px; + height: 13px; + right: 20px; + top: 20px; + z-index: 2; + + @include breakpoint-medium { + right: 40px; + top: 40px; + } +} + +.cs_nav-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + gap: 12px; + height: 13px; + flex: none; + flex-grow: 0; +} + +.cs_nav-text { + font-family: 'Poppins', sans-serif; + font-style: normal; + font-weight: $font-weight-300; + font-size: 11px; + line-height: 120%; + text-transform: lowercase; + text-decoration: none; + flex: none; + flex-grow: 0; + + &--active { + color: $gray-500; + + &:hover { + text-decoration: underline; + } + } + + &--disabled { + color: $gray-300; + cursor: default; + } +} + // Keyframes for smooth animations @keyframes fadeInUp { from { diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index d03b2b9b..065d2d28 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -1,50 +1,6 @@ const mainWidgets = require('../../lib/mainWidgets'); - -// Helper utility functions for tag counting -const tagCountHelpers = { - createTagMap(casesTags) { - const tagMap = {}; - casesTags.forEach((tag) => { - tagMap[tag.aposDocId] = tag.slug; - }); - return tagMap; - }, - - countTagsOfType(tagIds, tagMap, countsForType) { - if (!tagIds?.length) { - return; - } - tagIds.forEach((tagId) => { - const tagSlug = tagMap[tagId]; - if (tagSlug && !countsForType[tagSlug]) { - countsForType[tagSlug] = 0; - } - if (tagSlug) { - countsForType[tagSlug] += 1; - } - }); - }, - - async fetchCaseStudiesAndTags(req, aposModules, options) { - const caseStudies = await aposModules[options.pieces].find(req).toArray(); - const casesTags = await aposModules['cases-tags'].find(req).toArray(); - return [caseStudies, casesTags]; - }, - - processCaseStudies(caseStudies, tagMap, tagCounts) { - caseStudies.forEach((study) => { - this.countTagsOfType(study.industryIds, tagMap, tagCounts.industry); - - this.countTagsOfType(study.stackIds, tagMap, tagCounts.stack); - - this.countTagsOfType( - study.caseStudyTypeIds, - tagMap, - tagCounts.caseStudyType, - ); - }); - }, -}; +const TagCountService = require('./services/TagCountService'); +const NavigationService = require('./services/NavigationService'); module.exports = { extend: '@apostrophecms/piece-page-type', @@ -100,6 +56,30 @@ module.exports = { }; } }; + + self.beforeShow = async (req) => { + try { + const navigation = await self.getNavigationData(req); + + const reqCopy = req; + if (!reqCopy.data) { + reqCopy.data = {}; + } + reqCopy.data.prev = navigation.prev; + reqCopy.data.next = navigation.next; + reqCopy.data.query = req.query || {}; + } catch (error) { + self.apos.util.error('Error calculating navigation data:', error); + + const reqCopy = req; + if (!reqCopy.data) { + reqCopy.data = {}; + } + reqCopy.data.prev = null; + reqCopy.data.next = null; + reqCopy.data.query = req.query || {}; + } + }; }, methods(self) { @@ -112,18 +92,29 @@ module.exports = { }; const [caseStudies, casesTags] = - await tagCountHelpers.fetchCaseStudiesAndTags( + await TagCountService.fetchCaseStudiesAndTags( req, self.apos.modules, self.options, ); - const tagMap = tagCountHelpers.createTagMap(casesTags); + const tagMap = TagCountService.createTagMap(casesTags); - tagCountHelpers.processCaseStudies(caseStudies, tagMap, tagCounts); + TagCountService.processCaseStudies(caseStudies, tagMap, tagCounts); return tagCounts; }, + + async getNavigationData(req) { + const currentPiece = req.data.piece; + + return await NavigationService.getNavigationData( + req, + self.apos, + self, + currentPiece, + ); + }, }; }, }; diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js new file mode 100644 index 00000000..9f3dd5a6 --- /dev/null +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -0,0 +1,91 @@ +/** + * NavigationService - Single Responsibility: Case study navigation + * + * Handles all operations related to case study navigation. + * This service is responsible for: + * - Fetching filtered case studies for navigation + * - Finding current case study position in filtered list + * - Calculating previous and next case studies + */ +class NavigationService { + /** + * Gets all case studies with optional filtering applied + * @param {Object} req - ApostropheCMS request object + * @param {Object} apos - ApostropheCMS instance + * @param {boolean} applyFilters - Whether to apply current filters + * @param {Object} pageModule - Page module for filter application + * @returns {Array} Array of case study objects + */ + static async getAllCaseStudies( + req, + apos, + applyFilters = false, + pageModule = null, + ) { + let query = apos.modules['case-studies'].find(req); + + if (applyFilters && pageModule) { + query = pageModule.indexQuery(req, query) || query; + } + + // Use the same sorting as the case-studies piece type (updatedAt: -1) + return await query.sort({ updatedAt: -1 }).toArray(); + } + + /** + * Finds the index of the current case study in the filtered list + * @param {Array} allCaseStudies - Array of all case studies + * @param {Object} currentPiece - Current case study object + * @returns {number} Index of current case study (-1 if not found) + */ + static findCurrentIndex(allCaseStudies, currentPiece) { + // eslint-disable-next-line no-underscore-dangle + return allCaseStudies.findIndex((study) => study._id === currentPiece._id); + } + + /** + * Calculates previous and next case studies for navigation + * @param {Array} allCaseStudies - Array of all case studies + * @param {number} currentIndex - Index of current case study + * @returns {Object} Object with prev and next case studies + */ + static calculatePrevNext(allCaseStudies, currentIndex) { + let prev = null; + let next = null; + + // Previous: lower index (currentIndex - 1) + if (currentIndex > 0) { + prev = allCaseStudies[currentIndex - 1]; + } + + // Next: higher index (currentIndex + 1) + if (currentIndex < allCaseStudies.length - 1) { + next = allCaseStudies[currentIndex + 1]; + } + + return { prev, next }; + } + + /** + * Gets complete navigation data for a case study + * @param {Object} req - ApostropheCMS request object + * @param {Object} apos - ApostropheCMS instance + * @param {Object} pageModule - Page module for filter application + * @param {Object} currentPiece - Current case study object + * @returns {Object} Navigation data with prev and next + */ + static async getNavigationData(req, apos, pageModule, currentPiece) { + const allCaseStudies = await this.getAllCaseStudies( + req, + apos, + true, + pageModule, + ); + + const currentIndex = this.findCurrentIndex(allCaseStudies, currentPiece); + + return this.calculatePrevNext(allCaseStudies, currentIndex); + } +} + +module.exports = NavigationService; diff --git a/website/modules/case-studies-page/services/TagCountService.js b/website/modules/case-studies-page/services/TagCountService.js new file mode 100644 index 00000000..f8220e67 --- /dev/null +++ b/website/modules/case-studies-page/services/TagCountService.js @@ -0,0 +1,79 @@ +/** + * TagCountService - Single Responsibility: Tag counting and management + * + * Handles all operations related to counting and processing case study tags. + * This service is responsible for: + * - Creating tag maps from tag data + * - Counting tags by type (industry, stack, case study type) + * - Fetching and processing case studies and tags data + */ +class TagCountService { + /** + * Creates a mapping from tag IDs to tag slugs + * @param {Array} casesTags - Array of tag objects + * @returns {Object} Map of tag ID to slug + */ + static createTagMap(casesTags) { + const tagMap = {}; + casesTags.forEach((tag) => { + tagMap[tag.aposDocId] = tag.slug; + }); + return tagMap; + } + + /** + * Counts tags of a specific type and updates the counts object + * @param {Array} tagIds - Array of tag IDs to count + * @param {Object} tagMap - Map of tag ID to slug + * @param {Object} countsForType - Object to store counts for this type + */ + static countTagsOfType(tagIds, tagMap, countsForType) { + if (!tagIds?.length) { + return; + } + tagIds.forEach((tagId) => { + const tagSlug = tagMap[tagId]; + if (tagSlug && !countsForType[tagSlug]) { + countsForType[tagSlug] = 0; + } + if (tagSlug) { + countsForType[tagSlug] += 1; + } + }); + } + + /** + * Fetches case studies and tags data from the database + * @param {Object} req - ApostropheCMS request object + * @param {Object} aposModules - ApostropheCMS modules + * @param {Object} options - Module options + * @returns {Array} [caseStudies, casesTags] arrays + */ + static async fetchCaseStudiesAndTags(req, aposModules, options) { + const caseStudies = await aposModules[options.pieces].find(req).toArray(); + const casesTags = await aposModules['cases-tags'].find(req).toArray(); + return [caseStudies, casesTags]; + } + + /** + * Processes case studies to count tags by type + * @param {Array} caseStudies - Array of case study objects + * @param {Object} tagMap - Map of tag ID to slug + * @param {Object} tagCounts - Object to store all tag counts + */ + static processCaseStudies(caseStudies, tagMap, tagCounts) { + caseStudies.forEach((study) => { + this.countTagsOfType(study.industryIds, tagMap, tagCounts.industry); + + this.countTagsOfType(study.stackIds, tagMap, tagCounts.stack); + + this.countTagsOfType( + study.caseStudyTypeIds, + tagMap, + tagCounts.caseStudyType, + ); + }); + } +} + +module.exports = TagCountService; diff --git a/website/modules/case-studies-page/views/index.html b/website/modules/case-studies-page/views/index.html index 673bbc00..96c9392b 100644 --- a/website/modules/case-studies-page/views/index.html +++ b/website/modules/case-studies-page/views/index.html @@ -171,7 +171,22 @@
{% for article in data.pieces %} {# CARD START: {{ article.title }} #} - + {% set cardUrl = article._url %} {% if data.query.industry or + data.query.stack or data.query.caseStudyType or data.query.search %} {% + set queryParams = [] %} {% if data.query.industry %} {% for industry in + data.query.industry %} {% set queryParams = + queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} + {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in + data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + + loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if + data.query.caseStudyType %} {% for caseStudyType in + data.query.caseStudyType %} {% set queryParams = + queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + + caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} + {% set queryParams = queryParams.concat(['search=' + data.query.search]) + %} {% endif %} {% if queryParams.length > 0 %} {% set cardUrl = cardUrl + + '?' + queryParams.join('&') %} {% endif %} {% endif %} + {% set picture = apos.image.first(article.picture) %} {% if picture is defined %} -
- {% set picture = apos.image.first(data.piece.picture) %} {% if picture is - defined %} +
+ + - {{ picture._alt or '' }} + +
+
+ {% if data.prev %} {% set prevUrl = data.prev._url %} {% if + data.query.industry or data.query.stack or data.query.caseStudyType or + data.query.search %} {% set queryParams = [] %} {% if data.query.industry + %} {% for industry in data.query.industry %} {% set queryParams = + queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} + {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in + data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + + loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if + data.query.caseStudyType %} {% for caseStudyType in + data.query.caseStudyType %} {% set queryParams = + queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + + caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} {% + set queryParams = queryParams.concat(['search=' + data.query.search]) %} + {% endif %} {% set prevUrl = prevUrl + '?' + queryParams.join('&') %} {% + endif %} + previous project + {% else %} + previous + {% endif %} +
+ +
+ {% if data.next %} {% set nextUrl = data.next._url %} {% if + data.query.industry or data.query.stack or data.query.caseStudyType or + data.query.search %} {% set queryParams = [] %} {% if data.query.industry + %} {% for industry in data.query.industry %} {% set queryParams = + queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} + {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in + data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + + loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if + data.query.caseStudyType %} {% for caseStudyType in + data.query.caseStudyType %} {% set queryParams = + queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + + caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} {% + set queryParams = queryParams.concat(['search=' + data.query.search]) %} + {% endif %} {% set nextUrl = nextUrl + '?' + queryParams.join('&') %} {% + endif %} + next project + {% else %} + next project + {% endif %} +
+
- {% else %} +
+
+ {% set picture = apos.image.first(data.piece.picture) %} {% if picture is + defined %} -
Placeholder banner -
- {% endif %} -
-
-
- {% if data.piece.clientWebsite %} -

+ {% else %} + +
+ Placeholder banner +
+ + {% endif %} + +
+
+
+ {% if data.piece.clientWebsite %} +

+ {{ data.piece.title }} +

+ {% else %} +

{{ data.piece.title }}

+ {% endif %} {% if data.piece._industry and + data.piece._industry.length %} +
+ {% for industry in data.piece._industry %} {{ industry.title | + e}}{% if not loop.last %}, {% endif %} {% endfor %} +
+ {% endif %} {% if data.piece.portfolioTitle %} +

{{ data.piece.portfolioTitle }}

+ {% endif %} {% if data.piece.descriptor %} +

{{ data.piece.descriptor }}

+ {% endif %} {% if data.piece._caseStudyType and + data.piece._caseStudyType.length %} +
+ {% for type in data.piece._caseStudyType %} {{ type.title | e }}{% + if not loop.last %}, {% endif %} {% endfor %} +
+ {% endif %} +
+ +

- {% else %} -

{{ data.piece.title }}

- {% endif %} {% if data.piece._industry and data.piece._industry.length - %} -
- {% for industry in data.piece._industry %} {{ industry.title | e}}{% - if not loop.last %}, {% endif %} {% endfor %} -
- {% endif %} {% if data.piece.portfolioTitle %} -

{{ data.piece.portfolioTitle }}

- {% endif %} {% if data.piece.descriptor %} -

{{ data.piece.descriptor }}

- {% endif %} {% if data.piece._caseStudyType and - data.piece._caseStudyType.length %} -
- {% for type in data.piece._caseStudyType %} {{ type.title | e }}{% - if not loop.last %}, {% endif %} {% endfor %} + {% endif %} {% if data.piece.prodLink %} + {{ data.piece.prodLink }} + {% endif %}
- {% endif %}
- - -
- {% if data.piece.objective %} -
-

Objective

-

{{ data.piece.objective }}

-
- {% endif %} {% if data.piece.challenge %} -
-

Challenge

-

{{ data.piece.challenge }}

-
- {% endif %} {% if data.piece.solution %} -
-

Solution

-

{{ data.piece.solution }}

-
- {% endif %} {% if data.piece.results %} -
-

Results

-

{{ data.piece.results }}

-
- {% endif %} {% if data.piece._stack and data.piece._stack.length %} -
-

Tech stack

-

- {% for tech in data.piece._stack %} {{ tech.title | e }}{% if not - loop.last %}, {% endif %} {% endfor %} -

-
- {% endif %} -
-
-
+
+
+
{% area data.piece, 'testimonials' %}
-
{% area data.piece, 'testimonials' %}
+ {% endblock main %} From cd929ff045dac7de35676401f3545232a5cce262 Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 17:39:25 +0300 Subject: [PATCH 02/11] Fix sonar cloud issues --- .../services/NavigationService.js | 68 +++++++++++++++++-- .../modules/case-studies-page/views/show.html | 2 +- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js index 9f3dd5a6..04db94e0 100644 --- a/website/modules/case-studies-page/services/NavigationService.js +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -8,6 +8,67 @@ * - Calculating previous and next case studies */ class NavigationService { + /** + * Converts tag slugs to IDs + * @param {Object} apos - ApostropheCMS instance + * @param {Object} req - Request object + * @param {Array} slugs - Array of tag slugs + * @returns {Array} Array of tag IDs + */ + static async convertSlugsToIds(apos, req, slugs) { + const tagPromises = slugs.map((slug) => + apos.modules['cases-tags'].find(req, { slug }).toObject(), + ); + const tags = await Promise.all(tagPromises); + return tags.filter((tag) => tag).map((tag) => tag.aposDocId); + } + + /** + * Applies filters to a query based on request parameters + * @param {Object} query - ApostropheCMS query object + * @param {Object} req - Request object + * @param {Object} apos - ApostropheCMS instance + * @returns {Object} Modified query object + */ + static async applyFiltersToQuery(query, req, apos) { + let filteredQuery = query; + + if (req.query.industry && req.query.industry.length > 0) { + const industryIds = await this.convertSlugsToIds( + apos, + req, + req.query.industry, + ); + if (industryIds.length > 0) { + filteredQuery = filteredQuery.and({ + industryIds: { $in: industryIds }, + }); + } + } + + if (req.query.stack && req.query.stack.length > 0) { + const stackIds = await this.convertSlugsToIds(apos, req, req.query.stack); + if (stackIds.length > 0) { + filteredQuery = filteredQuery.and({ stackIds: { $in: stackIds } }); + } + } + + if (req.query.caseStudyType && req.query.caseStudyType.length > 0) { + const caseStudyTypeIds = await this.convertSlugsToIds( + apos, + req, + req.query.caseStudyType, + ); + if (caseStudyTypeIds.length > 0) { + filteredQuery = filteredQuery.and({ + caseStudyTypeIds: { $in: caseStudyTypeIds }, + }); + } + } + + return filteredQuery; + } + /** * Gets all case studies with optional filtering applied * @param {Object} req - ApostropheCMS request object @@ -24,11 +85,10 @@ class NavigationService { ) { let query = apos.modules['case-studies'].find(req); - if (applyFilters && pageModule) { - query = pageModule.indexQuery(req, query) || query; + if (applyFilters && req.query) { + query = await this.applyFiltersToQuery(query, req, apos); } - // Use the same sorting as the case-studies piece type (updatedAt: -1) return await query.sort({ updatedAt: -1 }).toArray(); } @@ -53,12 +113,10 @@ class NavigationService { let prev = null; let next = null; - // Previous: lower index (currentIndex - 1) if (currentIndex > 0) { prev = allCaseStudies[currentIndex - 1]; } - // Next: higher index (currentIndex + 1) if (currentIndex < allCaseStudies.length - 1) { next = allCaseStudies[currentIndex + 1]; } diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index ac322741..195355c4 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -44,7 +44,7 @@ >previous project {% else %} - previous + previous project {% endif %} From 569e7ff84cf982b6384662b063e9e0ae9a555d48 Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 17:55:52 +0300 Subject: [PATCH 03/11] fix Unexpected of a non-Promise --- website/modules/case-studies-page/index.js | 4 ++-- .../case-studies-page/services/NavigationService.js | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index 065d2d28..220e18ca 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -105,10 +105,10 @@ module.exports = { return tagCounts; }, - async getNavigationData(req) { + getNavigationData(req) { const currentPiece = req.data.piece; - return await NavigationService.getNavigationData( + return NavigationService.getNavigationData( req, self.apos, self, diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js index 04db94e0..7ba2b479 100644 --- a/website/modules/case-studies-page/services/NavigationService.js +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -16,9 +16,15 @@ class NavigationService { * @returns {Array} Array of tag IDs */ static async convertSlugsToIds(apos, req, slugs) { - const tagPromises = slugs.map((slug) => - apos.modules['cases-tags'].find(req, { slug }).toObject(), - ); + const tagPromises = slugs.map(async (slug) => { + const results = await apos.modules['cases-tags'] + .find(req, { slug }) + .toArray(); + if (results.length > 0) { + return results[0]; + } + return null; + }); const tags = await Promise.all(tagPromises); return tags.filter((tag) => tag).map((tag) => tag.aposDocId); } From 2c1b273290a57bbc6d2972ead3024c054c0e52ac Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 18:11:09 +0300 Subject: [PATCH 04/11] fix SonarCube Issue --- website/modules/case-studies-page/index.js | 4 ++-- .../case-studies-page/services/NavigationService.js | 8 ++++---- .../modules/case-studies-page/services/TagCountService.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index 220e18ca..065d2d28 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -105,10 +105,10 @@ module.exports = { return tagCounts; }, - getNavigationData(req) { + async getNavigationData(req) { const currentPiece = req.data.piece; - return NavigationService.getNavigationData( + return await NavigationService.getNavigationData( req, self.apos, self, diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js index 7ba2b479..a1a88155 100644 --- a/website/modules/case-studies-page/services/NavigationService.js +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -13,7 +13,7 @@ class NavigationService { * @param {Object} apos - ApostropheCMS instance * @param {Object} req - Request object * @param {Array} slugs - Array of tag slugs - * @returns {Array} Array of tag IDs + * @returns {Promise} Promise resolving to array of tag IDs */ static async convertSlugsToIds(apos, req, slugs) { const tagPromises = slugs.map(async (slug) => { @@ -34,7 +34,7 @@ class NavigationService { * @param {Object} query - ApostropheCMS query object * @param {Object} req - Request object * @param {Object} apos - ApostropheCMS instance - * @returns {Object} Modified query object + * @returns {Promise} Promise resolving to modified query object */ static async applyFiltersToQuery(query, req, apos) { let filteredQuery = query; @@ -81,7 +81,7 @@ class NavigationService { * @param {Object} apos - ApostropheCMS instance * @param {boolean} applyFilters - Whether to apply current filters * @param {Object} pageModule - Page module for filter application - * @returns {Array} Array of case study objects + * @returns {Promise} Promise resolving to array of case study objects */ static async getAllCaseStudies( req, @@ -136,7 +136,7 @@ class NavigationService { * @param {Object} apos - ApostropheCMS instance * @param {Object} pageModule - Page module for filter application * @param {Object} currentPiece - Current case study object - * @returns {Object} Navigation data with prev and next + * @returns {Promise} Promise resolving to navigation data with prev and next */ static async getNavigationData(req, apos, pageModule, currentPiece) { const allCaseStudies = await this.getAllCaseStudies( diff --git a/website/modules/case-studies-page/services/TagCountService.js b/website/modules/case-studies-page/services/TagCountService.js index f8220e67..ddc419e4 100644 --- a/website/modules/case-studies-page/services/TagCountService.js +++ b/website/modules/case-studies-page/services/TagCountService.js @@ -47,7 +47,7 @@ class TagCountService { * @param {Object} req - ApostropheCMS request object * @param {Object} aposModules - ApostropheCMS modules * @param {Object} options - Module options - * @returns {Array} [caseStudies, casesTags] arrays + * @returns {Promise} Promise resolving to [caseStudies, casesTags] arrays */ static async fetchCaseStudiesAndTags(req, aposModules, options) { const caseStudies = await aposModules[options.pieces].find(req).toArray(); From 407e2c07ff3899a0cdb1b4e5120ea09d263f9644 Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 18:27:14 +0300 Subject: [PATCH 05/11] added backend helper for URLs --- website/modules/case-studies-page/index.js | 93 +++++----------- .../services/NavigationService.js | 13 +++ .../services/TagCountService.js | 27 +++++ .../case-studies-page/services/UrlService.js | 100 ++++++++++++++++++ .../case-studies-page/views/index.html | 17 +-- 5 files changed, 168 insertions(+), 82 deletions(-) create mode 100644 website/modules/case-studies-page/services/UrlService.js diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index 065d2d28..f0f0241e 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -1,6 +1,7 @@ const mainWidgets = require('../../lib/mainWidgets'); const TagCountService = require('./services/TagCountService'); const NavigationService = require('./services/NavigationService'); +const UrlService = require('./services/UrlService'); module.exports = { extend: '@apostrophecms/piece-page-type', @@ -34,86 +35,46 @@ module.exports = { init(self) { self.beforeIndex = async (req) => { - try { - const tagCounts = await self.calculateTagCounts(req); - - const reqCopy = req; - if (!reqCopy.data) { - reqCopy.data = {}; - } - reqCopy.data.tagCounts = tagCounts; - } catch (error) { - self.apos.util.error('Error calculating tag counts:', error); - - const reqCopy = req; - if (!reqCopy.data) { - reqCopy.data = {}; - } - reqCopy.data.tagCounts = { - industry: {}, - stack: {}, - caseStudyType: {}, - }; - } + await self.setupIndexData(req); }; self.beforeShow = async (req) => { - try { - const navigation = await self.getNavigationData(req); - - const reqCopy = req; - if (!reqCopy.data) { - reqCopy.data = {}; - } - reqCopy.data.prev = navigation.prev; - reqCopy.data.next = navigation.next; - reqCopy.data.query = req.query || {}; - } catch (error) { - self.apos.util.error('Error calculating navigation data:', error); - - const reqCopy = req; - if (!reqCopy.data) { - reqCopy.data = {}; - } - reqCopy.data.prev = null; - reqCopy.data.next = null; - reqCopy.data.query = req.query || {}; - } + await self.setupShowData(req); }; }, methods(self) { return { - async calculateTagCounts(req) { - const tagCounts = { - industry: {}, - stack: {}, - caseStudyType: {}, - }; - - const [caseStudies, casesTags] = - await TagCountService.fetchCaseStudiesAndTags( + async setupIndexData(req) { + try { + const tagCounts = await TagCountService.calculateTagCounts( req, self.apos.modules, self.options, ); - - const tagMap = TagCountService.createTagMap(casesTags); - - TagCountService.processCaseStudies(caseStudies, tagMap, tagCounts); - - return tagCounts; + UrlService.attachIndexData(req, tagCounts); + } catch (error) { + self.apos.util.error('Error calculating tag counts:', error); + UrlService.attachIndexData(req, { + industry: {}, + stack: {}, + caseStudyType: {}, + }); + } }, - async getNavigationData(req) { - const currentPiece = req.data.piece; - - return await NavigationService.getNavigationData( - req, - self.apos, - self, - currentPiece, - ); + async setupShowData(req) { + try { + const navigation = await NavigationService.getNavigationDataForPage( + req, + self.apos, + self, + ); + UrlService.attachShowData(req, navigation); + } catch (error) { + self.apos.util.error('Error calculating navigation data:', error); + UrlService.attachShowData(req, { prev: null, next: null }); + } }, }; }, diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js index a1a88155..0ca2152d 100644 --- a/website/modules/case-studies-page/services/NavigationService.js +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -150,6 +150,19 @@ class NavigationService { return this.calculatePrevNext(allCaseStudies, currentIndex); } + + /** + * Gets navigation data for page module + * @param {Object} req - ApostropheCMS request object + * @param {Object} apos - ApostropheCMS instance + * @param {Object} pageModule - Page module for filter application + * @returns {Promise} Promise resolving to navigation data with prev and next + */ + static async getNavigationDataForPage(req, apos, pageModule) { + const currentPiece = req.data.piece; + + return await this.getNavigationData(req, apos, pageModule, currentPiece); + } } module.exports = NavigationService; diff --git a/website/modules/case-studies-page/services/TagCountService.js b/website/modules/case-studies-page/services/TagCountService.js index ddc419e4..0b825f9d 100644 --- a/website/modules/case-studies-page/services/TagCountService.js +++ b/website/modules/case-studies-page/services/TagCountService.js @@ -74,6 +74,33 @@ class TagCountService { ); }); } + + /** + * Calculates tag counts for case studies + * @param {Object} req - ApostropheCMS request object + * @param {Object} aposModules - ApostropheCMS modules + * @param {Object} options - Module options + * @returns {Promise} Tag counts by type + */ + static async calculateTagCounts(req, aposModules, options) { + const tagCounts = { + industry: {}, + stack: {}, + caseStudyType: {}, + }; + + const [caseStudies, casesTags] = await this.fetchCaseStudiesAndTags( + req, + aposModules, + options, + ); + + const tagMap = this.createTagMap(casesTags); + + this.processCaseStudies(caseStudies, tagMap, tagCounts); + + return tagCounts; + } } module.exports = TagCountService; diff --git a/website/modules/case-studies-page/services/UrlService.js b/website/modules/case-studies-page/services/UrlService.js new file mode 100644 index 00000000..1f48795e --- /dev/null +++ b/website/modules/case-studies-page/services/UrlService.js @@ -0,0 +1,100 @@ +/** + * UrlService - Single Responsibility: URL building and management + * + * Handles all operations related to building URLs with query parameters. + * This service is responsible for: + * - Building case study URLs with preserved query parameters + * - Properly encoding URL parameters + * - Handling both single values and arrays in query parameters + */ +class UrlService { + /** + * Builds a case study URL with current query parameters preserved + * @param {string} baseUrl - The base URL of the case study + * @param {Object} query - Current query parameters from the request + * @returns {string} Complete URL with query parameters + */ + static buildCaseStudyUrl(baseUrl, query = {}) { + const params = new URLSearchParams(); + + // Add industry parameters + if (query.industry) { + const industries = this.ensureArray(query.industry); + industries.forEach((industry, index) => { + params.append(`industry[${index}]`, industry); + }); + } + + // Add stack parameters + if (query.stack) { + const stacks = this.ensureArray(query.stack); + stacks.forEach((stack, index) => { + params.append(`stack[${index}]`, stack); + }); + } + + // Add case study type parameters + if (query.caseStudyType) { + const caseStudyTypes = this.ensureArray(query.caseStudyType); + caseStudyTypes.forEach((caseStudyType, index) => { + params.append(`caseStudyType[${index}]`, caseStudyType); + }); + } + + // Add search parameter + if (query.search) { + params.append('search', query.search); + } + + // Build final URL + const queryString = params.toString(); + if (queryString) { + return `${baseUrl}?${queryString}`; + } + return baseUrl; + } + + /** + * Ensures a value is an array + * @param {*} value - Value that could be string or array + * @returns {Array} Array version of the value + */ + static ensureArray(value) { + if (Array.isArray(value)) { + return value; + } + return [value]; + } + + /** + * Attaches index page data to request + * @param {Object} req - Request object + * @param {Object} tagCounts - Tag counts data + */ + static attachIndexData(req, tagCounts) { + const reqCopy = req; + if (!reqCopy.data) { + reqCopy.data = {}; + } + reqCopy.data.tagCounts = tagCounts; + reqCopy.data.buildCaseStudyUrl = (caseStudyUrl) => + this.buildCaseStudyUrl(caseStudyUrl, req.query); + } + + /** + * Attaches show page data to request + * @param {Object} req - Request object + * @param {Object} navigation - Navigation data + */ + static attachShowData(req, navigation) { + const reqCopy = req; + if (!reqCopy.data) { + reqCopy.data = {}; + } + reqCopy.data.prev = navigation.prev; + reqCopy.data.next = navigation.next; + reqCopy.data.query = req.query || {}; + } +} + +module.exports = UrlService; diff --git a/website/modules/case-studies-page/views/index.html b/website/modules/case-studies-page/views/index.html index 96c9392b..ff17f22d 100644 --- a/website/modules/case-studies-page/views/index.html +++ b/website/modules/case-studies-page/views/index.html @@ -171,22 +171,7 @@
{% for article in data.pieces %} {# CARD START: {{ article.title }} #} - {% set cardUrl = article._url %} {% if data.query.industry or - data.query.stack or data.query.caseStudyType or data.query.search %} {% - set queryParams = [] %} {% if data.query.industry %} {% for industry in - data.query.industry %} {% set queryParams = - queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} - {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in - data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' - + loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if - data.query.caseStudyType %} {% for caseStudyType in - data.query.caseStudyType %} {% set queryParams = - queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + - caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} - {% set queryParams = queryParams.concat(['search=' + data.query.search]) - %} {% endif %} {% if queryParams.length > 0 %} {% set cardUrl = cardUrl - + '?' + queryParams.join('&') %} {% endif %} {% endif %} - + {% set picture = apos.image.first(article.picture) %} {% if picture is defined %} Date: Thu, 5 Jun 2025 18:44:26 +0300 Subject: [PATCH 06/11] Replace this with class name in static methods --- .../services/NavigationService.js | 28 +++++++++++++------ .../services/TagCountService.js | 21 +++++++------- .../case-studies-page/services/UrlService.js | 8 +++--- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/website/modules/case-studies-page/services/NavigationService.js b/website/modules/case-studies-page/services/NavigationService.js index 0ca2152d..d0871f37 100644 --- a/website/modules/case-studies-page/services/NavigationService.js +++ b/website/modules/case-studies-page/services/NavigationService.js @@ -40,7 +40,7 @@ class NavigationService { let filteredQuery = query; if (req.query.industry && req.query.industry.length > 0) { - const industryIds = await this.convertSlugsToIds( + const industryIds = await NavigationService.convertSlugsToIds( apos, req, req.query.industry, @@ -53,14 +53,18 @@ class NavigationService { } if (req.query.stack && req.query.stack.length > 0) { - const stackIds = await this.convertSlugsToIds(apos, req, req.query.stack); + const stackIds = await NavigationService.convertSlugsToIds( + apos, + req, + req.query.stack, + ); if (stackIds.length > 0) { filteredQuery = filteredQuery.and({ stackIds: { $in: stackIds } }); } } if (req.query.caseStudyType && req.query.caseStudyType.length > 0) { - const caseStudyTypeIds = await this.convertSlugsToIds( + const caseStudyTypeIds = await NavigationService.convertSlugsToIds( apos, req, req.query.caseStudyType, @@ -92,7 +96,7 @@ class NavigationService { let query = apos.modules['case-studies'].find(req); if (applyFilters && req.query) { - query = await this.applyFiltersToQuery(query, req, apos); + query = await NavigationService.applyFiltersToQuery(query, req, apos); } return await query.sort({ updatedAt: -1 }).toArray(); @@ -139,16 +143,19 @@ class NavigationService { * @returns {Promise} Promise resolving to navigation data with prev and next */ static async getNavigationData(req, apos, pageModule, currentPiece) { - const allCaseStudies = await this.getAllCaseStudies( + const allCaseStudies = await NavigationService.getAllCaseStudies( req, apos, true, pageModule, ); - const currentIndex = this.findCurrentIndex(allCaseStudies, currentPiece); + const currentIndex = NavigationService.findCurrentIndex( + allCaseStudies, + currentPiece, + ); - return this.calculatePrevNext(allCaseStudies, currentIndex); + return NavigationService.calculatePrevNext(allCaseStudies, currentIndex); } /** @@ -161,7 +168,12 @@ class NavigationService { static async getNavigationDataForPage(req, apos, pageModule) { const currentPiece = req.data.piece; - return await this.getNavigationData(req, apos, pageModule, currentPiece); + return await NavigationService.getNavigationData( + req, + apos, + pageModule, + currentPiece, + ); } } diff --git a/website/modules/case-studies-page/services/TagCountService.js b/website/modules/case-studies-page/services/TagCountService.js index 0b825f9d..9494f3d4 100644 --- a/website/modules/case-studies-page/services/TagCountService.js +++ b/website/modules/case-studies-page/services/TagCountService.js @@ -63,11 +63,15 @@ class TagCountService { */ static processCaseStudies(caseStudies, tagMap, tagCounts) { caseStudies.forEach((study) => { - this.countTagsOfType(study.industryIds, tagMap, tagCounts.industry); + TagCountService.countTagsOfType( + study.industryIds, + tagMap, + tagCounts.industry, + ); - this.countTagsOfType(study.stackIds, tagMap, tagCounts.stack); + TagCountService.countTagsOfType(study.stackIds, tagMap, tagCounts.stack); - this.countTagsOfType( + TagCountService.countTagsOfType( study.caseStudyTypeIds, tagMap, tagCounts.caseStudyType, @@ -89,15 +93,12 @@ class TagCountService { caseStudyType: {}, }; - const [caseStudies, casesTags] = await this.fetchCaseStudiesAndTags( - req, - aposModules, - options, - ); + const [caseStudies, casesTags] = + await TagCountService.fetchCaseStudiesAndTags(req, aposModules, options); - const tagMap = this.createTagMap(casesTags); + const tagMap = TagCountService.createTagMap(casesTags); - this.processCaseStudies(caseStudies, tagMap, tagCounts); + TagCountService.processCaseStudies(caseStudies, tagMap, tagCounts); return tagCounts; } diff --git a/website/modules/case-studies-page/services/UrlService.js b/website/modules/case-studies-page/services/UrlService.js index 1f48795e..092c2475 100644 --- a/website/modules/case-studies-page/services/UrlService.js +++ b/website/modules/case-studies-page/services/UrlService.js @@ -19,7 +19,7 @@ class UrlService { // Add industry parameters if (query.industry) { - const industries = this.ensureArray(query.industry); + const industries = UrlService.ensureArray(query.industry); industries.forEach((industry, index) => { params.append(`industry[${index}]`, industry); }); @@ -27,7 +27,7 @@ class UrlService { // Add stack parameters if (query.stack) { - const stacks = this.ensureArray(query.stack); + const stacks = UrlService.ensureArray(query.stack); stacks.forEach((stack, index) => { params.append(`stack[${index}]`, stack); }); @@ -35,7 +35,7 @@ class UrlService { // Add case study type parameters if (query.caseStudyType) { - const caseStudyTypes = this.ensureArray(query.caseStudyType); + const caseStudyTypes = UrlService.ensureArray(query.caseStudyType); caseStudyTypes.forEach((caseStudyType, index) => { params.append(`caseStudyType[${index}]`, caseStudyType); }); @@ -78,7 +78,7 @@ class UrlService { } reqCopy.data.tagCounts = tagCounts; reqCopy.data.buildCaseStudyUrl = (caseStudyUrl) => - this.buildCaseStudyUrl(caseStudyUrl, req.query); + UrlService.buildCaseStudyUrl(caseStudyUrl, req.query); } /** From 0a9a69f62e17023050d9055a074cd235f4d769fe Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Thu, 5 Jun 2025 19:00:41 +0300 Subject: [PATCH 07/11] add reusable macro for show.html --- .../modules/case-studies-page/views/show.html | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index 195355c4..e6750a61 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -1,22 +1,20 @@ -{% extends "layout.html" %} {% block main %} +{% extends "layout.html" %} {% macro buildQueryString(query) %} {% set +queryParams = [] %} {% if query.industry %} {% for industry in query.industry %} +{% set queryParams = queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + +industry]) %} {% endfor %} {% endif %} {% if query.stack %} {% for stack in +query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + loop.index0 ++ '%5D=' + stack]) %} {% endfor %} {% endif %} {% if query.caseStudyType %} {% +for caseStudyType in query.caseStudyType %} {% set queryParams = +queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + caseStudyType]) +%} {% endfor %} {% endif %} {% if query.search %} {% set queryParams = +queryParams.concat(['search=' + query.search]) %} {% endif %} {{ '?' + +queryParams.join('&') if queryParams.length > 0 else '' }} {% endmacro %} {% +block main %}
- {% set backUrl = '/cases' %} {% if data.query.industry or data.query.stack - or data.query.caseStudyType or data.query.search %} {% set queryParams = [] - %} {% if data.query.industry %} {% for industry in data.query.industry %} {% - set queryParams = queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + - industry]) %} {% endfor %} {% endif %} {% if data.query.stack %} {% for - stack in data.query.stack %} {% set queryParams = - queryParams.concat(['stack%5B' + loop.index0 + '%5D=' + stack]) %} {% endfor - %} {% endif %} {% if data.query.caseStudyType %} {% for caseStudyType in - data.query.caseStudyType %} {% set queryParams = - queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + - caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} {% - set queryParams = queryParams.concat(['search=' + data.query.search]) %} {% - endif %} {% if queryParams.length > 0 %} {% set backUrl = backUrl + '?' + - queryParams.join('&') %} {% endif %} {% endif %} + {% set backUrl = '/cases' + buildQueryString(data.query) %} Back to all projects @@ -25,21 +23,8 @@
- {% if data.prev %} {% set prevUrl = data.prev._url %} {% if - data.query.industry or data.query.stack or data.query.caseStudyType or - data.query.search %} {% set queryParams = [] %} {% if data.query.industry - %} {% for industry in data.query.industry %} {% set queryParams = - queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} - {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in - data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + - loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if - data.query.caseStudyType %} {% for caseStudyType in - data.query.caseStudyType %} {% set queryParams = - queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + - caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} {% - set queryParams = queryParams.concat(['search=' + data.query.search]) %} - {% endif %} {% set prevUrl = prevUrl + '?' + queryParams.join('&') %} {% - endif %} + {% if data.prev %} {% set prevUrl = data.prev._url + + buildQueryString(data.query) %} previous project @@ -49,21 +34,8 @@
- {% if data.next %} {% set nextUrl = data.next._url %} {% if - data.query.industry or data.query.stack or data.query.caseStudyType or - data.query.search %} {% set queryParams = [] %} {% if data.query.industry - %} {% for industry in data.query.industry %} {% set queryParams = - queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + industry]) %} - {% endfor %} {% endif %} {% if data.query.stack %} {% for stack in - data.query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + - loop.index0 + '%5D=' + stack]) %} {% endfor %} {% endif %} {% if - data.query.caseStudyType %} {% for caseStudyType in - data.query.caseStudyType %} {% set queryParams = - queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + - caseStudyType]) %} {% endfor %} {% endif %} {% if data.query.search %} {% - set queryParams = queryParams.concat(['search=' + data.query.search]) %} - {% endif %} {% set nextUrl = nextUrl + '?' + queryParams.join('&') %} {% - endif %} + {% if data.next %} {% set nextUrl = data.next._url + + buildQueryString(data.query) %} next project From 700f700467270e9da73adc33e46265375d5aaab9 Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Mon, 9 Jun 2025 15:50:54 +0300 Subject: [PATCH 08/11] feat: enhance navigation with external SVGs and improved hover animations --- website/modules/asset/ui/src/scss/_cases.scss | 102 +++++++++++--- .../modules/case-studies-page/views/show.html | 126 ++++++++++++++---- website/public/images/arrow-back-desktop.svg | 3 + website/public/images/arrow-left.svg | 3 + website/public/images/arrow-right.svg | 3 + 5 files changed, 194 insertions(+), 43 deletions(-) create mode 100644 website/public/images/arrow-back-desktop.svg create mode 100644 website/public/images/arrow-left.svg create mode 100644 website/public/images/arrow-right.svg diff --git a/website/modules/asset/ui/src/scss/_cases.scss b/website/modules/asset/ui/src/scss/_cases.scss index 2ca35c47..0e2b72bd 100644 --- a/website/modules/asset/ui/src/scss/_cases.scss +++ b/website/modules/asset/ui/src/scss/_cases.scss @@ -797,22 +797,61 @@ } // Case Study Navigation +.cs_nav-container { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.cs_back-link { + font-weight: $font-weight-300; + font-size: 11px; + line-height: 120%; + text-decoration: none; + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 8px; + color: $gray-300; + @include breakpoint-medium { + gap: 12px; + } + &:hover { + text-decoration: underline; + cursor: pointer; + } + + .cs_back-arrow { + transition: opacity 0.2s ease; + flex-shrink: 0; + + &--desktop { + display: none; + + @include breakpoint-medium { + display: block; + } + } + + &--mobile { + display: block; + + @include breakpoint-medium { + display: none; + } + } + } +} + .cs_navigation { display: flex; flex-direction: row; align-items: center; padding: 0; - gap: 44px; - position: absolute; - width: 261px; - height: 13px; - right: 20px; - top: 20px; - z-index: 2; - + gap: 20px; @include breakpoint-medium { - right: 40px; - top: 40px; + gap: 44px; } } @@ -827,28 +866,59 @@ flex-grow: 0; } -.cs_nav-text { - font-family: 'Poppins', sans-serif; - font-style: normal; +.cs_nav-link { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; font-weight: $font-weight-300; font-size: 11px; line-height: 120%; - text-transform: lowercase; text-decoration: none; - flex: none; - flex-grow: 0; + transition: all 0.2s ease; &--active { color: $gray-500; &:hover { text-decoration: underline; + + .cs_nav-arrow[src*='arrow-left'] { + opacity: 0.7; + transform: translateX(2px); + } + + .cs_nav-arrow[src*='arrow-right'] { + opacity: 0.7; + transform: translateX(-2px); + } + } + + .cs_nav-arrow { + opacity: 1; } } &--disabled { color: $gray-300; cursor: default; + + .cs_nav-arrow { + opacity: 0.4; + } + } +} + +.cs_nav-arrow { + transition: all 0.2s ease; + flex-shrink: 0; +} + +.cs_nav-text { + display: none; + + @include breakpoint-medium { + display: block; } } diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index e6750a61..e0680638 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -12,36 +12,108 @@ block main %}
- -
- {% set backUrl = '/cases' + buildQueryString(data.query) %} - - Back to all projects - -
- - -
-
- {% if data.prev %} {% set prevUrl = data.prev._url + - buildQueryString(data.query) %} - previous project + +
+ {% set backUrl = '/cases' + buildQueryString(data.query) %} + - {% else %} - previous project - {% endif %} + + + back to all projects +
-
- {% if data.next %} {% set nextUrl = data.next._url + - buildQueryString(data.query) %} - next project - {% else %} - next project - {% endif %} + +
+
+ {% if data.prev %} {% set prevUrl = data.prev._url + + buildQueryString(data.query) %} + + + previous project + + {% else %} + + + previous project + + {% endif %} +
+ +
+ {% if data.next %} {% set nextUrl = data.next._url + + buildQueryString(data.query) %} + + next project + + + {% else %} + + next project + + + {% endif %} +
diff --git a/website/public/images/arrow-back-desktop.svg b/website/public/images/arrow-back-desktop.svg new file mode 100644 index 00000000..b022a042 --- /dev/null +++ b/website/public/images/arrow-back-desktop.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/public/images/arrow-left.svg b/website/public/images/arrow-left.svg new file mode 100644 index 00000000..c5e5533d --- /dev/null +++ b/website/public/images/arrow-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/public/images/arrow-right.svg b/website/public/images/arrow-right.svg new file mode 100644 index 00000000..e623ff55 --- /dev/null +++ b/website/public/images/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From bd6e5091614fb763578306feb97303ab92a19333 Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Mon, 9 Jun 2025 16:07:31 +0300 Subject: [PATCH 09/11] fix: add URL encoding to query string parameters --- .../modules/case-studies-page/views/show.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index e0680638..edeffa47 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -1,15 +1,15 @@ {% extends "layout.html" %} {% macro buildQueryString(query) %} {% set queryParams = [] %} {% if query.industry %} {% for industry in query.industry %} {% set queryParams = queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + -industry]) %} {% endfor %} {% endif %} {% if query.stack %} {% for stack in -query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + loop.index0 -+ '%5D=' + stack]) %} {% endfor %} {% endif %} {% if query.caseStudyType %} {% -for caseStudyType in query.caseStudyType %} {% set queryParams = -queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + caseStudyType]) -%} {% endfor %} {% endif %} {% if query.search %} {% set queryParams = -queryParams.concat(['search=' + query.search]) %} {% endif %} {{ '?' + -queryParams.join('&') if queryParams.length > 0 else '' }} {% endmacro %} {% -block main %} +(industry | urlencode)]) %} {% endfor %} {% endif %} {% if query.stack %} {% for +stack in query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + +loop.index0 + '%5D=' + (stack | urlencode)]) %} {% endfor %} {% endif %} {% if +query.caseStudyType %} {% for caseStudyType in query.caseStudyType %} {% set +queryParams = queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + +(caseStudyType | urlencode)]) %} {% endfor %} {% endif %} {% if query.search %} +{% set queryParams = queryParams.concat(['search=' + (query.search | +urlencode)]) %} {% endif %} {{ '?' + queryParams.join('&') if queryParams.length +> 0 else '' }} {% endmacro %} {% block main %}
From f3fca99a184e6c7b3fe3d1780ff6b8c00148008d Mon Sep 17 00:00:00 2001 From: IhorMasechko Date: Mon, 9 Jun 2025 19:34:51 +0300 Subject: [PATCH 10/11] fix: preserve filter state when navigating between case studies --- .../case-studies-page/services/UrlService.js | 25 +++++++++++++++++- .../modules/case-studies-page/views/show.html | 26 +++++-------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/website/modules/case-studies-page/services/UrlService.js b/website/modules/case-studies-page/services/UrlService.js index 092c2475..00a6c59c 100644 --- a/website/modules/case-studies-page/services/UrlService.js +++ b/website/modules/case-studies-page/services/UrlService.js @@ -91,9 +91,32 @@ class UrlService { if (!reqCopy.data) { reqCopy.data = {}; } + + const queryParams = req.query || {}; + + // Build complete URLs server-side to avoid template encoding issues reqCopy.data.prev = navigation.prev; reqCopy.data.next = navigation.next; - reqCopy.data.query = req.query || {}; + + // Generate URLs with query parameters server-side + if (navigation.prev) { + reqCopy.data.prevUrl = UrlService.buildCaseStudyUrl( + // eslint-disable-next-line no-underscore-dangle + navigation.prev._url, + queryParams, + ); + } + + if (navigation.next) { + reqCopy.data.nextUrl = UrlService.buildCaseStudyUrl( + // eslint-disable-next-line no-underscore-dangle + navigation.next._url, + queryParams, + ); + } + + reqCopy.data.backUrl = UrlService.buildCaseStudyUrl('/cases', queryParams); + reqCopy.data.query = queryParams; } } diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index edeffa47..e48ec5a8 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -1,23 +1,11 @@ -{% extends "layout.html" %} {% macro buildQueryString(query) %} {% set -queryParams = [] %} {% if query.industry %} {% for industry in query.industry %} -{% set queryParams = queryParams.concat(['industry%5B' + loop.index0 + '%5D=' + -(industry | urlencode)]) %} {% endfor %} {% endif %} {% if query.stack %} {% for -stack in query.stack %} {% set queryParams = queryParams.concat(['stack%5B' + -loop.index0 + '%5D=' + (stack | urlencode)]) %} {% endfor %} {% endif %} {% if -query.caseStudyType %} {% for caseStudyType in query.caseStudyType %} {% set -queryParams = queryParams.concat(['caseStudyType%5B' + loop.index0 + '%5D=' + -(caseStudyType | urlencode)]) %} {% endfor %} {% endif %} {% if query.search %} -{% set queryParams = queryParams.concat(['search=' + (query.search | -urlencode)]) %} {% endif %} {{ '?' + queryParams.join('&') if queryParams.length -> 0 else '' }} {% endmacro %} {% block main %} +{% extends "layout.html" %} {% block main %}