From d686e6e644629a949cde40aaa075e5923f5df528 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sat, 1 Nov 2025 03:34:24 +0530 Subject: [PATCH] Admin feature of adding new product --- services/web/src/actions/shopActions.ts | 15 +++ services/web/src/components/layout/layout.tsx | 6 +- services/web/src/components/shop/shop.tsx | 95 ++++++++++++++++++- services/web/src/components/shop/styles.css | 73 +++++++++++++- services/web/src/constants/actionTypes.ts | 1 + services/web/src/constants/messages.ts | 5 +- services/web/src/containers/shop/shop.js | 33 +++++++ services/web/src/sagas/shopSaga.ts | 43 +++++++++ 8 files changed, 265 insertions(+), 6 deletions(-) diff --git a/services/web/src/actions/shopActions.ts b/services/web/src/actions/shopActions.ts index b4d39de3..ba7286f8 100644 --- a/services/web/src/actions/shopActions.ts +++ b/services/web/src/actions/shopActions.ts @@ -114,3 +114,18 @@ export const applyCouponAction = ({ }, }; }; + +export const newProductAction = ({ + accessToken, + callback, + ...data +}: ActionPayload) => { + return { + type: actionTypes.NEW_PRODUCT, + payload: { + accessToken, + ...data, + callback, + }, + }; +}; diff --git a/services/web/src/components/layout/layout.tsx b/services/web/src/components/layout/layout.tsx index 3285c58c..04a92fb4 100644 --- a/services/web/src/components/layout/layout.tsx +++ b/services/web/src/components/layout/layout.tsx @@ -108,7 +108,11 @@ const AfterLogin: React.FC = ({ return ; } - if (!componentRole || (componentRole && componentRole === userRole)) { + if ( + !componentRole || + (componentRole && componentRole === userRole) || + userRole === roleTypes.ROLE_ADMIN + ) { return ; } diff --git a/services/web/src/components/shop/shop.tsx b/services/web/src/components/shop/shop.tsx index 5155c574..b344b1c3 100644 --- a/services/web/src/components/shop/shop.tsx +++ b/services/web/src/components/shop/shop.tsx @@ -34,8 +34,12 @@ import { OrderedListOutlined, ShoppingCartOutlined, } from "@ant-design/icons"; -import { COUPON_CODE_REQUIRED } from "../../constants/messages"; +import { + COUPON_CODE_REQUIRED, + PRODUCT_DETAILS_REQUIRED, +} from "../../constants/messages"; import { useNavigate } from "react-router-dom"; +import roleTypes from "../../constants/roleTypes"; const { Content } = Layout; const { Meta } = Card; @@ -59,6 +63,12 @@ interface ShopProps extends PropsFromRedux { nextOffset: number | null; onOffsetChange: (offset: number | null) => void; onBuyProduct: (product: Product) => void; + isNewProductFormOpen: boolean; + setIsNewProductFormOpen: (isOpen: boolean) => void; + newProductHasErrored: boolean; + newProductErrorMessage: string; + onNewProductFinish: (values: any) => void; + role: string; } const ProductAvatar: React.FC<{ image_url: string }> = ({ image_url }) => ( @@ -105,6 +115,12 @@ const Shop: React.FC = (props) => { nextOffset, onOffsetChange, onBuyProduct, + isNewProductFormOpen, + setIsNewProductFormOpen, + newProductHasErrored, + newProductErrorMessage, + onNewProductFinish, + role, } = props; return ( @@ -160,6 +176,23 @@ const Shop: React.FC = (props) => { ))} + {role === roleTypes.ROLE_ADMIN && ( + + setIsNewProductFormOpen(true)} + cover={} + > + + Add Product + + } + /> + + + )} + + + ); }; @@ -223,12 +303,23 @@ interface RootState { prevOffset: number | null; nextOffset: number | null; }; + userReducer: { + role: string; + }; } const mapStateToProps = (state: RootState) => { const { accessToken, availableCredit, products, prevOffset, nextOffset } = state.shopReducer; - return { accessToken, availableCredit, products, prevOffset, nextOffset }; + const { role } = state.userReducer; + return { + accessToken, + availableCredit, + products, + prevOffset, + nextOffset, + role, + }; }; const connector = connect(mapStateToProps); diff --git a/services/web/src/components/shop/styles.css b/services/web/src/components/shop/styles.css index 7ae94fef..2394e38c 100644 --- a/services/web/src/components/shop/styles.css +++ b/services/web/src/components/shop/styles.css @@ -315,7 +315,8 @@ /* Responsive Design */ @media (max-width: 1200px) { - .product-card .ant-card-cover { + .product-card .ant-card-cover, + .new-product-card .ant-card-cover { min-height: 240px; } } @@ -329,7 +330,8 @@ margin-bottom: var(--spacing-md); } - .product-card .ant-card-cover { + .product-card .ant-card-cover, + .new-product-card .ant-card-cover { min-height: 200px; padding: var(--spacing-md); } @@ -366,6 +368,19 @@ .pagination .ant-btn { width: 200px; } + + .new-product-card .ant-card-body { + min-height: 180px; + padding: var(--spacing-md); + } + + .add-icon { + font-size: 100px; + } + + .new-product-card .product-price { + font-size: 22px; + } } @media (max-width: 576px) { @@ -377,5 +392,59 @@ .page-header .ant-btn { width: 100%; } + + .new-product-card .ant-card-cover { + min-height: 180px; + } + + .new-product-card .ant-card-body { + min-height: 160px; + } + + .add-icon { + font-size: 80px; + } + + .new-product-card .product-price { + font-size: 18px; + } +} + +.new-product-card { + border: 5px dashed #d9d9d9 !important; + background: transparent !important; +} + +.new-product-card:hover { + border: none !important; + background: rgba(255, 255, 255, 0.8) !important; + border-color: rgba(139, 92, 246, 0.3); +} + +.new-product-card .ant-card-cover { + min-height: 280px; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-lg); } +.add-icon { + font-size: 150px; + color: #8b5cf6; + opacity: 0.5; + transition: all 0.3s ease; +} + +.new-product-card .ant-card-body { + min-height: 217px; + padding: var(--spacing-lg); + display: flex; + align-items: center; + justify-content: center; +} + +.new-product-card .product-price { + font-size: 30px; + font-weight: 700; +} \ No newline at end of file diff --git a/services/web/src/constants/actionTypes.ts b/services/web/src/constants/actionTypes.ts index 861a3b60..28247026 100644 --- a/services/web/src/constants/actionTypes.ts +++ b/services/web/src/constants/actionTypes.ts @@ -70,6 +70,7 @@ const actionTypes = { RETURN_ORDER: "RETURN_ORDER", ORDER_RETURNED: "ORDER_RETURNED", APPLY_COUPON: "APPLY_COUPON", + NEW_PRODUCT: "NEW_PRODUCT", GET_POSTS: "GET_POSTS", FETCHED_POSTS: "FETCHED_POSTS", diff --git a/services/web/src/constants/messages.ts b/services/web/src/constants/messages.ts index 4a03de0e..c003f2d8 100644 --- a/services/web/src/constants/messages.ts +++ b/services/web/src/constants/messages.ts @@ -53,7 +53,8 @@ export const POST_TITLE_REQUIRED: string = "Please enter title for post!"; export const POST_DESC_REQUIRED: string = "Please enter description for Post!"; export const COMMENT_REQUIRED: string = "Please enter a comment!"; export const COUPON_CODE_REQUIRED: string = "Please enter a coupon code!"; - +export const PRODUCT_DETAILS_REQUIRED: string = + "Please enter all product details!"; export const NO_VEHICLE_DESC_1: string = "Your newly purchased Vehicle Details have been sent to you email address. Please check your email for the VIN and PIN code of your vehicle using the MailHog web portal."; export const NO_VEHICLE_DESC_2: string = " Click here "; @@ -83,6 +84,8 @@ export const ORDER_NOT_RETURNED: string = "Could not return order"; export const INVALID_COUPON_CODE: string = "Invalid Coupon Code"; export const COUPON_APPLIED: string = "Coupon applied"; export const COUPON_NOT_APPLIED: string = "Could not validate coupon"; +export const PRODUCT_NOT_ADDED: string = "Could not add product"; +export const NEW_PRODUCT_ADDED: string = "Product added!"; export const INVALID_CREDS: string = "Invalid Username or Password"; export const INVALID_CODE_CREDS: string = "Invalid Email or Code"; export const SIGN_UP_SUCCESS: string = "User Registered Successfully!"; diff --git a/services/web/src/containers/shop/shop.js b/services/web/src/containers/shop/shop.js index c7fc7ebf..dc60a8fb 100644 --- a/services/web/src/containers/shop/shop.js +++ b/services/web/src/containers/shop/shop.js @@ -22,6 +22,7 @@ import { getProductsAction, buyProductAction, applyCouponAction, + newProductAction, } from "../../actions/shopActions"; import Shop from "../../components/shop/shop"; import { useNavigate } from "react-router-dom"; @@ -36,6 +37,10 @@ const ShopContainer = (props) => { const [errorMessage, setErrorMessage] = React.useState(""); const [isCouponFormOpen, setIsCouponFormOpen] = useState(false); + const [isNewProductFormOpen, setIsNewProductFormOpen] = useState(false); + const [newProductHasErrored, setNewProductHasErrored] = useState(false); + const [newProductErrorMessage, setNewProductErrorMessage] = useState(""); + useEffect(() => { const callback = (res, data) => { if (res !== responseTypes.SUCCESS) { @@ -98,6 +103,27 @@ const ShopContainer = (props) => { }); }; + const handleNewProductFormFinish = (values) => { + const callback = (res, data) => { + if (res === responseTypes.SUCCESS) { + setIsNewProductFormOpen(false); + Modal.success({ + title: SUCCESS_MESSAGE, + content: data, + onOk: () => handleOffsetChange(0), + }); + } else { + setNewProductHasErrored(true); + setNewProductErrorMessage(data); + } + }; + props.newProduct({ + callback, + accessToken, + ...values, + }); + }; + return ( { errorMessage={errorMessage} onFinish={handleFormFinish} onOffsetChange={handleOffsetChange} + isNewProductFormOpen={isNewProductFormOpen} + setIsNewProductFormOpen={setIsNewProductFormOpen} + newProductHasErrored={newProductHasErrored} + newProductErrorMessage={newProductErrorMessage} + onNewProductFinish={handleNewProductFormFinish} {...props} /> ); @@ -122,6 +153,7 @@ const mapDispatchToProps = { getProducts: getProductsAction, buyProduct: buyProductAction, applyCoupon: applyCouponAction, + newProduct: newProductAction, }; ShopContainer.propTypes = { @@ -129,6 +161,7 @@ ShopContainer.propTypes = { getProducts: PropTypes.func, buyProduct: PropTypes.func, applyCoupon: PropTypes.func, + newProduct: PropTypes.func, nextOffset: PropTypes.number, prevOffset: PropTypes.number, onOffsetChange: PropTypes.func, diff --git a/services/web/src/sagas/shopSaga.ts b/services/web/src/sagas/shopSaga.ts index 69689d34..5451314d 100644 --- a/services/web/src/sagas/shopSaga.ts +++ b/services/web/src/sagas/shopSaga.ts @@ -27,6 +27,8 @@ import { INVALID_COUPON_CODE, COUPON_APPLIED, COUPON_NOT_APPLIED, + NEW_PRODUCT_ADDED, + PRODUCT_NOT_ADDED, } from "../constants/messages"; interface ReceivedResponse extends Response { @@ -342,6 +344,46 @@ export function* applyCoupon(action: MyAction): Generator { } } +/** + * add a new product (admin only) + * @payload { accessToken, name, price, image_url, callback} payload + * accessToken: access token of the user + * name: product name + * price: product price + * image_url: product image URL + * callback : callback method + */ +export function* newProduct(action: MyAction): Generator { + const { accessToken, name, price, image_url, callback } = action.payload; + let recievedResponse: ReceivedResponse = {} as ReceivedResponse; + try { + yield put({ type: actionTypes.FETCHING_DATA }); + const postUrl = APIService.WORKSHOP_SERVICE + requestURLS.GET_PRODUCTS; + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; + const responseJson = yield fetch(postUrl, { + headers, + method: "POST", + body: JSON.stringify({ name, price: parseFloat(price), image_url }), + }).then((response: Response) => { + recievedResponse = response as ReceivedResponse; + return response.json(); + }); + + yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse }); + if (recievedResponse.ok) { + callback(responseTypes.SUCCESS, NEW_PRODUCT_ADDED); + } else { + callback(responseTypes.FAILURE, PRODUCT_NOT_ADDED); + } + } catch (e) { + yield put({ type: actionTypes.FETCHED_DATA, payload: recievedResponse }); + callback(responseTypes.FAILURE, PRODUCT_NOT_ADDED); + } +} + export function* shopActionWatcher(): Generator { yield takeLatest(actionTypes.GET_PRODUCTS, getProducts); yield takeLatest(actionTypes.BUY_PRODUCT, buyProduct); @@ -349,4 +391,5 @@ export function* shopActionWatcher(): Generator { yield takeLatest(actionTypes.GET_ORDER_BY_ID, getOrderById); yield takeLatest(actionTypes.RETURN_ORDER, returnOrder); yield takeLatest(actionTypes.APPLY_COUPON, applyCoupon); + yield takeLatest(actionTypes.NEW_PRODUCT, newProduct); }