From 35a9480dcfd644b67498fef62bf0c550d7aec9e0 Mon Sep 17 00:00:00 2001 From: Sekar C Mca Date: Wed, 22 Oct 2025 18:33:21 +0530 Subject: [PATCH 1/2] Enhance FAQ component to support filtering on subcategory This commit implements the enhancement requested in issue #6935. It adds support for filtering FAQs by both category and subcategory, with dynamic subcategory options based on selected categories. Uses SistentThemeProvider and theme tokens for consistent styling. Signed-off-by: Sekar Signed-off-by: Sekar C Mca --- src/sections/General/Faq/faqSection.style.js | 119 ++++++- src/sections/General/Faq/index.js | 333 +++++++++++++++---- 2 files changed, 383 insertions(+), 69 deletions(-) diff --git a/src/sections/General/Faq/faqSection.style.js b/src/sections/General/Faq/faqSection.style.js index 51e9de281704a..485da92d8ed16 100644 --- a/src/sections/General/Faq/faqSection.style.js +++ b/src/sections/General/Faq/faqSection.style.js @@ -18,6 +18,109 @@ const FaqSectionWrapper = styled.section` text-transform: capitalize; opacity: 0.8; } + + .filter-controls { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 1.5rem; + + button { + margin-right: 1rem; + } + } + + .filter-container { + background-color: ${props => props.theme.secondaryLightColor}; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border: 1px solid ${props => props.theme.shadowLightColor}; + } + + .filter-title { + color: ${props => props.theme.primaryColor}; + font-weight: 500; + margin-bottom: 1rem; + border-bottom: 1px solid ${props => props.theme.shadowLightColor}; + padding-bottom: 0.5rem; + text-align: center; + } + + .filter-group { + margin-bottom: 1.5rem; + + h3 { + font-size: 1.2rem; + margin-bottom: 1rem; + color: ${props => props.theme.primaryColor}; + } + + p { + margin: 0.5rem 0; + font-size: 0.9rem; + font-style: italic; + color: ${props => props.theme.textColor}; + } + } + .filter-options { + display: flex; + flex-wrap: wrap; + gap: 1rem; + } + + .filter-option { + display: flex; + align-items: center; + margin-right: 1rem; + margin-bottom: 0.5rem; + + input[type="checkbox"] { + margin-right: 0.5rem; + cursor: pointer; + accent-color: ${props => props.theme.primaryColor}; + } + + label { + cursor: pointer; + font-size: 0.9rem; + } + } + + .active-filters { + margin-bottom: 1.5rem; + padding: 0.75rem 1rem; + background-color: ${props => props.theme.secondaryLightColor}; + border-radius: 4px; + border-left: 4px solid ${props => props.theme.primaryColor}; + + p { + margin: 0; + font-size: 0.9rem; + + strong { + color: ${props => props.theme.primaryColor}; + } + } + } + + .no-results { + text-align: center; + padding: 3rem 1rem; + background-color: ${props => props.theme.secondaryLightColor}; + border-radius: 8px; + margin-bottom: 2rem; + + h3 { + margin-bottom: 1rem; + color: ${props => props.theme.textColor}; + } + + p { + margin-bottom: 1rem; + } + } + .accordion__item + .accordion__item { border-color: transparent; } @@ -96,6 +199,20 @@ const FaqSectionWrapper = styled.section` } @media only screen and (max-width: 568px) { + .filter-controls { + flex-direction: column; + align-items: flex-start; + + button { + margin-bottom: 0.75rem; + width: 100%; + } + } + + .filter-option { + width: 100%; + } + .section-title { text-align: center; } @@ -116,4 +233,4 @@ const FaqSectionWrapper = styled.section` } `; -export default FaqSectionWrapper; +export default FaqSectionWrapper; \ No newline at end of file diff --git a/src/sections/General/Faq/index.js b/src/sections/General/Faq/index.js index 22f8e8b89de4d..254ae0c7b0d30 100644 --- a/src/sections/General/Faq/index.js +++ b/src/sections/General/Faq/index.js @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; -import { Container } from "../../../reusecore/Layout"; +import { Container, Row, Col } from "../../../reusecore/Layout"; import SectionTitle from "../../../reusecore/SectionTitle"; -// import { FiSearch } from "@react-icons/all-files/fi/FiSearch"; +import { useStyledDarkMode } from "../../../theme/app/useStyledDarkMode"; +import { SistentThemeProvider, Collapse } from "@sistent/sistent"; import Button from "../../../reusecore/Button"; import { Accordion, @@ -22,22 +23,106 @@ import data from "../../../assets/data/faq"; import FaqSectionWrapper from "./faqSection.style"; const Faq = (props) => { + const propCategory = props.category; + const propSubcategory = props.subcategory; + const { isDark } = useStyledDarkMode(); + + const allCategories = [...new Set(data.faqs.map(faq => faq.category))].sort(); + + const allSubcategoriesRaw = [...new Set(data.faqs.map(faq => faq.subcategory || "General"))].sort(); + const allSubcategories = allSubcategoriesRaw.filter(subcat => !allCategories.includes(subcat)); + + const normalizeToArray = (val) => { + if (!val && val !== 0) return []; + return Array.isArray(val) ? val : [val]; + }; + + const [selectedCategories, setSelectedCategories] = useState(normalizeToArray(propCategory)); + const [selectedSubcategories, setSelectedSubcategories] = useState(normalizeToArray(propSubcategory)); + const [showFilters, setShowFilters] = useState(false); + + const [availableSubcategories, setAvailableSubcategories] = useState(allSubcategories); + + useEffect(() => { + if (propCategory !== undefined) setSelectedCategories(normalizeToArray(propCategory)); + if (propSubcategory !== undefined) setSelectedSubcategories(normalizeToArray(propSubcategory)); + }, [propCategory, propSubcategory]); + + const getSubcategoriesForCategories = (categories) => { + if (!categories || categories.length === 0) { + return allSubcategories; + } + + const subCategories = new Set(); + data.faqs.forEach(faq => { + if (categories.includes(faq.category)) { + subCategories.add(faq.subcategory || "General"); + } + }); + + return [...subCategories].sort(); + }; + + useEffect(() => { + setAvailableSubcategories(getSubcategoriesForCategories(selectedCategories)); + }, [selectedCategories]); + + const handleCategoryChange = (category) => { + let newCategories; + + if (selectedCategories.includes(category)) { + newCategories = selectedCategories.filter(c => c !== category); + } else { + newCategories = [...selectedCategories, category]; + } + + setSelectedCategories(newCategories); + + const newAvailableSubcategories = getSubcategoriesForCategories(newCategories); + setAvailableSubcategories(newAvailableSubcategories); + + const validSubcategories = selectedSubcategories.filter( + subcat => newAvailableSubcategories.includes(subcat) + ); + + if (validSubcategories.length !== selectedSubcategories.length) { + setSelectedSubcategories(validSubcategories); + } + }; + + const handleSubcategoryChange = (subcategory) => { + if (selectedSubcategories.includes(subcategory)) { + const newSubcategories = selectedSubcategories.filter(s => s !== subcategory); + setSelectedSubcategories(newSubcategories); + } else { + const newSubcategories = [...selectedSubcategories, subcategory]; + setSelectedSubcategories(newSubcategories); + } + }; + + const resetFilters = () => { + setSelectedCategories([]); + setSelectedSubcategories([]); + setAvailableSubcategories(allSubcategories); + }; let faq_keys = []; let faqs_data = []; - if (props.category === undefined) + + if (selectedCategories.length === 0 && selectedSubcategories.length === 0) { faqs_data = data.faqs; - else { - props.category.forEach(item => { - if (item === "all") - faqs_data = data.faqs; - else { - data.faqs.forEach(faq => { - if (faq.category.toString() === item) { - faqs_data.push(faq); - } - }); - } + } else { + faqs_data = data.faqs.filter(faq => { + const matchesCategory = selectedCategories.length === 0 || + selectedCategories.includes("all") || + selectedCategories.includes(faq.category.toString()); + + const subcatValue = faq.subcategory || "General"; + const matchesSubcategory = selectedSubcategories.length === 0 || + selectedSubcategories.includes("all") || + selectedSubcategories.includes(subcatValue); + + return matchesCategory && matchesSubcategory; }); } @@ -61,7 +146,7 @@ const Faq = (props) => { return ( - + {

Frequently Asked Questions

- {/*
- - -
*/} -
- - {faq_keys.map((categoryKey, categoryIndex) => ( - -

{categoryKey}

- {Object.keys(faqs[categoryKey]).map((subcategoryKey, subcategoryIndex) => ( - - {Object.keys(faqs[categoryKey]).length > 1 && ( -

{subcategoryKey}

- )} - {faqs[categoryKey][subcategoryKey].map((faq, faqIndex) => ( - - - -
{faq.question}
- - - - - - -
-
- -
- { - faq.answer.length >= 1 ?
    {faq.answer.map((ans, id) => (
  • {ans}

  • ))}
:
- } - {faq.link && -
- {faq.link.startsWith("/") - ?
- } + +
+
+ + + +
+ + +
+

Filter by Category

+
+ {allCategories.map((cat, index) => ( +
+ handleCategoryChange(cat)} + /> + +
+ ))} +
+
+ + +
+

Filter by Subcategory

+ {selectedCategories.length > 0 ? ( +
+ {availableSubcategories.length > 0 ? ( + availableSubcategories.map((subcat, index) => ( +
+ handleSubcategoryChange(subcat)} + /> + +
+ )) + ) : ( +

No subcategories available for the selected categories.

+ )} +
+ ) : ( +
+

Please select a category first to see relevant subcategories.

+
+ {allSubcategories.map((subcat, index) => ( +
+ handleSubcategoryChange(subcat)} + /> + +
+ ))} +
- - - ))} - - ))} - - ))} - + )} +
+ +
+
+
+
+ + {(selectedCategories.length > 0 || selectedSubcategories.length > 0 || faqs_data.length > 0) && ( +
+

+ {selectedCategories.length > 0 || selectedSubcategories.length > 0 ? ( + <> + Showing {faqs_data.length} {faqs_data.length === 1 ? "FAQ" : "FAQs"} + {selectedCategories.length > 0 && ( + <> in categories: {selectedCategories.join(", ")} + )} + {selectedSubcategories.length > 0 && ( + <> under subcategories: {selectedSubcategories.join(", ")} + )} + + ) : ( + <>Showing all {faqs_data.length} FAQs + )} +

+
+ )} + + + {faqs_data.length > 0 ? ( + + {faq_keys.map((categoryKey, categoryIndex) => ( + +

{categoryKey}

+ {Object.keys(faqs[categoryKey]).map((subcategoryKey, subcategoryIndex) => ( + + {Object.keys(faqs[categoryKey]).length > 1 && ( +

{subcategoryKey}

+ )} + {faqs[categoryKey][subcategoryKey].map((faq, faqIndex) => ( + + + +
{faq.question}
+ + + + + + +
+
+ +
+ { + faq.answer.length >= 1 ?
    {faq.answer.map((ans, id) => (
  • {ans}

  • ))}
:
+ } + {faq.link && +
+ {faq.link.startsWith("/") + ?
+ } +
+
+
+ ))} +
+ ))} +
+ ))} +
+ ) : ( +
+

No FAQs match the selected filters

+

Please try adjusting your filter criteria or

+ )} +

Didn't find an answer to your question?

-
); }; -export default Faq; +export default Faq; \ No newline at end of file From c50cc9a54c168eb03cb14ee747b1c254befdcd89 Mon Sep 17 00:00:00 2001 From: Sekar C Mca Date: Sun, 26 Oct 2025 23:01:24 +0530 Subject: [PATCH 2/2] fix: use SistentThemeProvider with proper theme tokens for dark mode support - Use Box component from @sistent/sistent with theme tokens - Replace hard-coded colors with background.default and divider tokens - Implement Collapse component properly for filter transitions - Remove unused Row and Col imports - Fix dark mode visibility issues with proper theme-aware styling Signed-off-by: Sekar C Mca --- src/sections/General/Faq/faqSection.style.js | 28 +- src/sections/General/Faq/index.js | 299 ++++++++----------- 2 files changed, 138 insertions(+), 189 deletions(-) diff --git a/src/sections/General/Faq/faqSection.style.js b/src/sections/General/Faq/faqSection.style.js index 485da92d8ed16..1630a14e8e5eb 100644 --- a/src/sections/General/Faq/faqSection.style.js +++ b/src/sections/General/Faq/faqSection.style.js @@ -30,19 +30,10 @@ const FaqSectionWrapper = styled.section` } } - .filter-container { - background-color: ${props => props.theme.secondaryLightColor}; - padding: 1.5rem; - border-radius: 8px; - margin-bottom: 2rem; - border: 1px solid ${props => props.theme.shadowLightColor}; - } - .filter-title { color: ${props => props.theme.primaryColor}; font-weight: 500; margin-bottom: 1rem; - border-bottom: 1px solid ${props => props.theme.shadowLightColor}; padding-bottom: 0.5rem; text-align: center; } @@ -55,13 +46,13 @@ const FaqSectionWrapper = styled.section` margin-bottom: 1rem; color: ${props => props.theme.primaryColor}; } - - p { - margin: 0.5rem 0; - font-size: 0.9rem; - font-style: italic; - color: ${props => props.theme.textColor}; - } + } + + .filter-message { + margin: 0.5rem 0; + font-size: 0.9rem; + font-style: italic; + opacity: 0.8; } .filter-options { display: flex; @@ -86,6 +77,11 @@ const FaqSectionWrapper = styled.section` font-size: 0.9rem; } } + + .filter-message { + padding: 0.5rem; + border-radius: 4px; + } .active-filters { margin-bottom: 1.5rem; diff --git a/src/sections/General/Faq/index.js b/src/sections/General/Faq/index.js index 254ae0c7b0d30..b9be51310dd9a 100644 --- a/src/sections/General/Faq/index.js +++ b/src/sections/General/Faq/index.js @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; -import { Container, Row, Col } from "../../../reusecore/Layout"; +import { Container } from "../../../reusecore/Layout"; import SectionTitle from "../../../reusecore/SectionTitle"; import { useStyledDarkMode } from "../../../theme/app/useStyledDarkMode"; -import { SistentThemeProvider, Collapse } from "@sistent/sistent"; +import { SistentThemeProvider, Collapse, Box } from "@sistent/sistent"; import Button from "../../../reusecore/Button"; import { Accordion, @@ -19,139 +19,104 @@ import { IoIosArrowDown } from "@react-icons/all-files/io/IoIosArrowDown"; import { IoIosArrowUp } from "@react-icons/all-files/io/IoIosArrowUp"; import data from "../../../assets/data/faq"; - import FaqSectionWrapper from "./faqSection.style"; -const Faq = (props) => { - const propCategory = props.category; - const propSubcategory = props.subcategory; +const Faq = () => { const { isDark } = useStyledDarkMode(); + // Extract all available categories const allCategories = [...new Set(data.faqs.map(faq => faq.category))].sort(); - const allSubcategoriesRaw = [...new Set(data.faqs.map(faq => faq.subcategory || "General"))].sort(); - const allSubcategories = allSubcategoriesRaw.filter(subcat => !allCategories.includes(subcat)); - - const normalizeToArray = (val) => { - if (!val && val !== 0) return []; - return Array.isArray(val) ? val : [val]; - }; - - const [selectedCategories, setSelectedCategories] = useState(normalizeToArray(propCategory)); - const [selectedSubcategories, setSelectedSubcategories] = useState(normalizeToArray(propSubcategory)); - const [showFilters, setShowFilters] = useState(false); - - const [availableSubcategories, setAvailableSubcategories] = useState(allSubcategories); - - useEffect(() => { - if (propCategory !== undefined) setSelectedCategories(normalizeToArray(propCategory)); - if (propSubcategory !== undefined) setSelectedSubcategories(normalizeToArray(propSubcategory)); - }, [propCategory, propSubcategory]); - + // Function to get subcategories for selected categories const getSubcategoriesForCategories = (categories) => { - if (!categories || categories.length === 0) { - return allSubcategories; - } - + if (!categories || categories.length === 0) return []; const subCategories = new Set(); data.faqs.forEach(faq => { if (categories.includes(faq.category)) { subCategories.add(faq.subcategory || "General"); } }); - return [...subCategories].sort(); }; - useEffect(() => { - setAvailableSubcategories(getSubcategoriesForCategories(selectedCategories)); - }, [selectedCategories]); + // State for filters + const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedSubcategories, setSelectedSubcategories] = useState([]); + const [showFilters, setShowFilters] = useState(false); + // Handle category filter change const handleCategoryChange = (category) => { let newCategories; - if (selectedCategories.includes(category)) { + // Remove category and its subcategories newCategories = selectedCategories.filter(c => c !== category); + const remainingValidSubcategories = getSubcategoriesForCategories(newCategories); + setSelectedSubcategories(prev => + prev.filter(sub => remainingValidSubcategories.includes(sub)) + ); } else { + // Add category newCategories = [...selectedCategories, category]; } - setSelectedCategories(newCategories); - - const newAvailableSubcategories = getSubcategoriesForCategories(newCategories); - setAvailableSubcategories(newAvailableSubcategories); - - const validSubcategories = selectedSubcategories.filter( - subcat => newAvailableSubcategories.includes(subcat) - ); - - if (validSubcategories.length !== selectedSubcategories.length) { - setSelectedSubcategories(validSubcategories); - } }; + // Handle subcategory filter change const handleSubcategoryChange = (subcategory) => { - if (selectedSubcategories.includes(subcategory)) { - const newSubcategories = selectedSubcategories.filter(s => s !== subcategory); - setSelectedSubcategories(newSubcategories); - } else { - const newSubcategories = [...selectedSubcategories, subcategory]; - setSelectedSubcategories(newSubcategories); - } + setSelectedSubcategories(prev => + prev.includes(subcategory) + ? prev.filter(s => s !== subcategory) + : [...prev, subcategory] + ); }; + // Reset filters const resetFilters = () => { setSelectedCategories([]); setSelectedSubcategories([]); - setAvailableSubcategories(allSubcategories); }; - let faq_keys = []; - let faqs_data = []; + // Get available subcategories based on selected categories + const availableSubcategories = getSubcategoriesForCategories(selectedCategories); - if (selectedCategories.length === 0 && selectedSubcategories.length === 0) { - faqs_data = data.faqs; - } else { - faqs_data = data.faqs.filter(faq => { - const matchesCategory = selectedCategories.length === 0 || - selectedCategories.includes("all") || - selectedCategories.includes(faq.category.toString()); + // Filter FAQs based on selected categories and subcategories + const faqs_data = data.faqs.filter(faq => { + // If no categories selected, show all FAQs + if (selectedCategories.length === 0) return true; - const subcatValue = faq.subcategory || "General"; - const matchesSubcategory = selectedSubcategories.length === 0 || - selectedSubcategories.includes("all") || - selectedSubcategories.includes(subcatValue); + // Check if FAQ's category is selected + const categoryMatches = selectedCategories.includes(faq.category); + if (!categoryMatches) return false; - return matchesCategory && matchesSubcategory; - }); - } + // If category matches but no subcategories selected, show all FAQs in that category + if (selectedSubcategories.length === 0) return true; - let faqs = faqs_data.reduce((faq, ind) => { - const category = ind.category; - const subcategory = ind.subcategory || "General"; + // Check if FAQ's subcategory matches + const faqSubcategory = faq.subcategory || "General"; + return selectedSubcategories.includes(faqSubcategory); + }); - if (!faq[category]) { - faq[category] = {}; - } + // Group FAQs by category and subcategory + const faqs = faqs_data.reduce((acc, faq) => { + const category = faq.category; + const subcategory = faq.subcategory || "General"; - if (!faq[category][subcategory]) { - faq[category][subcategory] = []; + if (!acc[category]) { + acc[category] = {}; } - - faq[category][subcategory].push(ind); - return faq; + if (!acc[category][subcategory]) { + acc[category][subcategory] = []; + } + acc[category][subcategory].push(faq); + return acc; }, {}); - faq_keys = Object.keys(faqs); + const faq_keys = Object.keys(faqs); return ( - +

Frequently Asked Questions

@@ -162,9 +127,9 @@ const Faq = (props) => { onClick={() => setShowFilters(!showFilters)} title={showFilters ? "Hide Filters" : "Show Filters"} /> - {(selectedCategories.length > 0 || selectedSubcategories.length > 0) && (
- -
- - -
-

Filter by Category

-
- {allCategories.map((cat, index) => ( + + + +

Filter by Category

+
+ {allCategories.map((cat, index) => ( +
+ handleCategoryChange(cat)} + /> + +
+ ))} +
+
+ +

Filter by Subcategory

+ {selectedCategories.length > 0 ? ( +
+ {availableSubcategories.length > 0 ? ( + availableSubcategories.map((subcat, index) => (
handleCategoryChange(cat)} + id={`subcategory-${subcat}`} + checked={selectedSubcategories.includes(subcat)} + onChange={() => handleSubcategoryChange(subcat)} /> - +
- ))} -
-
- - -
-

Filter by Subcategory

- {selectedCategories.length > 0 ? ( -
- {availableSubcategories.length > 0 ? ( - availableSubcategories.map((subcat, index) => ( -
- handleSubcategoryChange(subcat)} - /> - -
- )) - ) : ( -

No subcategories available for the selected categories.

- )} -
+ )) ) : ( -
-

Please select a category first to see relevant subcategories.

-
- {allSubcategories.map((subcat, index) => ( -
- handleSubcategoryChange(subcat)} - /> - -
- ))} -
-
+

No subcategories available for the selected categories.

)}
- - -
+ ) : ( +

Please select a category first to see relevant subcategories.

+ )} + + - {(selectedCategories.length > 0 || selectedSubcategories.length > 0 || faqs_data.length > 0) && ( + {faqs_data.length > 0 && (

- {selectedCategories.length > 0 || selectedSubcategories.length > 0 ? ( - <> - Showing {faqs_data.length} {faqs_data.length === 1 ? "FAQ" : "FAQs"} - {selectedCategories.length > 0 && ( - <> in categories: {selectedCategories.join(", ")} - )} - {selectedSubcategories.length > 0 && ( - <> under subcategories: {selectedSubcategories.join(", ")} - )} - - ) : ( - <>Showing all {faqs_data.length} FAQs + Showing {faqs_data.length} {faqs_data.length === 1 ? "FAQ" : "FAQs"} + {selectedCategories.length > 0 && ( + <> in categories: {selectedCategories.join(", ")} + )} + {selectedSubcategories.length > 0 && ( + <> under subcategories: {selectedSubcategories.join(", ")} )}

@@ -291,17 +235,26 @@ const Faq = (props) => {
- { - faq.answer.length >= 1 ?
    {faq.answer.map((ans, id) => (
  • {ans}

  • ))}
:
- } - {faq.link && + {faq.answer.length >= 1 ? ( +
    + {faq.answer.map((ans, id) => ( +
  • {ans}

  • + ))} +
+ ) : ( +
+ )} + {faq.link && (
- {faq.link.startsWith("/") - ?
- } + )}