diff --git a/src/components/Dashboard/Notetaker/Category/CategorySidebar.jsx b/src/components/Dashboard/Notetaker/Category/CategorySidebar.jsx index 79522040..09d99979 100644 --- a/src/components/Dashboard/Notetaker/Category/CategorySidebar.jsx +++ b/src/components/Dashboard/Notetaker/Category/CategorySidebar.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { MdCreateNewFolder, MdOutlineCancel } from "react-icons/md"; import { LuFolderCog } from "react-icons/lu"; -import { useDispatch, useSelector } from "react-redux"; import { toast } from "react-toastify"; import { TbEditCircle } from "react-icons/tb"; import { AiTwotoneDelete } from "react-icons/ai"; @@ -16,55 +15,49 @@ import { import CategoryList from "./CategoryList"; import LoadingSpinner from "../../../Other/MixComponents/Spinner/LoadingSpinner"; import ModifyCategory from "./ModifyCategory"; -import { createCategory, editCategory, removeCategory } from "../../../../features/notes/category/categorySlice"; +import { updateCategory, deleteCategory, createCategory } from "../../../../features/notes/category/categorySlice"; +import { useDispatch } from "react-redux"; -const CategorySidebar = ({ pickedCategory, onPick, onUnpickNote, pickedNote }) => { +const CategorySidebar = ({ + pickedCategory, + onPick, + onUnpickNote, + requiredCategories, + setCopyCategoryOptionMode, + categories, + isCategoryLoading, + setPickedCategory, +}) => { const dispatch = useDispatch(); - const { categories } = useSelector((state) => state.categories); - const [nonRequiredCategories, setNonRequiredCategories] = useState([]); - const [requiredCategories, setRequiredCategories] = useState([]); const [modalOpenMode, setModalOpenMode] = useState(""); const [categoryOptionMode, setCategoryOptionMode] = useState(false); - const isCategoryLoading = false; useEffect(() => { - const findNonRequiredCategories = categories.filter((item) => !item.required); - if (!findNonRequiredCategories.length && categoryOptionMode) handleCloseOptionsMode(); - setNonRequiredCategories(() => { - return findNonRequiredCategories; - }); - setRequiredCategories(categories.filter((item) => item.required)); + if (!categories.length && categoryOptionMode) handleCloseOptionsMode(); }, [categories]); useEffect(() => { - if (Object.keys(pickedNote).length !== 0) setCategoryOptionMode(false); - }, [pickedNote]); - + setCopyCategoryOptionMode(categoryOptionMode); + }, [categoryOptionMode]); const handleCreateCategory = () => { setModalOpenMode("Create"); onUnpickNote(); }; - const handleEditCategory = () => { - if (Object.keys(pickedCategory).length === 0) { - toast.error("Need to pick a category to edit it"); - return; - } - setModalOpenMode("Edit"); - }; + const handleCloseModal = () => { setModalOpenMode(""); }; const handleSave = (categoryName) => { if (modalOpenMode === "Create") { - dispatch(createCategory(categoryName)); + dispatch(createCategory({ name: categoryName })); } else { - dispatch(editCategory({ ...pickedCategory, name: categoryName })); + dispatch(updateCategory({ id: pickedCategory._id, categoryData: { name: categoryName } })); onPick({}); } handleCloseModal(); }; const handleOpenOptionsMode = () => { - if (nonRequiredCategories.length === 0) { + if (categories.length === 0) { toast.error( `Options Mode Is Only For Unique Categories Modification. First Add At Least One Unique Category And Click Again`, @@ -73,19 +66,29 @@ const CategorySidebar = ({ pickedCategory, onPick, onUnpickNote, pickedNote }) = } setCategoryOptionMode(true); onUnpickNote(); - onPick(nonRequiredCategories[0]); + onPick(categories[0]); }; const handleCloseOptionsMode = () => { setCategoryOptionMode(false); handleCloseModal(); onPick(requiredCategories[0]); }; + const handleEditCategory = () => { + if (Object.keys(pickedCategory).length === 0) { + toast.error("Need to pick a category to edit it"); + return; + } + setModalOpenMode("Edit"); + }; + const handleDeleteCategory = () => { if (Object.keys(pickedCategory).length === 0) { toast.error("Need to pick a category to delete it"); return; } - dispatch(removeCategory(pickedCategory._id)); + dispatch(deleteCategory(pickedCategory._id)).then(() => { + setPickedCategory({}); + }); }; return ( @@ -143,7 +146,7 @@ const CategorySidebar = ({ pickedCategory, onPick, onUnpickNote, pickedNote }) = )} - {nonRequiredCategories} + {categories} )} diff --git a/src/components/Dashboard/Notetaker/NoteApp.css b/src/components/Dashboard/Notetaker/NoteApp.css index 7cc110ea..ef36274f 100644 --- a/src/components/Dashboard/Notetaker/NoteApp.css +++ b/src/components/Dashboard/Notetaker/NoteApp.css @@ -33,3 +33,13 @@ opacity: 0.7; cursor: pointer; } +.slide-in { + transform: translateX(0); + transition: 0.5s; + z-index: 0; +} +.slide-out { + transform: translateX(-100vw); + transition: 2s; + z-index: -100; +} diff --git a/src/components/Dashboard/Notetaker/NoteApp.jsx b/src/components/Dashboard/Notetaker/NoteApp.jsx index 5a054189..038ccb83 100644 --- a/src/components/Dashboard/Notetaker/NoteApp.jsx +++ b/src/components/Dashboard/Notetaker/NoteApp.jsx @@ -1,33 +1,48 @@ import React, { useEffect, useState } from "react"; -import { MdNoteAdd } from "react-icons/md"; import { useDispatch, useSelector } from "react-redux"; -import { - NotesContainer, - NotesSidebarContainer, - NotesSidebarHeader, - NotesSidebarHeaderTitle, - SearchContainer, -} from "./NoteElements"; -import SearchInputBox from "../../Common/SearchInputBox"; +import { NotesContainer } from "./NoteElements"; import "./NoteApp.css"; -import NoteList from "./NoteList"; import NoteDescription from "./NoteDescription"; import { getNotes, noteReset, pinNote } from "../../../features/notes/notesSlice"; -import LoadingSpinner from "../../Other/MixComponents/Spinner/LoadingSpinner"; import { CategorySidebar } from "./Category"; +import NoteSidebar from "./NoteSidebar"; +import { toast } from "react-toastify"; +import { categoryReset, getCategories } from "../../../features/notes/category/categorySlice"; + +const requiredCategories = [ + { + name: "Pinned Notes", + type: "pinned", + }, + { + name: "Other Notes", + type: "other", + }, +]; const NoteApp = () => { const dispatch = useDispatch(); const { notes, isNoteLoading, isNoteError, noteMessage } = useSelector((state) => state.notes); - const { categories } = useSelector((state) => state.categories); - const [searchTerm, setSearchTerm] = useState(""); - const [filteredNotes, setFilteredNotes] = useState([]); + const { categories, isCategoryLoading, isCategoryError, categoryMessage } = useSelector( + (state) => state.categories, + ); const [pickedCategory, setPickedCategory] = useState({}); const [pickedNote, setPickedNote] = useState({}); + const [categoryOptionMode, setCategoryOptionMode] = useState(false); const [needToAdd, setNeedToAdd] = useState(false); useEffect(() => { + if (isCategoryError) { + toast.error(categoryMessage); + console.log(categoryMessage); + } + dispatch(getCategories()); + return () => dispatch(categoryReset()); + }, [dispatch, isCategoryError, categoryMessage]); + + useEffect(() => { + if (categoryOptionMode || isCategoryLoading) return; if (isNoteError) { console.log(noteMessage); } @@ -35,63 +50,29 @@ const NoteApp = () => { if (payload.length > 0) { let pickedNote = payload.find((note) => note.pinned); if (pickedNote) { - setPickedCategory(() => { - return categories.find((category) => category.name === "Pinned Notes"); - }); + setPickedCategory(requiredCategories[0]); setPickedNote( pickedNote.title.includes("UntitledNote") ? { ...pickedNote, title: "" } : pickedNote, ); } else { pickedNote = payload.find((note) => !note.pinned); - setPickedCategory(() => { - return categories.find((category) => category.name === "Other Notes"); - }); + setPickedCategory(requiredCategories[1]); setPickedNote( pickedNote.title.includes("UntitledNote") ? { ...pickedNote, title: "" } : pickedNote, ); } } else { - setPickedCategory(() => { - return categories.find((category) => category.name === "Pinned Notes"); - }); + setPickedCategory(requiredCategories[0]); } }); return () => dispatch(noteReset()); - }, [dispatch, isNoteError, noteMessage]); - - useEffect(() => { - const newFilteredNotes = notes?.filter((note) => { - const searchedNote = - note?.title?.toLowerCase().includes(searchTerm?.toLowerCase()) || - note?.content?.toLowerCase().includes(searchTerm?.toLowerCase()); - if (!searchedNote) return false; - if (Object.keys(pickedCategory).length === 0) return false; - if (pickedCategory.name === "Other Notes") { - return !note.pinned; - } - if (pickedCategory.name === "Pinned Notes") { - return note.pinned; - } - return note.category.toLowerCase() === pickedCategory.name.toLowerCase(); - }); - setFilteredNotes(newFilteredNotes); - setPickedNote({}); - }, [searchTerm, notes, pickedCategory]); + }, [dispatch, isNoteError, noteMessage, categoryOptionMode, isCategoryLoading]); - const handleSearchTermChange = (e) => { - setSearchTerm(e.target.value); - }; - const handlePickNote = (noteId) => { - const pickedNote = notes.find((note) => note._id === noteId); + const handleCloseMDEditorMode = () => { setNeedToAdd(false); - setPickedNote( - pickedNote === -1 - ? {} - : pickedNote.title.includes("UntitledNote") - ? { ...pickedNote, title: "" } - : pickedNote, - ); + setPickedNote({}); }; + const handlePinNote = (noteId) => { const pinnedNote = notes.find((note) => note._id === noteId); const noteData = { @@ -100,14 +81,6 @@ const NoteApp = () => { }; dispatch(pinNote({ id: noteId, noteData })); }; - const handleOpenAddNewNoteMode = () => { - setNeedToAdd(true); - setPickedNote({}); - }; - const handleCloseMDEditorMode = () => { - setNeedToAdd(false); - setPickedNote({}); - }; return ( @@ -115,39 +88,31 @@ const NoteApp = () => { pickedCategory={pickedCategory} onPick={setPickedCategory} onUnpickNote={handleCloseMDEditorMode} + requiredCategories={requiredCategories} + setCopyCategoryOptionMode={setCategoryOptionMode} + categories={categories} + isCategoryLoading={isCategoryLoading} + setPickedCategory={setPickedCategory} + /> + - - - - {(pickedCategory && pickedCategory.name) || "Please, Pick Category"} - - - - - - - {isNoteLoading ? ( - - ) : ( - - {filteredNotes} - - )} - {pickedNote} diff --git a/src/components/Dashboard/Notetaker/NoteDescription.jsx b/src/components/Dashboard/Notetaker/NoteDescription.jsx index 7bd90f9f..2b79950b 100644 --- a/src/components/Dashboard/Notetaker/NoteDescription.jsx +++ b/src/components/Dashboard/Notetaker/NoteDescription.jsx @@ -19,15 +19,41 @@ import { TbEditCircle } from "react-icons/tb"; import { AiTwotoneDelete } from "react-icons/ai"; import { RiMore2Fill } from "react-icons/ri"; -const NoteDescription = ({ children, pickedCategory, onPin, needToAdd, onCloseAddMode, onChangePickedNote }) => { +const NoteDescription = ({ + children, + pickedCategory, + onPin, + needToAdd, + onCloseAddMode, + onChangePickedNote, + requiredCategories, +}) => { const dispatch = useDispatch(); const [showNote, setShowNote] = useState(children || {}); const [needToEdit, setNeedToEdit] = useState(false); + const [categoryName, setCategoryName] = useState(""); + const [isPinnedCategory, setIsPinnedCategory] = useState(false); + useEffect(() => { setShowNote(children); setNeedToEdit(false); }, [children]); + useEffect(() => { + setIsPinnedCategory(() => { + const pinnedCategory = requiredCategories.find((requiredCategory) => requiredCategory.type === "pinned"); + return pinnedCategory.name === pickedCategory.name; + }); + + setCategoryName(() => { + const matchedCategory = requiredCategories.find( + (requiredCategory) => requiredCategory.name === pickedCategory.name, + ); + if (matchedCategory) return "notes"; + return pickedCategory.name; + }); + }, [pickedCategory, requiredCategories]); + const handleDeleteNote = () => { dispatch(deleteNote(children._id)); setShowNote({}); @@ -59,21 +85,20 @@ const NoteDescription = ({ children, pickedCategory, onPin, needToAdd, onCloseAd handleClose(); return; } - if (needToEdit) { dispatch( updateNote({ id: children._id, - category: pickedCategory.name, - pinned: pickedCategory.name === "Pinned Notes", + category: categoryName, + pinned: isPinnedCategory, noteData: newNote, }), ); } else if (needToAdd) { dispatch( createNote({ - category: pickedCategory.name, - pinned: pickedCategory.name === "Pinned Notes", + category: categoryName, + pinned: isPinnedCategory, ...newNote, }), ); @@ -81,7 +106,6 @@ const NoteDescription = ({ children, pickedCategory, onPin, needToAdd, onCloseAd onChangePickedNote(newNote); handleClose(); }; - return ( @@ -116,7 +140,6 @@ const NoteDescription = ({ children, pickedCategory, onPin, needToAdd, onCloseAd )} - {needToAdd || needToEdit ? ( diff --git a/src/components/Dashboard/Notetaker/NoteSidebar.jsx b/src/components/Dashboard/Notetaker/NoteSidebar.jsx new file mode 100644 index 00000000..44d5a734 --- /dev/null +++ b/src/components/Dashboard/Notetaker/NoteSidebar.jsx @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from "react"; +import { MdNoteAdd } from "react-icons/md"; + +import SearchInputBox from "../../Common/SearchInputBox"; +import { NotesSidebarContainer, NotesSidebarHeader, NotesSidebarHeaderTitle, SearchContainer } from "./NoteElements"; +import LoadingSpinner from "../../Other/MixComponents/Spinner/LoadingSpinner"; +import NoteList from "./NoteList"; +import "./NoteApp.css"; + +const NoteSidebar = ({ + categoryOptionMode, + pickedCategory, + pickedNote, + onPickNote, + onNeedToAddNote, + onPinNote, + notes, + isNoteLoading, + isCategoryLoading, +}) => { + const [filteredNotes, setFilteredNotes] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const newFilteredNotes = notes?.filter((note) => { + const searchedNote = + note?.title?.toLowerCase().includes(searchTerm?.toLowerCase()) || + note?.content?.toLowerCase().includes(searchTerm?.toLowerCase()); + if (!searchedNote) return false; + if (Object.keys(pickedCategory).length === 0) return false; + if (pickedCategory.name === "Other Notes") { + return !note.pinned; + } + if (pickedCategory.name === "Pinned Notes") { + return note.pinned; + } + return note.category.toLowerCase() === pickedCategory.name.toLowerCase(); + }); + setFilteredNotes(newFilteredNotes); + onPickNote({}); + }, [searchTerm, notes, pickedCategory]); + + const handleSearchTermChange = (e) => { + setSearchTerm(e.target.value); + }; + const handleOpenAddNewNoteMode = () => { + onNeedToAddNote(true); + onPickNote({}); + }; + + const handlePickNote = (noteId) => { + const pickedNote = notes.find((note) => note._id === noteId); + onNeedToAddNote(false); + onPickNote( + pickedNote === -1 + ? {} + : pickedNote.title.includes("UntitledNote") + ? { ...pickedNote, title: "" } + : pickedNote, + ); + }; + + return ( + + + + {(pickedCategory && pickedCategory.name) || "Please, Pick Category"} + + + + + + + + {isNoteLoading ? ( + + ) : ( + !isCategoryLoading && ( + + {filteredNotes} + + ) + )} + + ); +}; +export default NoteSidebar; diff --git a/src/features/notes/category/categoryService.js b/src/features/notes/category/categoryService.js index e69de29b..731dcf8d 100644 --- a/src/features/notes/category/categoryService.js +++ b/src/features/notes/category/categoryService.js @@ -0,0 +1,63 @@ +import axios from "axios"; +import { getApiUrl } from "../../apiUrl"; + +const API_URL = getApiUrl("api/category/"); + +// Create new category +const createCategory = async (categoryData, token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + const response = await axios.post(API_URL, categoryData, config); + + return response.data; +}; + +// Update category +const updateCategory = async (id, categoryData, token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.put(API_URL + id, categoryData, config); + + return response.data; +}; + +// Get user categories +const getCategories = async (token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.get(API_URL, config); + + return response.data; +}; + +// Delete user category +const deleteCategory = async (categoryId, token) => { + const config = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const response = await axios.delete(API_URL + categoryId, config); + return response.data; +}; + +const categoryService = { + createCategory, + updateCategory, + getCategories, + deleteCategory, +}; + +export default categoryService; diff --git a/src/features/notes/category/categorySlice.js b/src/features/notes/category/categorySlice.js index c10f70c6..c0614408 100644 --- a/src/features/notes/category/categorySlice.js +++ b/src/features/notes/category/categorySlice.js @@ -1,66 +1,139 @@ -import { createSlice, nanoid } from "@reduxjs/toolkit"; -import { toast } from "react-toastify"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import categoryService from "./categoryService"; const initialState = { - categories: [ - { - name: "Pinned Notes", - required: true, - _id: nanoid(), - }, - { - name: "Other Notes", - required: true, - _id: nanoid(), - }, - ], + categories: [], + isCategoryError: false, + isCategorySuccess: false, + isCategoryLoading: false, + categoryMessage: "", }; +// Create new category +export const createCategory = createAsyncThunk("category/create", async (categoryData, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await categoryService.createCategory(categoryData, token); + } catch (error) { + const message = + (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); + return thunkAPI.rejectWithValue(message); + } +}); + +// Update existing category +export const updateCategory = createAsyncThunk("category/update", async ({ id, categoryData }, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await categoryService.updateCategory(id, categoryData, token); + } catch (error) { + const message = + (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); + return thunkAPI.rejectWithValue(message); + } +}); + +// Get user categories +export const getCategories = createAsyncThunk("category/getCategories", async (_, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await categoryService.getCategories(token); + } catch (error) { + const message = + (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); + return thunkAPI.rejectWithValue(message); + } +}); + +// Delete user category +export const deleteCategory = createAsyncThunk("category/delete", async (id, thunkAPI) => { + try { + const token = thunkAPI.getState().auth.user.token; + return await categoryService.deleteCategory(id, token); + } catch (error) { + const message = + (error.response && error.response.data && error.response.data.message) || error.message || error.toString(); + return thunkAPI.rejectWithValue(message); + } +}); + export const categorySlice = createSlice({ name: "categories", initialState, reducers: { - createCategory: { - reducer: (state, action) => { - console.log(action.payload); - const categoryNameExists = state.categories.find((category) => category.name === action.payload.name); - if (categoryNameExists) { - toast.error("This Category Name Already Exists, Change the Name And Try Again."); - return; - } + categoryReset: () => initialState, + }, + extraReducers: (builder) => { + builder + .addCase(createCategory.pending, (state) => { + state.isCategoryLoading = true; + }) + .addCase(createCategory.fulfilled, (state, action) => { + state.isCategorySuccess = true; + state.isCategoryLoading = false; + state.isCategoryError = false; state.categories = [...state.categories, action.payload]; - }, - prepare: (categoryName) => { - const id = nanoid(); - return { payload: { name: categoryName, _id: id } }; - }, - }, - removeCategory: (state, action) => { - if (action.payload.required) { - toast.error("It is not possible to remove required category."); - return; - } - state.categories = state.categories.filter((category) => category._id !== action.payload); - }, - editCategory: (state, action) => { - const editedCategory = action.payload; - console.log(editedCategory); - if (editedCategory.required) { - toast.error("It is not possible to edit required category."); - return; - } - const categoryNameExists = state.categories.find((category) => category.name === action.payload.name); - const prevCategory = state.categories.find((category) => category._id === action.payload._id); - if (categoryNameExists && prevCategory.name !== editedCategory.name) { - toast.error("This Category Name Already Exists, Change the Name And Try Again."); - return; - } - const indexOfEditedCategory = state.categories.findIndex((category) => category._id === editedCategory._id); - if (indexOfEditedCategory < 0) state.categories = [...state.categories, editedCategory]; - state.categories[indexOfEditedCategory] = { ...state.categories[indexOfEditedCategory], ...editedCategory }; - }, + }) + .addCase(createCategory.rejected, (state, action) => { + state.isCategorySuccess = false; + state.isCategoryLoading = false; + state.isCategoryError = true; + state.categoryMessage = action.payload; + }) + .addCase(updateCategory.pending, (state) => { + state.isCategoryLoading = true; + }) + .addCase(updateCategory.fulfilled, (state, action) => { + state.isCategorySuccess = true; + state.isCategoryLoading = false; + state.isCategoryError = false; + state.categories = state.categories.map((category) => + category._id === action.payload._id + ? { + ...category, + ...action.payload, + } + : category, + ); + }) + .addCase(updateCategory.rejected, (state, action) => { + state.isCategorySuccess = false; + state.isCategoryLoading = false; + state.isCategoryError = true; + state.categoryMessage = action.payload; + }) + .addCase(getCategories.pending, (state) => { + state.isCategoryLoading = true; + }) + .addCase(getCategories.fulfilled, (state, action) => { + state.isCategorySuccess = true; + state.isCategoryLoading = false; + state.isCategoryError = false; + state.categories = action.payload; + }) + .addCase(getCategories.rejected, (state, action) => { + state.isCategorySuccess = false; + state.isCategoryLoading = false; + state.isCategoryError = true; + state.categoryMessage = action.payload; + }) + .addCase(deleteCategory.pending, (state) => { + state.isCategoryLoading = true; + }) + .addCase(deleteCategory.fulfilled, (state, action) => { + state.isCategorySuccess = true; + state.isCategoryLoading = false; + state.isCategoryError = false; + state.categories = state.categories.filter((category) => category._id !== action.payload.id); + }) + .addCase(deleteCategory.rejected, (state, action) => { + state.isCategorySuccess = false; + state.isCategoryLoading = false; + state.isCategoryError = true; + state.categoryMessage = action.payload; + }); }, }); -export const { createCategory, removeCategory, editCategory } = categorySlice.actions; +export const { categoryReset } = categorySlice.actions; export default categorySlice.reducer;