From 87d34912f835dfba64a9b8e0a5d4c4535ae696d6 Mon Sep 17 00:00:00 2001 From: oliviahyw <112413539+oliviahyw@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:20:53 -0600 Subject: [PATCH 01/28] FAQ components --- src/components/Faq/Faq.jsx | 21 ++++++++++ src/components/Faq/FaqManagement.jsx | 50 ++++++++++++++++++++++++ src/components/Faq/FaqSearch.jsx | 57 ++++++++++++++++++++++++++++ src/components/Faq/index.js | 3 ++ src/utils/URL.js | 5 +++ 5 files changed, 136 insertions(+) create mode 100644 src/components/Faq/Faq.jsx create mode 100644 src/components/Faq/FaqManagement.jsx create mode 100644 src/components/Faq/FaqSearch.jsx create mode 100644 src/components/Faq/index.js diff --git a/src/components/Faq/Faq.jsx b/src/components/Faq/Faq.jsx new file mode 100644 index 0000000000..010b66f8dd --- /dev/null +++ b/src/components/Faq/Faq.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import FaqSearch from './FaqSearch'; +import FaqManagement from './FaqManagement'; + +const userHasPermission = permission => { + const userPermissions = JSON.parse(localStorage.getItem('userPermissions')); + return userPermissions && userPermissions.includes(permission); +}; + +function Faq() { + return ( +
Modified by: {record.modifiedBy}
+Modified at: {new Date(record.modifiedAt).toLocaleString()}
+Previous Question: {record.previousQuestion}
+Previous Answer: {record.previousAnswer}
+No results found.
+Loading FAQ...
; + if (!faq) returnFAQ not found
; return (Modified by: {record.modifiedBy}
-Modified at: {new Date(record.modifiedAt).toLocaleString()}
-Previous Question: {record.previousQuestion}
-Previous Answer: {record.previousAnswer}
-{faq.answer}
+Modified By: {change.modifiedBy}
+Date: {new Date(change.modifiedAt).toLocaleString()}
+Previous Question: {change.previousQuestion}
+Previous Answer: {change.previousAnswer}
+No modification history available.
+ )}{faq.answer}
-{selectedFAQ.answer}
+{faq.answer}
++ Question: {faq.question} +
++ Answer: {faq.answer} +
+ ++ User: {faq.createdBy ? faq.createdBy : 'Unknown'} +
++ Date: {new Date(faq.createdAt).toLocaleString()} +
Modified By: {change.modifiedBy}
-Date: {new Date(change.modifiedAt).toLocaleString()}
-Previous Question: {change.previousQuestion}
-Previous Answer: {change.previousAnswer}
++ Updated By: {change.updatedBy ? change.updatedBy : 'Unknown'} +
++ Updated At: {new Date(change.updatedAt).toLocaleString()} +
++ Previous Question: {change.previousQuestion} +
++ Previous Answer: {change.previousAnswer} +
++ Updated Question: {change.updatedQuestion} +
++ Updated Answer: {change.updatedAnswer} +
Loading FAQs...
) : Array.isArray(faqs) && faqs.length === 0 ? ( diff --git a/src/components/Faq/FaqSearch.jsx b/src/components/Faq/FaqSearch.jsx index 3d3a0644dd..9664c7bd4d 100644 --- a/src/components/Faq/FaqSearch.jsx +++ b/src/components/Faq/FaqSearch.jsx @@ -1,5 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { debounce } from 'lodash'; +import { Button } from 'reactstrap'; import { searchFAQs, logUnansweredQuestion } from './api'; function FaqSearch() { @@ -7,6 +8,7 @@ function FaqSearch() { const [suggestions, setSuggestions] = useState([]); const [notFound, setNotFound] = useState(false); const [selectedFAQ, setSelectedFAQ] = useState(null); + const [logging, setLogging] = useState(false); const fetchSuggestions = debounce(async query => { try { @@ -23,7 +25,7 @@ function FaqSearch() { setSearchQuery(query); setSelectedFAQ(null); - if (query.length > 1) { + if (query.length >= 1) { fetchSuggestions(query); } else { setSuggestions([]); @@ -39,11 +41,17 @@ function FaqSearch() { }; const handleLogUnanswered = async () => { + if (!searchQuery.trim()) return; + + setLogging(true); try { - await logUnansweredQuestion(searchQuery); - alert('Your question has been recorded and will be reviewed.'); + const response = await logUnansweredQuestion(searchQuery); + alert(response.data.message || 'Your question has been recorded.'); } catch (error) { console.error('Error logging unanswered question:', error); + alert('Failed to log question. It may already exist.'); + } finally { + setLogging(false); } }; @@ -56,7 +64,7 @@ function FaqSearch() { onChange={handleSearchChange} placeholder="Search FAQs" /> - + {selectedFAQ ? (No results found.
-Loading unanswered FAQs...
+ ) : unansweredFaqs.length === 0 ? ( +No unanswered FAQs found.
+ ) : ( +Logged on: {new Date(faq.createdAt).toLocaleString()}
+{selectedFAQ.answer}
-{faq.answer}
+ +No results found.
FAQ not found
; return ( -- Question: {faq.question} -
-- Answer: {faq.answer} -
++ Question: {faq.question} +
++ Answer: {faq.answer} +
+- User: {faq.createdBy ? faq.createdBy : 'Unknown'} -
-- Date: {new Date(faq.createdAt).toLocaleString()} -
-+ User: {faq.createdBy ? faq.createdBy : 'Unknown'} +
++ Date: {new Date(faq.createdAt).toLocaleString()} +
+Updated By: {change.updatedBy ? change.updatedBy : 'Unknown'}
Updated At: {new Date(change.updatedAt).toLocaleString()}
-+
Previous Question: {change.previousQuestion}
-+
Previous Answer: {change.previousAnswer}
-+
Updated Question: {change.updatedQuestion}
-+
Updated Answer: {change.updatedAnswer}
No FAQs available at the moment. Please add new FAQs.
) : ( -{faq.answer}
-Loading unanswered FAQs...
+Loading unanswered FAQs...
) : unansweredFaqs.length === 0 ? ( -No unanswered FAQs found.
+No unanswered FAQs found.
) : ( -Logged on: {new Date(faq.createdAt).toLocaleString()}
++ Logged on: {new Date(faq.createdAt).toLocaleString()} +
Loading FAQs...
; + } else if (!faqs || faqs.length === 0) { + content =No FAQs available at the moment. Please add new FAQs.
; + } else { + content = ( +{faq.answer}
+Loading FAQs...
- ) : Array.isArray(faqs) && faqs.length === 0 ? ( -No FAQs available at the moment. Please add new FAQs.
- ) : ( -{faq.answer}
-Error: FAQs data is not in the correct format.
- )} -No results found.
Loading unanswered FAQs...
; + } + + if (unansweredFaqs.length === 0) { + returnNo unanswered FAQs found.
; + } + return (Loading unanswered FAQs...
- ) : unansweredFaqs.length === 0 ? ( -No unanswered FAQs found.
- ) : ( -- Logged on: {new Date(faq.createdAt).toLocaleString()} -
-+ Logged on: {new Date(faq.createdAt).toLocaleString()} +
+Loading FAQ...
; diff --git a/src/components/Faq/FaqSearch.jsx b/src/components/Faq/FaqSearch.jsx index 449f0f60d7..26600a6a76 100644 --- a/src/components/Faq/FaqSearch.jsx +++ b/src/components/Faq/FaqSearch.jsx @@ -17,11 +17,15 @@ function FaqSearch() { const [expandedFAQ, setExpandedFAQ] = useState(null); useEffect(() => { + let isMounted = true; + const fetchFAQs = async () => { try { const response = await getAllFAQs(); - setAllFAQs(response.data); - setSuggestions(response.data); + if (isMounted) { + setAllFAQs(response.data); + setSuggestions(response.data); + } } catch (error) { // eslint-disable-next-line no-console console.error('Error fetching FAQs:', error); @@ -29,6 +33,10 @@ function FaqSearch() { }; fetchFAQs(); + + return () => { + isMounted = false; + }; }, []); const fetchSuggestions = debounce(async query => { diff --git a/src/components/Faq/UnansweredFaqs.jsx b/src/components/Faq/UnansweredFaqs.jsx index f662671b9b..1f2ff6f821 100644 --- a/src/components/Faq/UnansweredFaqs.jsx +++ b/src/components/Faq/UnansweredFaqs.jsx @@ -7,20 +7,26 @@ function UnansweredFaqs() { const [loading, setLoading] = useState(true); useEffect(() => { + let isMounted = true; + const fetchUnansweredFAQs = async () => { try { const response = await getUnansweredFAQs(); - setUnansweredFaqs(response.data || []); + if (isMounted) setUnansweredFaqs(response.data || []); } catch (error) { // eslint-disable-next-line no-console console.error('Error fetching unanswered FAQs:', error); - setUnansweredFaqs([]); + if (isMounted) setUnansweredFaqs([]); } finally { - setLoading(false); + if (isMounted) setLoading(false); } }; fetchUnansweredFAQs(); + + return () => { + isMounted = false; + }; }, []); const handleMarkAsLogged = async faqId => { From 89393d65c7b79e2c0fc333ef46568375d8917c56 Mon Sep 17 00:00:00 2001 From: oliviahyw <112413539+oliviahyw@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:52:32 -0500 Subject: [PATCH 13/28] Built the frontend chart --- package-lock.json | 53 +++++++++++++++------------ package.json | 6 +-- src/components/ApplicantsAgeChart.jsx | 42 +++++++++++++++++++++ src/routes.js | 3 ++ 4 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 src/components/ApplicantsAgeChart.jsx diff --git a/package-lock.json b/package-lock.json index d0ab0f1915..b299df8e0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9196,30 +9196,30 @@ } }, "@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" }, "@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "requires": { "@types/d3-time": "*" } }, "@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "requires": { "@types/d3-path": "*" } }, "@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" }, "@types/d3-timer": { "version": "3.0.2", @@ -20395,9 +20395,9 @@ "dev": true }, "fast-equals": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", - "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==" + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==" }, "fast-glob": { "version": "3.3.2", @@ -29829,9 +29829,9 @@ } }, "react-smooth": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", - "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "requires": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -30032,18 +30032,25 @@ } }, "recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", + "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", "requires": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", - "react-smooth": "^4.0.0", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" + }, + "dependencies": { + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + } } }, "recharts-scale": { diff --git a/package.json b/package.json index 9a79f8958c..80729ab94e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@fortawesome/free-regular-svg-icons": "^5.12.1", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.19", + "@react-leaflet/core": ">=1.0.0 <1.1.0", "@sentry/browser": "^4.6.6", "@tinymce/tinymce-react": "^3.14.0", "axios": "^0.21.2", @@ -30,6 +31,7 @@ "html-to-pdfmake": "^2.0.6", "joi": "^14.0.6", "jwt-decode": "^2.2.0", + "leaflet": "1.7.1", "lodash": "^4.17.21", "lz-string": "^1.5.0", "moment": "^2.29.2", @@ -50,8 +52,6 @@ "react-icons": "^4.3.1", "react-input-range": "^1.3.0", "react-leaflet": ">=3.1.0 <3.2.0", - "@react-leaflet/core": ">=1.0.0 <1.1.0", - "leaflet": "1.7.1", "react-leaflet-cluster": "^1.0.4", "react-multi-select-component": "^4.0.2", "react-phone-input-2": "^2.14.0", @@ -69,7 +69,7 @@ "reactjs-popup": "^2.0.5", "reactstrap": "^8.4.1", "read-excel-file": "^5.5.3", - "recharts": "^2.12.7", + "recharts": "^2.15.2", "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-concatenate-reducers": "^1.0.0", diff --git a/src/components/ApplicantsAgeChart.jsx b/src/components/ApplicantsAgeChart.jsx new file mode 100644 index 0000000000..55e9838aa7 --- /dev/null +++ b/src/components/ApplicantsAgeChart.jsx @@ -0,0 +1,42 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + LabelList, +} from 'recharts'; + +const data = [ + { ageGroup: '18 - 21', applicants: 25 }, + { ageGroup: '21 - 24', applicants: 60 }, + { ageGroup: '24 - 27', applicants: 45 }, + { ageGroup: '27 - 30', applicants: 7 }, + { ageGroup: '30 - 33', applicants: 10 }, +]; + +function ApplicantsChartPage() { + return ( +Loading...
: