Skip to content

Commit f3f4d53

Browse files
authored
Admin feature of adding new product (#338)
1 parent 71bac16 commit f3f4d53

File tree

8 files changed

+265
-6
lines changed

8 files changed

+265
-6
lines changed

services/web/src/actions/shopActions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,18 @@ export const applyCouponAction = ({
114114
},
115115
};
116116
};
117+
118+
export const newProductAction = ({
119+
accessToken,
120+
callback,
121+
...data
122+
}: ActionPayload) => {
123+
return {
124+
type: actionTypes.NEW_PRODUCT,
125+
payload: {
126+
accessToken,
127+
...data,
128+
callback,
129+
},
130+
};
131+
};

services/web/src/components/layout/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ const AfterLogin: React.FC<AfterLoginProps> = ({
108108
return <Navigate to="/login" />;
109109
}
110110

111-
if (!componentRole || (componentRole && componentRole === userRole)) {
111+
if (
112+
!componentRole ||
113+
(componentRole && componentRole === userRole) ||
114+
userRole === roleTypes.ROLE_ADMIN
115+
) {
112116
return <Component />;
113117
}
114118

services/web/src/components/shop/shop.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,12 @@ import {
3434
OrderedListOutlined,
3535
ShoppingCartOutlined,
3636
} from "@ant-design/icons";
37-
import { COUPON_CODE_REQUIRED } from "../../constants/messages";
37+
import {
38+
COUPON_CODE_REQUIRED,
39+
PRODUCT_DETAILS_REQUIRED,
40+
} from "../../constants/messages";
3841
import { useNavigate } from "react-router-dom";
42+
import roleTypes from "../../constants/roleTypes";
3943

4044
const { Content } = Layout;
4145
const { Meta } = Card;
@@ -59,6 +63,12 @@ interface ShopProps extends PropsFromRedux {
5963
nextOffset: number | null;
6064
onOffsetChange: (offset: number | null) => void;
6165
onBuyProduct: (product: Product) => void;
66+
isNewProductFormOpen: boolean;
67+
setIsNewProductFormOpen: (isOpen: boolean) => void;
68+
newProductHasErrored: boolean;
69+
newProductErrorMessage: string;
70+
onNewProductFinish: (values: any) => void;
71+
role: string;
6272
}
6373

6474
const ProductAvatar: React.FC<{ image_url: string }> = ({ image_url }) => (
@@ -105,6 +115,12 @@ const Shop: React.FC<ShopProps> = (props) => {
105115
nextOffset,
106116
onOffsetChange,
107117
onBuyProduct,
118+
isNewProductFormOpen,
119+
setIsNewProductFormOpen,
120+
newProductHasErrored,
121+
newProductErrorMessage,
122+
onNewProductFinish,
123+
role,
108124
} = props;
109125

110126
return (
@@ -160,6 +176,23 @@ const Shop: React.FC<ShopProps> = (props) => {
160176
</Card>
161177
</Col>
162178
))}
179+
{role === roleTypes.ROLE_ADMIN && (
180+
<Col span={8} key="new-product-card">
181+
<Card
182+
className="new-product-card"
183+
onClick={() => setIsNewProductFormOpen(true)}
184+
cover={<PlusOutlined className="add-icon" />}
185+
>
186+
<Meta
187+
description={
188+
<div className="product-info product-price">
189+
Add Product
190+
</div>
191+
}
192+
/>
193+
</Card>
194+
</Col>
195+
)}
163196
</Row>
164197
<Row justify="center" className="pagination">
165198
<Button
@@ -211,6 +244,53 @@ const Shop: React.FC<ShopProps> = (props) => {
211244
</Form.Item>
212245
</Form>
213246
</Modal>
247+
<Modal
248+
title="Add New Product"
249+
open={isNewProductFormOpen}
250+
footer={null}
251+
onCancel={() => setIsNewProductFormOpen(false)}
252+
>
253+
<Form
254+
name="basic"
255+
initialValues={{
256+
remember: true,
257+
}}
258+
onFinish={onNewProductFinish}
259+
>
260+
<Form.Item
261+
name="name"
262+
rules={[{ required: true, message: PRODUCT_DETAILS_REQUIRED }]}
263+
>
264+
<Input placeholder="Product Name" />
265+
</Form.Item>
266+
<Form.Item
267+
name="price"
268+
rules={[
269+
{ required: true, message: PRODUCT_DETAILS_REQUIRED },
270+
{
271+
pattern: /^\d+$/,
272+
message: "Please enter a valid price!",
273+
},
274+
]}
275+
>
276+
<Input placeholder="Price" type="number" step="1" />
277+
</Form.Item>
278+
<Form.Item
279+
name="image_url"
280+
rules={[{ required: true, message: PRODUCT_DETAILS_REQUIRED }]}
281+
>
282+
<Input placeholder="Image URL (e.g., images/product.svg)" />
283+
</Form.Item>
284+
<Form.Item>
285+
{newProductHasErrored && (
286+
<div className="error-message">{newProductErrorMessage}</div>
287+
)}
288+
<Button type="primary" htmlType="submit" className="form-button">
289+
Add
290+
</Button>
291+
</Form.Item>
292+
</Form>
293+
</Modal>
214294
</Layout>
215295
);
216296
};
@@ -223,12 +303,23 @@ interface RootState {
223303
prevOffset: number | null;
224304
nextOffset: number | null;
225305
};
306+
userReducer: {
307+
role: string;
308+
};
226309
}
227310

228311
const mapStateToProps = (state: RootState) => {
229312
const { accessToken, availableCredit, products, prevOffset, nextOffset } =
230313
state.shopReducer;
231-
return { accessToken, availableCredit, products, prevOffset, nextOffset };
314+
const { role } = state.userReducer;
315+
return {
316+
accessToken,
317+
availableCredit,
318+
products,
319+
prevOffset,
320+
nextOffset,
321+
role,
322+
};
232323
};
233324

234325
const connector = connect(mapStateToProps);

services/web/src/components/shop/styles.css

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@
315315

316316
/* Responsive Design */
317317
@media (max-width: 1200px) {
318-
.product-card .ant-card-cover {
318+
.product-card .ant-card-cover,
319+
.new-product-card .ant-card-cover {
319320
min-height: 240px;
320321
}
321322
}
@@ -329,7 +330,8 @@
329330
margin-bottom: var(--spacing-md);
330331
}
331332

332-
.product-card .ant-card-cover {
333+
.product-card .ant-card-cover,
334+
.new-product-card .ant-card-cover {
333335
min-height: 200px;
334336
padding: var(--spacing-md);
335337
}
@@ -366,6 +368,19 @@
366368
.pagination .ant-btn {
367369
width: 200px;
368370
}
371+
372+
.new-product-card .ant-card-body {
373+
min-height: 180px;
374+
padding: var(--spacing-md);
375+
}
376+
377+
.add-icon {
378+
font-size: 100px;
379+
}
380+
381+
.new-product-card .product-price {
382+
font-size: 22px;
383+
}
369384
}
370385

371386
@media (max-width: 576px) {
@@ -377,5 +392,59 @@
377392
.page-header .ant-btn {
378393
width: 100%;
379394
}
395+
396+
.new-product-card .ant-card-cover {
397+
min-height: 180px;
398+
}
399+
400+
.new-product-card .ant-card-body {
401+
min-height: 160px;
402+
}
403+
404+
.add-icon {
405+
font-size: 80px;
406+
}
407+
408+
.new-product-card .product-price {
409+
font-size: 18px;
410+
}
411+
}
412+
413+
.new-product-card {
414+
border: 5px dashed #d9d9d9 !important;
415+
background: transparent !important;
416+
}
417+
418+
.new-product-card:hover {
419+
border: none !important;
420+
background: rgba(255, 255, 255, 0.8) !important;
421+
border-color: rgba(139, 92, 246, 0.3);
422+
}
423+
424+
.new-product-card .ant-card-cover {
425+
min-height: 280px;
426+
display: flex;
427+
align-items: center;
428+
justify-content: center;
429+
padding: var(--spacing-lg);
380430
}
381431

432+
.add-icon {
433+
font-size: 150px;
434+
color: #8b5cf6;
435+
opacity: 0.5;
436+
transition: all 0.3s ease;
437+
}
438+
439+
.new-product-card .ant-card-body {
440+
min-height: 217px;
441+
padding: var(--spacing-lg);
442+
display: flex;
443+
align-items: center;
444+
justify-content: center;
445+
}
446+
447+
.new-product-card .product-price {
448+
font-size: 30px;
449+
font-weight: 700;
450+
}

services/web/src/constants/actionTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const actionTypes = {
7070
RETURN_ORDER: "RETURN_ORDER",
7171
ORDER_RETURNED: "ORDER_RETURNED",
7272
APPLY_COUPON: "APPLY_COUPON",
73+
NEW_PRODUCT: "NEW_PRODUCT",
7374

7475
GET_POSTS: "GET_POSTS",
7576
FETCHED_POSTS: "FETCHED_POSTS",

services/web/src/constants/messages.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export const POST_TITLE_REQUIRED: string = "Please enter title for post!";
5353
export const POST_DESC_REQUIRED: string = "Please enter description for Post!";
5454
export const COMMENT_REQUIRED: string = "Please enter a comment!";
5555
export const COUPON_CODE_REQUIRED: string = "Please enter a coupon code!";
56-
56+
export const PRODUCT_DETAILS_REQUIRED: string =
57+
"Please enter all product details!";
5758
export const NO_VEHICLE_DESC_1: string =
5859
"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.";
5960
export const NO_VEHICLE_DESC_2: string = " Click here ";
@@ -83,6 +84,8 @@ export const ORDER_NOT_RETURNED: string = "Could not return order";
8384
export const INVALID_COUPON_CODE: string = "Invalid Coupon Code";
8485
export const COUPON_APPLIED: string = "Coupon applied";
8586
export const COUPON_NOT_APPLIED: string = "Could not validate coupon";
87+
export const PRODUCT_NOT_ADDED: string = "Could not add product";
88+
export const NEW_PRODUCT_ADDED: string = "Product added!";
8689
export const INVALID_CREDS: string = "Invalid Username or Password";
8790
export const INVALID_CODE_CREDS: string = "Invalid Email or Code";
8891
export const SIGN_UP_SUCCESS: string = "User Registered Successfully!";

services/web/src/containers/shop/shop.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getProductsAction,
2323
buyProductAction,
2424
applyCouponAction,
25+
newProductAction,
2526
} from "../../actions/shopActions";
2627
import Shop from "../../components/shop/shop";
2728
import { useNavigate } from "react-router-dom";
@@ -36,6 +37,10 @@ const ShopContainer = (props) => {
3637
const [errorMessage, setErrorMessage] = React.useState("");
3738
const [isCouponFormOpen, setIsCouponFormOpen] = useState(false);
3839

40+
const [isNewProductFormOpen, setIsNewProductFormOpen] = useState(false);
41+
const [newProductHasErrored, setNewProductHasErrored] = useState(false);
42+
const [newProductErrorMessage, setNewProductErrorMessage] = useState("");
43+
3944
useEffect(() => {
4045
const callback = (res, data) => {
4146
if (res !== responseTypes.SUCCESS) {
@@ -98,6 +103,27 @@ const ShopContainer = (props) => {
98103
});
99104
};
100105

106+
const handleNewProductFormFinish = (values) => {
107+
const callback = (res, data) => {
108+
if (res === responseTypes.SUCCESS) {
109+
setIsNewProductFormOpen(false);
110+
Modal.success({
111+
title: SUCCESS_MESSAGE,
112+
content: data,
113+
onOk: () => handleOffsetChange(0),
114+
});
115+
} else {
116+
setNewProductHasErrored(true);
117+
setNewProductErrorMessage(data);
118+
}
119+
};
120+
props.newProduct({
121+
callback,
122+
accessToken,
123+
...values,
124+
});
125+
};
126+
101127
return (
102128
<Shop
103129
onBuyProduct={handleBuyProduct}
@@ -107,6 +133,11 @@ const ShopContainer = (props) => {
107133
errorMessage={errorMessage}
108134
onFinish={handleFormFinish}
109135
onOffsetChange={handleOffsetChange}
136+
isNewProductFormOpen={isNewProductFormOpen}
137+
setIsNewProductFormOpen={setIsNewProductFormOpen}
138+
newProductHasErrored={newProductHasErrored}
139+
newProductErrorMessage={newProductErrorMessage}
140+
onNewProductFinish={handleNewProductFormFinish}
110141
{...props}
111142
/>
112143
);
@@ -122,13 +153,15 @@ const mapDispatchToProps = {
122153
getProducts: getProductsAction,
123154
buyProduct: buyProductAction,
124155
applyCoupon: applyCouponAction,
156+
newProduct: newProductAction,
125157
};
126158

127159
ShopContainer.propTypes = {
128160
accessToken: PropTypes.string,
129161
getProducts: PropTypes.func,
130162
buyProduct: PropTypes.func,
131163
applyCoupon: PropTypes.func,
164+
newProduct: PropTypes.func,
132165
nextOffset: PropTypes.number,
133166
prevOffset: PropTypes.number,
134167
onOffsetChange: PropTypes.func,

0 commit comments

Comments
 (0)