From 4533d178ada81bd4eaeec51611c9edc2031af518 Mon Sep 17 00:00:00 2001 From: ShivaniYadav07 Date: Sat, 23 Nov 2024 22:33:07 +0530 Subject: [PATCH 1/2] refactor chap&topnumber --- src/actions/chapterAction.js | 3 +- src/actions/topicAction.js | 7 +- src/pages/CreateChapter.jsx | 298 ++++++++++++++++++++--------------- src/pages/CreateTopic.jsx | 204 ++++++++++++------------ 4 files changed, 274 insertions(+), 238 deletions(-) diff --git a/src/actions/chapterAction.js b/src/actions/chapterAction.js index 7f10d76..529dadc 100644 --- a/src/actions/chapterAction.js +++ b/src/actions/chapterAction.js @@ -29,8 +29,8 @@ export const createChapter = (chapterData) => async (dispatch) => { return response.data; } catch (error) { const errorResponseData = error.response?.data; - const errorMessage = errorResponseData?.message || error.message; + dispatch({ type: CREATE_CHAPTER_FAIL, payload: errorMessage, @@ -41,6 +41,7 @@ export const createChapter = (chapterData) => async (dispatch) => { }; + export const getChapters = (subjectName, standard) => async (dispatch) => { try { dispatch({ type: GET_CHAPTERS_REQUEST }); diff --git a/src/actions/topicAction.js b/src/actions/topicAction.js index f0314c3..07d9dc5 100644 --- a/src/actions/topicAction.js +++ b/src/actions/topicAction.js @@ -28,19 +28,20 @@ export const createTopic = (topicData) => async (dispatch) => { payload: data, }); - return data; + return data; } catch (error) { - const errorMessage = error.response?.data?.message || error.message; + const errorMessage = error.response?.data?.message || error.message || "An error occurred"; dispatch({ type: CREATE_TOPIC_FAIL, payload: errorMessage, }); - return Promise.reject(errorMessage); + return Promise.reject({ message: errorMessage, status: error.response?.status }); } }; + export const getTopics = (subjectName, standard, chapterId) => async (dispatch) => { try { dispatch({ type: GET_TOPICS_REQUEST }); diff --git a/src/pages/CreateChapter.jsx b/src/pages/CreateChapter.jsx index eee1b09..353d6fb 100644 --- a/src/pages/CreateChapter.jsx +++ b/src/pages/CreateChapter.jsx @@ -7,144 +7,186 @@ import { createChapter } from "../actions/chapterAction"; import { getSubjects } from "../actions/subjectAction"; const CreateChapter = () => { - const dispatch = useDispatch(); - const { isLoading } = useSelector((state) => state.chapter); - const { subjectList } = useSelector((state) => state.getSubject); + const dispatch = useDispatch(); + const { isLoading } = useSelector((state) => state.chapter); + const { subjectList } = useSelector((state) => state.getSubject); - const [standard, setStandard] = useState(""); - const [subject, setSubject] = useState(""); - const [chapters, setChapters] = useState([{ name: "" }]); - const [isSubmitting, setIsSubmitting] = useState(false); + const [standard, setStandard] = useState(""); + const [subject, setSubject] = useState(""); + const [chapters, setChapters] = useState([{ name: "", chapterNumber: "" }]); + const [isSubmitting, setIsSubmitting] = useState(false); - useEffect(() => { - if (standard) { - dispatch(getSubjects(standard)); - } - }, [standard, dispatch]); - - const handleFormSubmit = async (event) => { - event.preventDefault(); - - if (isSubmitting) return; - - setIsSubmitting(true); - - const formattedData = { - subject: { - name: subject, - chapters, - standard: standard, - }, - }; - - try { - const result = await dispatch(createChapter(formattedData)); - - if (result.success) { - toast.success('Chapter added successfully!'); - setChapters([{ name: '' }]); - } else { - const errorMessage = result?.message || 'Failed to add chapter.'; - toast.error(errorMessage); - } - } catch (error) { - toast.error(error); - } finally { - setIsSubmitting(false); - } + useEffect(() => { + if (standard) { + dispatch(getSubjects(standard.trim())); + } + }, [standard, dispatch]); + + const handleFormSubmit = async (event) => { + event.preventDefault(); + + if (isSubmitting) return; + + // Validate chapters array + if (!chapters.length) { + toast.error("Please add at least one chapter."); + return; + } + + const seenChapters = new Set(); + for (const chapter of chapters) { + const uniqueKey = `${chapter.name.trim().toLowerCase()}-${chapter.chapterNumber}`; + if (seenChapters.has(uniqueKey)) { + toast.error("Chapter names and chapter numbers must be unique for the same subject and standard."); + return; + } + seenChapters.add(uniqueKey); + } + + // Validate standard and subject + if (!standard || !standard.trim()) { + toast.error("Please select a standard."); + return; + } + + if (!subject || !subject.trim()) { + toast.error("Please select a subject."); + return; + } + + setIsSubmitting(true); + + const formattedData = { + subject: { + name: subject.trim(), + standard: standard.trim(), + chapters: chapters.map((chapter) => ({ + name: chapter.name.trim(), + chapterNumber: parseInt(String(chapter.chapterNumber).trim(), 10), + topics: chapter.topics || [], + })), + }, }; - - + + try { + const result = await dispatch(createChapter(formattedData)); + if (result.success) { + toast.success("Chapter added successfully!"); + setChapters([{ name: "", chapterNumber: "", topics: [] }]); + } else { + toast.error(result.message || "Failed to add chapter."); + } + } catch (error) { + console.error("Error adding chapter:", error); + toast.error(error.message || "An error occurred while adding the chapter."); + } finally { + setIsSubmitting(false); + } + }; + + + + const handleChapterChange = (index, field, value) => { + const updatedChapters = [...chapters]; + updatedChapters[index][field] = field === "chapterNumber" ? Number(value) : value.trim(); + setChapters(updatedChapters); + }; + + const handleAddChapter = () => { + const lastChapter = chapters[chapters.length - 1]; + if (lastChapter.name.trim() && lastChapter.chapterNumber !== "") { + setChapters([...chapters, { name: "", chapterNumber: "" }]); + } else { + toast.error("Please complete the current chapter details before adding a new one."); + } + }; + - const handleChapterChange = (index, value) => { - const updatedChapters = [...chapters]; - updatedChapters[index].name = value; - setChapters(updatedChapters); - }; + return ( +
+

Create Chapter

+
+
+ setSubject(value)} + options={subjectList?.map((name) => ({ value: name, label: name }))} + /> + +
- return ( -
-

Create Chapter

- -
- + handleChapterChange(index, "chapterNumber", e.target.value) + } + min="0" + required + /> -
- + handleChapterChange(index, "name", e.target.value) + } + required + /> +
+
+))} -
- {chapters.map((chapter, index) => ( -
- handleChapterChange(index, e.target.value)} - /> - -
- ))} -
+ -
- Add more chapters -
+
+ Add more chapters +
- - -
- ); + + +
+ ); }; export default CreateChapter; diff --git a/src/pages/CreateTopic.jsx b/src/pages/CreateTopic.jsx index 64e112d..955a664 100644 --- a/src/pages/CreateTopic.jsx +++ b/src/pages/CreateTopic.jsx @@ -11,10 +11,9 @@ const CreateTopic = () => { const [standard, setStandard] = useState(""); const [subject, setSubject] = useState(""); const [chapter, setChapter] = useState(""); - const [topics, setTopics] = useState([{ name: "", subtopics: [] }]); + const [topics, setTopics] = useState([{ name: "", topicNumber: "", subtopics: [] }]); const [isSubmitting, setIsSubmitting] = useState(false); - const [chapterId, setChapterId] = useState(""); // Store selected chapter ID - + const [chapterId, setChapterId] = useState(""); const dispatch = useDispatch(); const { isLoading } = useSelector((state) => state.topic); @@ -23,83 +22,69 @@ const CreateTopic = () => { useEffect(() => { if (standard) { - dispatch(getSubjects(standard)); // Fetch subjects based on selected standard + dispatch(getSubjects(standard)); } if (standard && subject) { - dispatch(getChapters(subject, standard)); // Fetch chapters based on selected subject and standard + dispatch(getChapters(subject, standard)); } }, [standard, subject, dispatch]); const handleFormSubmit = async (event) => { event.preventDefault(); - - if (isSubmitting) return; // Prevent multiple submissions + + if (isSubmitting) return; setIsSubmitting(true); - - // Validate the data (ensure chapterId is present) + if (!chapterId) { toast.error("Please select a valid chapter."); setIsSubmitting(false); return; } - - // Format topics before submitting - const formattedTopics = topics - .map((topic) => { - if (typeof topic.name !== 'string' || topic.name.trim() === '') { - return null; // Invalid topic names - } - return { - name: topic.name.trim(), - subtopics: Array.isArray(topic.subtopics) ? topic.subtopics : [], // Ensure subtopics are an array - }; - }) - .filter(topic => topic !== null); // Filter out invalid topics - - if (formattedTopics.length === 0) { - toast.error("Please provide at least one valid topic."); + + const topicNumbers = topics.map((topic, index) => topic.topicNumber || index + 1); + const duplicateNumbers = topicNumbers.filter((num, index, self) => self.indexOf(num) !== index); + + if (duplicateNumbers.length > 0) { + toast.error(`Duplicate topic numbers found: ${duplicateNumbers.join(", ")}`); setIsSubmitting(false); return; } - - // Prepare data to submit including chapterId - const formattedData = { - standard, - subjectName: subject, - chapterName: chapter, - chapterId, // Add the chapterId for backend validation - topics: formattedTopics, - }; - + + const formattedTopics = topics.map((topic, index) => ({ + ...topic, + topicNumber: topic.topicNumber || index + 1, + })); + + const data = { standard, subjectName: subject, chapterName: chapter, chapterId, topics: formattedTopics }; + try { - const result = await dispatch(createTopic(formattedData)); // Dispatch the create topic action - - if (result && result.success) { + const result = await dispatch(createTopic(data)); + + if (result.success) { toast.success("Topics added successfully!"); - setTopics([{ name: "", subtopics: [] }]); // Reset topics - } else { - const errorMessage = result?.message || "Failed to add topics. Please try again."; - toast.error(errorMessage); + setTopics([{ name: "", subtopics: [] }]); } } catch (error) { - toast.error("An error occurred. Please try again."); + if (error?.message) { + toast.error(error.message); + } else { + toast.error("An unexpected error occurred."); + } } finally { setIsSubmitting(false); } + }; - - - // Handle topic name change - const handleTopicChange = (index, newName) => { + + const handleTopicChange = (index, key, value) => { const updatedTopics = [...topics]; - updatedTopics[index].name = newName; - setTopics(updatedTopics); // Update topics array + updatedTopics[index][key] = value; + setTopics(updatedTopics); }; - // Add new topic input field const handleAddTopic = () => { - setTopics([...topics, { name: "", subtopics: [] }]); // Add a new topic to the list + setTopics([...topics, { name: "", topicNumber: "", subtopics: [] }]); }; return ( @@ -111,7 +96,6 @@ const CreateTopic = () => { { value={subject} options={subjectList?.map((name) => ({ value: name, label: name }))} /> - + - {/* Chapter Selection */}
- + option.label.toLowerCase().includes(input.toLowerCase()) + } + onChange={(value, option) => { + setChapter(option.label); + setChapterId(option.key); + }} + value={chapter} + options={chapterList?.map((chapter) => ({ + value: chapter.name, + label: chapter.name, + key: chapter._id, + }))} + /> + +
+
{topics.map((topic, index) => ( -
- handleTopicChange(index, e.target.value)} - required - /> - +
+
+ + setTopics((prevTopics) => + prevTopics.map((t, i) => + i === index ? { ...t, topicNumber: Math.max(0, Number(e.target.value)) } : t + ) + ) + } + min="0" + required + /> + + + setTopics((prevTopics) => + prevTopics.map((t, i) => (i === index ? { ...t, name: e.target.value } : t)) + ) + } + required + /> +
))} + +
- {/* Add More Topics Button */}
{ Add more topics
- {/* Submit Button */} From ec7cee91e528c75fdb7869e0e2341c8a8b11a866 Mon Sep 17 00:00:00 2001 From: ShivaniYadav07 Date: Mon, 25 Nov 2024 12:07:48 +0530 Subject: [PATCH 2/2] added numbers to update topic and update chapters --- src/pages/EditDetails.jsx | 352 ++++++++++++++++++++++---------------- 1 file changed, 203 insertions(+), 149 deletions(-) diff --git a/src/pages/EditDetails.jsx b/src/pages/EditDetails.jsx index a5b5dd0..ba44e15 100644 --- a/src/pages/EditDetails.jsx +++ b/src/pages/EditDetails.jsx @@ -29,6 +29,8 @@ const EditDetails = () => { const [isChapterModalOpen, setIsChapterModalOpen] = useState(false); const [newChapterName, setNewChapterName] = useState(""); const [currentChapterId, setCurrentChapterId] = useState(null); + const [newChapterNumber, setNewChapterNumber] = useState(null); + const [newTopicNumber, setNewTopicNumber] = useState(null); useEffect(() => { const fetchData = async () => { try { @@ -72,14 +74,14 @@ const EditDetails = () => { try { setLoadingTopics((prev) => ({ ...prev, [chapterId]: true })); - + const response = await axios.get( - `${server}/api/get/topic?subjectName=${subjectName}&standard=${standard}&chapterId=${chapterId}` + `${server}/api/get/topic?subjectName=${subjectName}&standard=${standard}&chapterId=${chapterId}` ); - + setTopicsByChapter((prev) => ({ ...prev, - [chapterId]: response.data.topics || [], + [chapterId]: response.data.topics || [], })); } catch (error) { console.error("Error fetching topics:", error.message); @@ -87,12 +89,12 @@ const EditDetails = () => { setLoadingTopics((prev) => ({ ...prev, [chapterId]: false })); } }; - + const handleChapterClick = (subjectName, chapterId) => { if (!topicsByChapter[chapterId]) { fetchTopics(subjectName, chapterId); } - + setVisibleChapters((prev) => { const updatedChapters = { ...prev, @@ -102,7 +104,7 @@ const EditDetails = () => { return updatedChapters; }); }; - + const fetchSubTopics = async ( subjectName, @@ -112,11 +114,11 @@ const EditDetails = () => { ) => { try { setLoadingSubtopics((prev) => ({ ...prev, [topicId]: true })); - + const response = await axios.get( `${server}/api/get/subtopic?subjectName=${subjectName}&standard=${standard}&chapterId=${chapterId}&topicId=${topicId}` // Use chapterId and topicId ); - + setSubTopicByTopics((prev) => ({ ...prev, [topicId]: response.data.subtopics || [], // Store subtopics using topicId @@ -127,63 +129,74 @@ const EditDetails = () => { setLoadingSubtopics((prev) => ({ ...prev, [topicId]: false })); } }; - + const handleTopicClick = (subjectName, chapterId, topicId, topicName) => { if (!subTopicByTopics[topicId]) { fetchSubTopics(subjectName, chapterId, topicId, topicName); } - + setVisibleTopics((prev) => ({ ...prev, [topicId]: !prev[topicId], })); }; - + const handleUpdateTopic = async () => { try { - // Ensure both topicId and newTopicName are provided - if (!currentTopicId || !newTopicName) { - toast.error("Topic ID and new name must be provided"); + // Validate the input fields + if (!currentTopicId || !newTopicName || newTopicNumber === undefined) { + toast.error("Topic ID, new name, and topic number must be provided"); return; } - // API request to update the topic name + // Make the API request to update the topic const response = await axios.put( `${server}/api/update/topic/${currentTopicId}`, - { name: newTopicName } + { name: newTopicName, topicNumber: newTopicNumber } ); if (response.data.success) { - toast.success("Topic name updated successfully"); + toast.success("Topic name and number updated successfully"); + // Update the local state to reflect the changes setTopicsByChapter((prev) => { const updatedChapters = { ...prev }; Object.keys(updatedChapters).forEach((chapter) => { updatedChapters[chapter] = updatedChapters[chapter].map((topic) => topic._id === currentTopicId - ? { ...topic, name: newTopicName } + ? { ...topic, name: newTopicName, topicNumber: newTopicNumber } : topic ); }); return updatedChapters; }); } else { - toast.error(response.data.message || "Failed to update topic name"); + // Display error message from backend + toast.error(response.data.message || "Failed to update topic"); } } catch (error) { console.error("Error in handleUpdateTopic:", error); - toast.error("An unexpected error occurred. Please try again later."); + + // Display backend error message or a fallback error message + if (error.response && error.response.data && error.response.data.message) { + toast.error(error.response.data.message); + } else { + toast.error("An unexpected error occurred. Please try again later."); + } } finally { - setIsModalOpen(false); // Close the modal after the update + // Close the modal after the update + setIsModalOpen(false); } }; + const openEditModal = (topicId, topicName) => { setCurrentTopicId(topicId); setNewTopicName(topicName); setIsModalOpen(true); + setNewTopicNumber(topicNumber) }; const handleDeleteTopic = (topicId) => { Modal.confirm({ @@ -256,7 +269,7 @@ const EditDetails = () => { console.error("Error in handleUpdateSubtopic:", error); toast.error("An unexpected error occurred. Please try again later."); } finally { - setIsSubtopicModalOpen(false); + setIsSubtopicModalOpen(false); } }; @@ -308,43 +321,56 @@ const EditDetails = () => { const handleUpdateChapter = async () => { try { - if (!currentChapterId || !newChapterName) { - toast.error("Chapter ID and new name must be provided"); + if (!currentChapterId || !newChapterName || newChapterNumber === undefined) { + toast.error("Chapter ID, new name, and chapter number must be provided."); return; } const response = await axios.put( `${server}/api/update/chapter/${currentChapterId}`, - { name: newChapterName } + { + name: newChapterName, + chapterNumber: newChapterNumber + } ); if (response.data.success) { - toast.success("Topic name updated successfully"); + toast.success("Chapter name and number updated successfully!"); + setChaptersBySubject((prev) => { const updatedChapters = { ...prev }; - Object.keys(updatedChapters).forEach((chapter) => { - updatedChapters[chapter] = updatedChapters[chapter].map((chapter) => + + Object.keys(updatedChapters).forEach((subject) => { + updatedChapters[subject] = updatedChapters[subject].map((chapter) => chapter._id === currentChapterId - ? { ...chapter, name: newChapterName } + ? { ...chapter, name: newChapterName, chapterNumber: newChapterNumber } : chapter ); }); + return updatedChapters; }); } else { - toast.error(response.data.message || "Failed to update chapter name"); + toast.error(response.data.message || "Failed to update chapter."); } } catch (error) { console.error("Error in handleUpdateChapter:", error); - toast.error("An unexpected error occurred. Please try again later."); - } finally { + + const errorMessage = error.response?.data?.message || "An unexpected error occurred. Please try again later."; + + toast.error(errorMessage); + } + finally { setIsChapterModalOpen(false); } }; + + const openEditChapterModal = (chapterId, chapterName) => { setCurrentChapterId(chapterId); setNewChapterName(chapterName); setIsChapterModalOpen(true); + setNewChapterNumber(chapterNumber); }; const handleDeleteChapter = (chapterId) => { @@ -363,7 +389,7 @@ const EditDetails = () => { if (response.data.success) { toast.success("Subtopic deleted successfully"); - + setSubTopicByTopics((prev) => { const updatedTopics = { ...prev }; Object.keys(updatedTopics).forEach((chapter) => { @@ -428,121 +454,121 @@ const EditDetails = () => { />
{Array.isArray(chaptersBySubject[subject]) && - chaptersBySubject[subject].length > 0 ? ( -
    - {chaptersBySubject[subject].map((chapter) => ( -
  • -
    - {chapter.name} -
    - - handleChapterClick(subject, chapter._id) - } - > - {visibleChapters[chapter._id] ? ( - - ) : ( - - )} - - - openEditChapterModal(chapter._id, chapter.name) - } - /> - handleDeleteChapter(chapter._id)} - /> -
    -
    - {visibleChapters[chapter._id] && ( // Use chapter._id here as well -
    - {loadingTopics[chapter._id] ? ( // Loading state using chapter._id - - ) : ( - <> - {Array.isArray(topicsByChapter[chapter._id]) && topicsByChapter[chapter._id].length > 0 ? ( - -) : ( -
    No chapters available
    -)} +
    + {visibleChapters[chapter._id] && ( +
    + {loadingTopics[chapter._id] ? ( + + ) : ( + <> + {Array.isArray(topicsByChapter[chapter._id]) && topicsByChapter[chapter._id].length > 0 ? ( +
      + {topicsByChapter[chapter._id].map((topic) => ( +
    • +
      + {topic.name} +
      + + handleTopicClick( + subject, + chapter._id, + topic._id, + topic.name + ) + } + > + {visibleTopics[topic._id] ? : } + + openEditModal(topic._id, topic.name)} + /> + handleDeleteTopic(topic._id)} + /> +
      +
      + {visibleTopics[topic._id] && ( +
      + {loadingSubtopics[topic._id] ? ( + + ) : ( + <> + {subTopicByTopics[topic._id] && subTopicByTopics[topic._id].length > 0 ? ( +
        + {subTopicByTopics[topic._id].map((subtopic) => ( +
      • + {subtopic.name} +
        + openEditSubtopicModal(subtopic._id, subtopic.name)} + /> + handleDeleteSubtopic(subtopic._id)} + /> +
        +
      • + ))} +
      + ) : ( +
      No subtopics available
      + )} + + )} +
      + )} +
    • + ))} +
    + ) : ( +
    No topics available
    + )} + + + )} +
    + )} +
  • + ))} +
+ ) : ( +
No chapters available
+ )}
)) ) : ( @@ -550,7 +576,7 @@ const EditDetails = () => { )} setIsModalOpen(false)} @@ -559,8 +585,17 @@ const EditDetails = () => { value={newTopicName} onChange={(e) => setNewTopicName(e.target.value)} placeholder="Enter new topic name" + className="mb-4" + /> + + setNewTopicNumber(Number(e.target.value))} + placeholder="Enter new topic number" /> + { /> setIsChapterModalOpen(false)} > - setNewChapterName(e.target.value)} - placeholder="Enter new Chapter name" - /> +
+ + setNewChapterName(e.target.value)} + placeholder="Enter new Chapter name" + className="w-full" + /> +
+
+ + setNewChapterNumber(Number(e.target.value))} + placeholder="Enter new Chapter number" + className="w-full" + /> +
+ ); };