From dfbfbfabe922a1b4e6e7347a2526f3b2ee63451f Mon Sep 17 00:00:00 2001 From: AhmedHHamdy Date: Tue, 31 Oct 2023 11:07:37 +0200 Subject: [PATCH 1/5] Add sorting/filtering features --- client/app/home/page.jsx | 61 +++++++++++++++- client/components/client/ClientCard.jsx | 2 +- client/components/ui/ActionMenu.jsx | 92 +++++++++++++++++++++++-- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/client/app/home/page.jsx b/client/app/home/page.jsx index b789f51..3085bde 100644 --- a/client/app/home/page.jsx +++ b/client/app/home/page.jsx @@ -11,19 +11,43 @@ import { fetchClients } from '@utils/client' const Home = () => { const auth = useAuthContext() const [clients, setClients] = useState(null) + const [filteringOptions, setFilteringOptions] = useState([]); // Add state for filtering options useEffect(() => { - fetchClients().then(data => setClients(data)) + fetchClients().then(data => { + setClients(data) + setFilteringOptions(data); // Initialize filteringOptions with the fetched data + }) }, [auth?.user]) if (!auth?.checkAuth) return if (auth?.isAuthenticated === "unauthenticated") return redirect('/') + function sortClients(sortingFunction) { + // Clone the clients array and sort it using the provided sorting function. + const sortedClients = [...clients].sort(sortingFunction); + setClients(sortedClients); // We set the clients state variable with sorted array + }; + + function filterClients(data) { + setClients(data) // We set the clients state variable with the filtered array coming from action menu component + } + + function clearFilter() { + setClients(filteringOptions); // We reset the clients state variable with the original data from the server + }; + return ( <>
- +
+
{clients === null && } {clients && clients.length > 0 @@ -31,10 +55,41 @@ const Home = () => { return }) } + {clients && clients.length === 0 &&

You have not added any clients!

}
) } -export default Home \ No newline at end of file +export default Home + + +//
+// +// +// +//
+ +{/*
+
+
    +
  • Home
  • +
  • + + Profile + New + +
  • +
  • + Client
  • +
  • + Outreach
  • +
  • Settings
  • +
  • Logout
  • +
  • + +
  • +
+
+
+ + */} diff --git a/client/components/client/ClientCard.jsx b/client/components/client/ClientCard.jsx index d02a8bd..5e363cb 100644 --- a/client/components/client/ClientCard.jsx +++ b/client/components/client/ClientCard.jsx @@ -2,7 +2,7 @@ import React from 'react' import { EllipsisHorizontalIcon, PhoneIcon, EnvelopeIcon, MapPinIcon } from '@heroicons/react/24/outline' const ClientCard = ({ client }) => { return ( -
+

{client.businessName}

diff --git a/client/components/ui/ActionMenu.jsx b/client/components/ui/ActionMenu.jsx index cab85b7..8dbc92c 100644 --- a/client/components/ui/ActionMenu.jsx +++ b/client/components/ui/ActionMenu.jsx @@ -1,12 +1,94 @@ import Link from 'next/link' -import { FunnelIcon, ArrowsUpDownIcon, ListBulletIcon } from '@heroicons/react/20/solid' +import { FunnelIcon, ArrowsUpDownIcon, ListBulletIcon, ArrowRightIcon } from '@heroicons/react/20/solid' +import { useState } from 'react'; + +const ActionMenu = ({ sortClients, filterClients, clearFilter, filteringOptions }) => { + const [currentSort, setCurrentSort] = useState(''); + const [currentFilter, setCurrentFilter] = useState([]); + + function handleSortAZ() { + sortClients((a, b) => a['businessType'].localeCompare(b['businessType'], "en", { sensitivity: 'base' })); // We provide the function that is used for sorting from A to Z, case-insensitive comparison + setCurrentSort('Sorted AZ') + }; + + function handleSortZA() { + sortClients((a, b) => b['businessType'].localeCompare(a['businessType'], "en", { sensitivity: 'base' })); // We provide the function that is used for sorting from Z to A, case-insensitive comparison + setCurrentSort('Sorted ZA') + } + + + function handleFiltering(event) { + const filteringValue = event.target.value || event.target.innerHTML // We get filtering value ['clinic'] for example when you click on the checkbox input and if you're clicking on the current filter badge you get the filtering value from it to through event target inner html + const updateFilters = [...currentFilter] + + if (updateFilters.includes(filteringValue)) { // We check the if the updatedFilter array if it has any filtering values + updateFilters.splice(updateFilters.indexOf(filteringValue), 1) // if it has any filtering values, it deletes it + } else { + updateFilters.push(filteringValue) // if it hasn't, we push the filtering value to the updatedFilters + } + + let filteredData = filteringOptions.filter((option) => + updateFilters.includes(option.businessType) // We get an array of filteredData based on the filtering values + ); + + + if (filteredData.length == 0) { // if the filteredData is empty array we reassign the filteredData with filteringOptions (Original Data from the server) + filteredData = filteringOptions // this necessary because when we clear all filters the array we need ti reset to the original data + } + + filterClients(filteredData) // we call the filterClients function from the home page with filteredData + setCurrentFilter(updateFilters) // We set the currentFilter array with updatedFilters + } + + function removeFilter () { + setCurrentFilter([]) // We set the currentFilter state variable to an empty array + clearFilter() // We call the clearFilter function from the home page to reset the clients state variable to the original data from the server + } + + const filterOptionElements = filteringOptions?.map((option, i) => { + return ( + + ) + + }) + -const ActionMenu = () => { return ( -
- - +
+ {/* */} +
+ +
    +
  • A Z
  • +
  • Z A
  • +
+
+ {/* */} +
+ +
    + {filterOptionElements} +
+
+ {currentSort &&
{currentSort}
+ +} {currentFilter && +
+ {currentFilter.map((filter, i) => + (
{filter}
)) + } + {currentFilter.length > 0 &&
Clear
} +
}
) } From e075835325442208c3aa7b2acc4c67b0810dc0a0 Mon Sep 17 00:00:00 2001 From: AhmedHHamdy Date: Tue, 31 Oct 2023 15:04:18 +0200 Subject: [PATCH 2/5] remove unnessary comments --- client/app/home/page.jsx | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/client/app/home/page.jsx b/client/app/home/page.jsx index 3085bde..b8bacca 100644 --- a/client/app/home/page.jsx +++ b/client/app/home/page.jsx @@ -63,33 +63,3 @@ const Home = () => { } export default Home - - -//
-// -// -// -//
- -{/*
-
-
    -
  • Home
  • -
  • - - Profile - New - -
  • -
  • + Client
  • -
  • + Outreach
  • -
  • Settings
  • -
  • Logout
  • -
  • - -
  • -
-
-
-
- */} From 577d8060d9e0ecac8b6445afd4afed4b12c1d896 Mon Sep 17 00:00:00 2001 From: Lara Alexander Date: Tue, 31 Oct 2023 13:39:13 -0400 Subject: [PATCH 3/5] Added css class dropdown-menu and refactored some logic (minor) to the filtering functions in ActionMenu --- client/components/ui/ActionMenu.jsx | 96 +++++++++++++++-------------- client/components/ui/Header.jsx | 6 +- client/styles/globals.css | 4 ++ 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/client/components/ui/ActionMenu.jsx b/client/components/ui/ActionMenu.jsx index 8dbc92c..ce0a81e 100644 --- a/client/components/ui/ActionMenu.jsx +++ b/client/components/ui/ActionMenu.jsx @@ -1,62 +1,66 @@ -import Link from 'next/link' -import { FunnelIcon, ArrowsUpDownIcon, ListBulletIcon, ArrowRightIcon } from '@heroicons/react/20/solid' -import { useState } from 'react'; +import { FunnelIcon, ArrowsUpDownIcon, ListBulletIcon, XMarkIcon } from '@heroicons/react/20/solid' +import { useState } from 'react' const ActionMenu = ({ sortClients, filterClients, clearFilter, filteringOptions }) => { - const [currentSort, setCurrentSort] = useState(''); - const [currentFilter, setCurrentFilter] = useState([]); + const [currentSort, setCurrentSort] = useState('') + const [currentFilter, setCurrentFilter] = useState([]) function handleSortAZ() { - sortClients((a, b) => a['businessType'].localeCompare(b['businessType'], "en", { sensitivity: 'base' })); // We provide the function that is used for sorting from A to Z, case-insensitive comparison + // Sort from A to Z, case-insensitive comparison + sortClients((a, b) => a['businessType'].localeCompare(b['businessType'], "en", { sensitivity: 'base' })) setCurrentSort('Sorted AZ') }; function handleSortZA() { - sortClients((a, b) => b['businessType'].localeCompare(a['businessType'], "en", { sensitivity: 'base' })); // We provide the function that is used for sorting from Z to A, case-insensitive comparison + // Sort from Z to A, case-insensitive comparison + sortClients((a, b) => b['businessType'].localeCompare(a['businessType'], "en", { sensitivity: 'base' })) setCurrentSort('Sorted ZA') } - function handleFiltering(event) { - const filteringValue = event.target.value || event.target.innerHTML // We get filtering value ['clinic'] for example when you click on the checkbox input and if you're clicking on the current filter badge you get the filtering value from it to through event target inner html + function handleFiltering(event, filterValue) { + const filteringValue = filterValue const updateFilters = [...currentFilter] - if (updateFilters.includes(filteringValue)) { // We check the if the updatedFilter array if it has any filtering values - updateFilters.splice(updateFilters.indexOf(filteringValue), 1) // if it has any filtering values, it deletes it + // Check if filters array includes new filter value; if it does, remove it, else add to filters array + if (updateFilters.includes(filteringValue)) { + updateFilters.splice(updateFilters.indexOf(filteringValue), 1) } else { - updateFilters.push(filteringValue) // if it hasn't, we push the filtering value to the updatedFilters + updateFilters.push(filteringValue) } let filteredData = filteringOptions.filter((option) => - updateFilters.includes(option.businessType) // We get an array of filteredData based on the filtering values - ); + // We get an array of filteredData based on the filtering values + updateFilters.includes(option.businessType) + ) - if (filteredData.length == 0) { // if the filteredData is empty array we reassign the filteredData with filteringOptions (Original Data from the server) - filteredData = filteringOptions // this necessary because when we clear all filters the array we need ti reset to the original data + if (filteredData.length == 0) { + // important: when all fitlers are cleared, need to reset to original client data + filteredData = filteringOptions } - filterClients(filteredData) // we call the filterClients function from the home page with filteredData - setCurrentFilter(updateFilters) // We set the currentFilter array with updatedFilters + filterClients(filteredData) + setCurrentFilter(updateFilters) } - function removeFilter () { - setCurrentFilter([]) // We set the currentFilter state variable to an empty array - clearFilter() // We call the clearFilter function from the home page to reset the clients state variable to the original data from the server + function removeFilter() { + setCurrentFilter([]) + clearFilter() } const filterOptionElements = filteringOptions?.map((option, i) => { return ( - + ) }) @@ -64,31 +68,33 @@ const ActionMenu = ({ sortClients, filterClients, clearFilter, filteringOptions return (
- {/* */}
- +
  • A Z
  • Z A
- {/* */}
- -
    + +
      {filterOptionElements}
- {currentSort &&
{currentSort}
- -} {currentFilter && -
- {currentFilter.map((filter, i) => - (
{filter}
)) - } - {currentFilter.length > 0 &&
Clear
} -
} + {currentSort && +
+ {currentSort} +
+ } + {currentFilter && +
+ {currentFilter.map((filter, i) => + (
handleFiltering(e, filter)}>{filter}
)) + } + {currentFilter.length > 0 &&
Clear
} +
+ }
) } diff --git a/client/components/ui/Header.jsx b/client/components/ui/Header.jsx index b749ad2..6b65b06 100644 --- a/client/components/ui/Header.jsx +++ b/client/components/ui/Header.jsx @@ -41,7 +41,7 @@ const Header = ({ ThemeToggle }) => { />
-
    +
    • Home
    • @@ -56,9 +56,7 @@ const Header = ({ ThemeToggle }) => {
    • Admin Tools
    • }
    • Logout
    • -
    • - -
    • +
    diff --git a/client/styles/globals.css b/client/styles/globals.css index 71311ea..e7d51e2 100644 --- a/client/styles/globals.css +++ b/client/styles/globals.css @@ -56,6 +56,10 @@ hr { @apply w-full max-w-[500px] border-0 rounded-full mx-auto h-[2px] bg-accent; } +.dropdown-menu { + @apply mt-3 z-[1] p-2 shadow menu menu-sm dropdown-content bg-primary border border-secondary rounded-box w-52; +} + /* Forms */ form { From 50df067505b5e30a413681746bb0e0fd2ed6bb0e Mon Sep 17 00:00:00 2001 From: Lara Alexander Date: Tue, 31 Oct 2023 13:48:49 -0400 Subject: [PATCH 4/5] Added some GitHub-related files --- .github/CODEOWNERS | 2 ++ .github/pull_request_template.md | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/pull_request_template.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..89621ed --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Require specific PR reviews for entire repository +** @devlarabar \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0bd6587 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +### Description + +Put a description of your changes here. + +### Testing + +Put a short description of how you tested your changes here. + +### Did you change the schema? + +- [ ] Yes +- [ ] No + +### Issue + +Closes #ISSUE_NUM + + + From 05958e762d2c42d331eb6336c9ec3c72c3dfa5df Mon Sep 17 00:00:00 2001 From: Lara Alexander Date: Tue, 31 Oct 2023 14:08:33 -0400 Subject: [PATCH 5/5] Fixed the bug where business type filters were showing dupes, and fixed the filter being case sensitive --- client/app/home/page.jsx | 29 +++++++++++++++-------------- client/components/ui/ActionMenu.jsx | 18 ++++++++++++------ server/controllers/client.js | 2 +- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/client/app/home/page.jsx b/client/app/home/page.jsx index b8bacca..42738e4 100644 --- a/client/app/home/page.jsx +++ b/client/app/home/page.jsx @@ -11,12 +11,12 @@ import { fetchClients } from '@utils/client' const Home = () => { const auth = useAuthContext() const [clients, setClients] = useState(null) - const [filteringOptions, setFilteringOptions] = useState([]); // Add state for filtering options + const [filteringOptions, setFilteringOptions] = useState([]) useEffect(() => { fetchClients().then(data => { setClients(data) - setFilteringOptions(data); // Initialize filteringOptions with the fetched data + setFilteringOptions(data) }) }, [auth?.user]) @@ -25,26 +25,27 @@ const Home = () => { function sortClients(sortingFunction) { // Clone the clients array and sort it using the provided sorting function. - const sortedClients = [...clients].sort(sortingFunction); - setClients(sortedClients); // We set the clients state variable with sorted array - }; + const sortedClients = [...clients].sort(sortingFunction) + setClients(sortedClients) + } function filterClients(data) { - setClients(data) // We set the clients state variable with the filtered array coming from action menu component + setClients(data) } function clearFilter() { - setClients(filteringOptions); // We reset the clients state variable with the original data from the server - }; - + // Reset 'clients' to original data from the server + setClients(filteringOptions) + } + return ( <>
    -
    diff --git a/client/components/ui/ActionMenu.jsx b/client/components/ui/ActionMenu.jsx index ce0a81e..20b1bea 100644 --- a/client/components/ui/ActionMenu.jsx +++ b/client/components/ui/ActionMenu.jsx @@ -31,7 +31,7 @@ const ActionMenu = ({ sortClients, filterClients, clearFilter, filteringOptions let filteredData = filteringOptions.filter((option) => // We get an array of filteredData based on the filtering values - updateFilters.includes(option.businessType) + updateFilters.includes(option.businessType.toLowerCase()) ) @@ -49,15 +49,21 @@ const ActionMenu = ({ sortClients, filterClients, clearFilter, filteringOptions clearFilter() } - const filterOptionElements = filteringOptions?.map((option, i) => { + const businessTypesList = [...new Set( + filteringOptions + ?.map((client, i) => client.businessType.toLowerCase()) + .sort() + )] + + const filterOptionElements = businessTypesList?.map((option, i) => { return ( diff --git a/server/controllers/client.js b/server/controllers/client.js index ad08662..92e8046 100644 --- a/server/controllers/client.js +++ b/server/controllers/client.js @@ -14,7 +14,7 @@ module.exports = { const client = await Client.create({ user: user, businessName, - businessType, + businessType: businessType.toLowerCase(), address, email, phone