diff --git a/backend/controllers/categoryController.js b/backend/controllers/categoryController.js new file mode 100644 index 0000000..aef9b2f --- /dev/null +++ b/backend/controllers/categoryController.js @@ -0,0 +1,120 @@ +const asyncHandler = require('express-async-handler') +const Category = require('../models/categoryModel') +const Product = require('../models/productModel') + +// @desc Fetch all categories +// @route GET /api/categories +// @access Public +const getCategories = asyncHandler(async (req, res) => { + const categories = await Category.find({}) + res.json(categories) +}) + +// @desc Fetch single category +// @route GET /api/categories/:id +// @access Public +const getCategoryById = asyncHandler(async (req, res) => { + const category = await Category.findById(req.params.id) + if (category) { + res.json(category) + } else { + res.status(404) + throw new Error('Category not found') + } +}) + +// @desc Delete a category +// @route DELETE /api/categories/:id +// @access Private/Admin +const deleteCategory = asyncHandler(async (req, res) => { + const category = await Category.findById(req.params.id) + + if (category) { + await category.remove() + res.json({ message: 'Category removed' }) + } else { + res.status(404) + throw new Error('Category not found') + } +}) + +// @desc Create a category +// @route POST /api/categories +// @access Private/Admin +const createCategory = asyncHandler(async (req, res) => { + const { + name, + imageSrc, + imageAlt, + } = req.body + const category = new Category({ + user: req.user._id, + name, + imageSrc, + imageAlt, + href:"#" + }) + + try { + const createdCategory = await category.save() + + res.status(201).json(createdCategory) + } catch (error) { + if (error.name === 'ValidationError') { + let errors = '' + Object.keys(error.errors).forEach((key) => { + errors += error.errors[key].message + '.\n' + }) + res.status(500).json(errors) + } + } +}) + +// @desc Update a category +// @route PUT /api/categories/:id +// @access Private/Admin +const updateCategory = asyncHandler(async (req, res) => { + const { + name, + imageSrc, + imageAlt, + } = req.body + console.log(name) + const category = await Category.findById(req.params.id) + + if (category) { + category.name = name + category.imageSrc = imageSrc + category.imageAlt = imageAlt + + const updatedCategory = await category.save() + res.json(updatedCategory) + } else { + res.status(404) + throw new Error('Category not found') + } +}) + +// @desc Fetch products by category +// @route GET /api/categories/:category +// @access Public +const getProductsByCategory = asyncHandler(async (req, res) => { + const categorywiseProducts = await Product.find({ + category: req.params.category, + }) + if (categorywiseProducts) { + res.json(categorywiseProducts) + } else { + res.status(404) + throw new Error('Products not found') + } +}) + +module.exports = { + getCategories, + getCategoryById, + getProductsByCategory, + deleteCategory, + createCategory, + updateCategory, +} diff --git a/backend/routes/categoryRoutes.js b/backend/routes/categoryRoutes.js index e90b951..49c9960 100644 --- a/backend/routes/categoryRoutes.js +++ b/backend/routes/categoryRoutes.js @@ -1,39 +1,35 @@ const express = require('express') -const asyncHandler = require('express-async-handler') const router = express.Router() const Product = require('../models/productModel') -const Category = require('../models/categoryModel') +const { + getCategories, + getCategoryById, + getProductsByCategory, + createCategory, + deleteCategory, + updateCategory, +} = require('../controllers/categoryController') +const { protect, admin } = require('../middleware/authMiddleware') // @desc Fetch all categories // @route GET /api/categories // @access Public +router.route('/').get(getCategories).post(protect, admin, createCategory) -router.get( - '/', - asyncHandler(async (req, res) => { - const categories = await Category.find({}) - res.json(categories) - }), -) +// @desc Fetch category by id +// @route GET /api/categories/category/:id +// @route PUT /api/categories/category/:id +// @route DELETE /api/categories/category/:id +// @access Private/Admin +router + .route('/category/:id') + .get(protect, admin, getCategoryById) + .delete(protect, admin, deleteCategory) + .put(protect, admin, updateCategory) // @desc Fetch products based on category // @route GET /api/categories/:category // @access Public - -router.get( - '/:category', - asyncHandler(async (req, res) => { - const categorywiseProducts = await Product.find({ - category: req.params.category, - }) - // const categorywiseProducts = products.filter((p) => p.category === req.params.category) - if (categorywiseProducts) { - res.json(categorywiseProducts) - } else { - res.status(404) - throw new Error('Products not found') - } - }), -) +router.route('/:category').get(getProductsByCategory) module.exports = router diff --git a/frontend/src/App.js b/frontend/src/App.js index fa32d87..4422554 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -25,8 +25,10 @@ import OrderListScreen from './screens/OrderListScreen' import SearchScreen from './screens/SearchScreen' import CarouselListScreen from './screens/CarouselListScreen' import CarouselEditScreen from './screens/CarouselEditScreen' +import CategoryEditScreen from './screens/CategoryEditScreen' import CreateCarouselScreen from './screens/CreateCarouselScreen' -import { CategoryListScreen } from './screens/CategoryListScreen' +import CategoryListScreen from './screens/CategoryListScreen' +import CreateCategoryScreen from './screens/CreateCategoryScreen' import Error404Screen from './screens/Error404Screen' import Terms from './pages/Terms' import Returns from './pages/Returns' @@ -81,6 +83,14 @@ function App() { path="/admin/carousel/:id/edit" component={CarouselEditScreen} /> + + diff --git a/frontend/src/actions/categoryActions.js b/frontend/src/actions/categoryActions.js index 88aca3a..17d8786 100644 --- a/frontend/src/actions/categoryActions.js +++ b/frontend/src/actions/categoryActions.js @@ -4,8 +4,22 @@ import { CATEGORY_LIST_REQUEST, CATEGORY_LIST_SUCCESS, CATEGORY_LIST_FAIL, + CATEGORY_DELETE_REQUEST, + CATEGORY_DELETE_SUCCESS, + CATEGORY_DELETE_FAIL, + CATEGORY_CREATE_REQUEST, + CATEGORY_CREATE_SUCCESS, + CATEGORY_CREATE_FAIL, + CATEGORY_UPDATE_REQUEST, + CATEGORY_UPDATE_SUCCESS, + CATEGORY_UPDATE_FAIL, + CATEGORY_DETAILS_REQUEST, + CATEGORY_DETAILS_SUCCESS, + CATEGORY_DETAILS_FAIL } from '../constants/categoryConstants' +import { logout } from './userActions' + export const listCategories = () => async (dispatch) => { try { dispatch({ type: CATEGORY_LIST_REQUEST }) @@ -21,3 +35,137 @@ export const listCategories = () => async (dispatch) => { }) } } + +export const listCategoryDetails = (id) => async (dispatch) => { + try { + dispatch({ type: CATEGORY_DETAILS_REQUEST }) + const { data } = await axios.get(`/api/categories/category/${id}`) + console.log(data); + dispatch({ type: CATEGORY_DETAILS_SUCCESS, payload: data }) + } catch (error) { + dispatch({ + type: CATEGORY_DETAILS_FAIL, + payload: + error.response && error.response.data.message + ? error.response.data.message + : error.message, + }) + } +} + +export const deleteCategory = (id) => async (dispatch, getState) => { + try { + dispatch({ + type: CATEGORY_DELETE_REQUEST, + }) + + const { + userLogin: { userInfo }, + } = getState() + + const config = { + headers: { + Authorization: `Bearer ${userInfo.token}`, + }, + } + + await axios.delete(`/api/categories/category/${id}`, config) + + dispatch({ + type: CATEGORY_DELETE_SUCCESS, + }) + } catch (error) { + const message = + error.response && error.response.data.message + ? error.response.data.message + : error.message + if (message === 'Not authorized, token failed') { + dispatch(logout()) + } + dispatch({ + type: CATEGORY_DELETE_FAIL, + payload: message, + }) + } +} + +export const createCategory = (category) => async (dispatch, getState) => { + try { + dispatch({ + type: CATEGORY_CREATE_REQUEST, + }) + + const { + userLogin: { userInfo }, + } = getState() + + const config = { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${userInfo.token}`, + }, + } + console.log(category); + const { data } = await axios.post(`/api/categories`, category, config) + + dispatch({ + type: CATEGORY_CREATE_SUCCESS, + payload: data, + }) + } catch (error) { + const message = + error.response && error.response.data + ? error.response.data + : error.message + if (message === 'Not authorized, token failed') { + dispatch(logout()) + } + dispatch({ + type: CATEGORY_CREATE_FAIL, + payload: message, + }) + } +} + +export const updateCategory = (category) => async (dispatch, getState) => { + try { + dispatch({ + type: CATEGORY_UPDATE_REQUEST, + }) + + const { + userLogin: { userInfo }, + } = getState() + + const config = { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${userInfo.token}`, + }, + } + + const { data } = await axios.put( + `/api/categories/category/${category._id}`, + category, + config, + ) + + dispatch({ + type: CATEGORY_UPDATE_SUCCESS, + payload: data, + }) + dispatch({ type: CATEGORY_DETAILS_SUCCESS, payload: data }) + } catch (error) { + const message = + error.response && error.response.data.message + ? error.response.data.message + : error.message + if (message === 'Not authorized, token failed') { + dispatch(logout()) + } + dispatch({ + type: CATEGORY_UPDATE_FAIL, + payload: message, + }) + } +} \ No newline at end of file diff --git a/frontend/src/constants/categoryConstants.js b/frontend/src/constants/categoryConstants.js index 30303e3..331bef0 100644 --- a/frontend/src/constants/categoryConstants.js +++ b/frontend/src/constants/categoryConstants.js @@ -1,3 +1,23 @@ export const CATEGORY_LIST_REQUEST = 'CATEGORY_LIST_REQUEST' export const CATEGORY_LIST_SUCCESS = 'CATEGORY_LIST_SUCCESS' export const CATEGORY_LIST_FAIL = 'CATEGORY_LIST_FAIL' + +export const CATEGORY_DELETE_REQUEST = 'CATEGORY_DELETE_REQUEST' +export const CATEGORY_DELETE_SUCCESS = 'CATEGORY_DELETE_SUCCESS' +export const CATEGORY_DELETE_FAIL = 'CATEGORY_DELETE_FAIL' + +export const CATEGORY_CREATE_REQUEST = 'CATEGORY_CREATE_REQUEST' +export const CATEGORY_CREATE_SUCCESS = 'CATEGORY_CREATE_SUCCESS' +export const CATEGORY_CREATE_FAIL = 'CATEGORY_CREATE_FAIL' +export const CATEGORY_CREATE_RESET = 'CATEGORY_CREATE_RESET' + +export const CATEGORY_UPDATE_REQUEST = 'CATEGORY_UPDATE_REQUEST' +export const CATEGORY_UPDATE_SUCCESS = 'CATEGORY_UPDATE_SUCCESS' +export const CATEGORY_UPDATE_FAIL = 'CATEGORY_UPDATE_FAIL' +export const CATEGORY_UPDATE_RESET = 'CATEGORY_UPDATE_RESET' + + +export const CATEGORY_DETAILS_REQUEST = 'CATEGORY_DETAILS_REQUEST' +export const CATEGORY_DETAILS_SUCCESS = 'CATEGORY_DETAILS_SUCCESS' +export const CATEGORY_DETAILS_FAIL = 'CATEGORY_DETAILS_FAIL' +export const CATEGORY_DETAILS_RESET = 'CATEGORY_DETAILS_RESET' diff --git a/frontend/src/reducers/categoryReducers.js b/frontend/src/reducers/categoryReducers.js index af67ba1..ac23145 100644 --- a/frontend/src/reducers/categoryReducers.js +++ b/frontend/src/reducers/categoryReducers.js @@ -2,6 +2,21 @@ import { CATEGORY_LIST_REQUEST, CATEGORY_LIST_SUCCESS, CATEGORY_LIST_FAIL, + CATEGORY_DELETE_REQUEST, + CATEGORY_DELETE_SUCCESS, + CATEGORY_DELETE_FAIL, + CATEGORY_CREATE_REQUEST, + CATEGORY_CREATE_SUCCESS, + CATEGORY_CREATE_FAIL, + CATEGORY_CREATE_RESET, + CATEGORY_UPDATE_REQUEST, + CATEGORY_UPDATE_SUCCESS, + CATEGORY_UPDATE_FAIL, + CATEGORY_UPDATE_RESET, + CATEGORY_DETAILS_REQUEST, + CATEGORY_DETAILS_SUCCESS, + CATEGORY_DETAILS_FAIL, + CATEGORY_DETAILS_RESET, } from '../constants/categoryConstants' export const categoryListReducer = (state = { categories: [] }, action) => { @@ -16,3 +31,61 @@ export const categoryListReducer = (state = { categories: [] }, action) => { return state } } + +export const categoryDetailsReducer = (state = { category: {} }, action) => { + switch (action.type) { + case CATEGORY_DETAILS_REQUEST: + return { loading: true, ...state } + case CATEGORY_DETAILS_SUCCESS: + return { loading: false, category: action.payload } + case CATEGORY_DETAILS_FAIL: + return { loading: false, error: action.payload } + case CATEGORY_DETAILS_RESET: + return { category: {} } + default: + return state + } +} + +export const categoryDeleteReducer = (state = {}, action) => { + switch (action.type) { + case CATEGORY_DELETE_REQUEST: + return { loading: true } + case CATEGORY_DELETE_SUCCESS: + return { loading: false, success: true } + case CATEGORY_DELETE_FAIL: + return { loading: false, error: action.payload } + default: + return state + } +} + +export const categoryCreateReducer = (state = { category: {} }, action) => { + switch (action.type) { + case CATEGORY_CREATE_REQUEST: + return { loading: true } + case CATEGORY_CREATE_SUCCESS: + return { loading: false, success: true, category: action.payload } + case CATEGORY_CREATE_FAIL: + return { loading: false, error: action.payload } + case CATEGORY_CREATE_RESET: + return {} + default: + return state + } +} + +export const categoryUpdateReducer = (state = { category: {} }, action) => { + switch (action.type) { + case CATEGORY_UPDATE_REQUEST: + return { loading: true } + case CATEGORY_UPDATE_SUCCESS: + return { loading: false, success: true, category: action.payload } + case CATEGORY_UPDATE_FAIL: + return { loading: false, error: action.payload } + case CATEGORY_UPDATE_RESET: + return { category: {} } + default: + return state + } +} diff --git a/frontend/src/screens/CategoryEditScreen.js b/frontend/src/screens/CategoryEditScreen.js new file mode 100644 index 0000000..383e514 --- /dev/null +++ b/frontend/src/screens/CategoryEditScreen.js @@ -0,0 +1,167 @@ +import React, { useEffect, useState } from 'react' +import axios from 'axios' +import { Button, Form,Row,Col } from 'react-bootstrap' +import { useDispatch, useSelector } from 'react-redux' +import { Link } from 'react-router-dom' +import { listCategoryDetails,updateCategory } from '../actions/categoryActions' +import FormContainer from '../components/FormContainer' +import Loader from '../components/Loader' +import Message from '../components/Message' +import { + CATEGORY_DETAILS_RESET, + CATEGORY_UPDATE_RESET, + CATEGORY_UPDATE_FAIL, +} from '../constants/categoryConstants' + +const CategoryEditScreen = ({ match, history }) => { + const categoryId = match.params.id + const [name, setName] = useState('') + const [imageSrc, setImageSrc] = useState('') + const [imageAlt, setImageAlt] = useState('') + const [uploading, setUploading] = useState(false) + + const dispatch = useDispatch() + + const categoryDetails = useSelector((state) => state.categoryDetails) + const { loading, error, category } = categoryDetails + + const categoryUpdate = useSelector((state) => state.categoryUpdate) + const { + loading: loadingUpdate, + error: errorUpdate, + success: successUpdate, + } = categoryUpdate + + useEffect(() => { + if (successUpdate) { + dispatch({ type: CATEGORY_UPDATE_RESET }) + dispatch({ type: CATEGORY_DETAILS_RESET }) + history.push('/admin/categorylist') + } else { + if (category._id !== categoryId) { + dispatch(listCategoryDetails(categoryId)) + } else { + setName(category.name) + setImageSrc(category.imageSrc) + setImageAlt(category.imageAlt) + } + } + }, [dispatch, history, category, categoryId, successUpdate]) + + const uploadFileHandler = async (e) => { + const file = e.target.files[0] + const formData = new FormData() + formData.append('image', file) + setUploading(true) + + try { + const config = { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + + const { data } = await axios.post('/api/upload', formData, config) + + setImageSrc(data) + setUploading(false) + } catch (error) { + const message = + error.response && error.response.data.message + ? error.response.data.message + : error.message + dispatch({ + type: CATEGORY_UPDATE_FAIL, + payload: message, + }) + setImageSrc('') + setUploading(false) + } + } + + const submitHandler = (e) => { + console.log(categoryId); + e.preventDefault() + dispatch( + updateCategory({ + _id: categoryId, + name, + imageSrc, + imageAlt, + }), + ) + } + + return ( + <> + +

Edit Category

+ {loadingUpdate && } + {errorUpdate && {errorUpdate}} + {loading ? ( + + ) : error ? ( + {error} + ) : ( +
+ + + Name + setName(e.target.value)} + > + + + + + Upload Category Image + + + + {uploading && } + {!uploading && imageSrc && ( + product + )} + + + + + + Image Alt + setImageAlt(e.target.value)} + > + + + + + Go Back + +
+ )} +
+ + ) +} + +export default CategoryEditScreen diff --git a/frontend/src/screens/CategoryListScreen.js b/frontend/src/screens/CategoryListScreen.js index 6790cb3..8851d03 100644 --- a/frontend/src/screens/CategoryListScreen.js +++ b/frontend/src/screens/CategoryListScreen.js @@ -1,17 +1,104 @@ -import React from 'react' -import { Col, Row } from 'react-bootstrap' +import React, { useEffect } from 'react' +import { LinkContainer } from 'react-router-bootstrap' +import { Table, Button, Row, Col, ButtonGroup } from 'react-bootstrap' +import { useDispatch, useSelector } from 'react-redux' +import Message from '../components/Message' +import Loader from '../components/Loader' +import { deleteCategory, listCategories } from '../actions/categoryActions' +import { CATEGORY_CREATE_RESET } from '../constants/categoryConstants' + +const CategoryListScreen = ({ history, match }) => { + const dispatch = useDispatch() + + const categoryList = useSelector((state) => state.categoryList) + const { loading, error, categories } = categoryList + + const categoryDelete = useSelector((state) => state.categoryDelete) + const { + loading: loadingDelete, + error: errorDelete, + success: successDelete, + } = categoryDelete + + const userLogin = useSelector((state) => state.userLogin) + const { userInfo } = userLogin + + useEffect(() => { + dispatch({ type: CATEGORY_CREATE_RESET }) + + if (!userInfo || !userInfo.isAdmin) { + history.push('/login') + } else { + dispatch(listCategories()) + } + }, [dispatch, history, userInfo, successDelete]) + + const deleteHandler = (id) => { + if (window.confirm('Are you sure')) { + dispatch(deleteCategory(id)) + } + } + + const createCategoryHandler = () => { + history.push('/admin/createcategory') + } -export const CategoryListScreen = () => { return ( <> -

Category

+

Categorys

+ + +
- -

Page is under construction!

-
+ {loadingDelete && } + {errorDelete && {errorDelete}} + {loading ? ( + + ) : error ? ( + {error} + ) : ( + + + + + + + + + + {categories.map((category) => ( + + + + + + ))} + +
CATEGORY IDNAMEOPTIONS
{category._id}{category.name} + + + + + + +
+ )} ) } + +export default CategoryListScreen + diff --git a/frontend/src/screens/CreateCategoryScreen.js b/frontend/src/screens/CreateCategoryScreen.js new file mode 100644 index 0000000..7b66caa --- /dev/null +++ b/frontend/src/screens/CreateCategoryScreen.js @@ -0,0 +1,137 @@ +import React, { useState, useEffect } from 'react' +import axios from 'axios' +import { Link } from 'react-router-dom' +import { Row, Col, Form, Button } from 'react-bootstrap' +import { useDispatch, useSelector } from 'react-redux' +import Message from '../components/Message' +import Loader from '../components/Loader' +import FormContainer from '../components/FormContainer' +import { CATEGORY_CREATE_FAIL } from '../constants/categoryConstants' +import { createCategory } from '../actions/categoryActions' + +const CreateCategoryScreen = ({ history }) => { + const [name, setName] = useState('') + const [imageSrc, setImageSrc] = useState('') + const [imageAlt, setImageAlt] = useState('') + const [uploading, setUploading] = useState(false) + + const dispatch = useDispatch() + + const categoryCreate = useSelector((state) => state.categoryCreate) + const { loading, error, success } = categoryCreate + + useEffect(() => { + if (success) { + history.push('/admin/categorylist') + } + }, [success, history]) + + const uploadFileHandler = async (e) => { + const file = e.target.files[0] + const formData = new FormData() + formData.append('image', file) + setUploading(true) + + try { + const config = { + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + + const { data } = await axios.post('/api/upload', formData, config) + + setImageSrc(data) + setUploading(false) + } catch (error) { + const message = + error.response && error.response.data.message + ? error.response.data.message + : error.message + dispatch({ + type: CATEGORY_CREATE_FAIL, + payload: message, + }) + setImageSrc('') + setUploading(false) + } + } + + const submitHandler = (e) => { + e.preventDefault() + dispatch( + createCategory({ + name, + imageSrc, + imageAlt, + }), + ) + } + + return ( + <> + +

Create Category

+ {loading && } + {error && {error}} + +
+ + Name + setName(e.target.value)} + > + + + + + + Category Image + + + + {uploading && } + {!uploading && imageSrc && ( + category + )} + + + + + + Image Alt + setImageAlt(e.target.value)} + > + + + + Go Back + +
+
+ + ) +} + +export default CreateCategoryScreen diff --git a/frontend/src/store.js b/frontend/src/store.js index f0f4ba0..be250f7 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -7,7 +7,12 @@ import { carouselListReducer, carouselUpdateReducer, } from './reducers/carouselReducers' -import { categoryListReducer } from './reducers/categoryReducers' +import { + categoryDeleteReducer, + categoryListReducer, + categoryCreateReducer, + categoryUpdateReducer, + categoryDetailsReducer } from './reducers/categoryReducers' import { productListReducer, productDetailsReducer, @@ -44,6 +49,10 @@ const reducer = combineReducers({ carouselCreate: carouselCreateReducer, carouselUpdate: carouselUpdateReducer, categoryList: categoryListReducer, + categoryDetails: categoryDetailsReducer, + categoryCreate: categoryCreateReducer, + categoryUpdate: categoryUpdateReducer, + categoryDelete: categoryDeleteReducer, productList: productListReducer, productListBySearch: productListBySearchReducer, productListByCategory: productListByCategoryReducer,