Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions services/web/src/actions/shopActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,18 @@ export const applyCouponAction = ({
},
};
};

export const newProductAction = ({
accessToken,
callback,
...data
}: ActionPayload) => {
return {
type: actionTypes.NEW_PRODUCT,
payload: {
accessToken,
...data,
callback,
},
};
};
6 changes: 5 additions & 1 deletion services/web/src/components/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ const AfterLogin: React.FC<AfterLoginProps> = ({
return <Navigate to="/login" />;
}

if (!componentRole || (componentRole && componentRole === userRole)) {
if (
!componentRole ||
(componentRole && componentRole === userRole) ||
userRole === roleTypes.ROLE_ADMIN
) {
return <Component />;
}

Expand Down
95 changes: 93 additions & 2 deletions services/web/src/components/shop/shop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 }) => (
Expand Down Expand Up @@ -105,6 +115,12 @@ const Shop: React.FC<ShopProps> = (props) => {
nextOffset,
onOffsetChange,
onBuyProduct,
isNewProductFormOpen,
setIsNewProductFormOpen,
newProductHasErrored,
newProductErrorMessage,
onNewProductFinish,
role,
} = props;

return (
Expand Down Expand Up @@ -160,6 +176,23 @@ const Shop: React.FC<ShopProps> = (props) => {
</Card>
</Col>
))}
{role === roleTypes.ROLE_ADMIN && (
<Col span={8} key="new-product-card">
<Card
className="new-product-card"
onClick={() => setIsNewProductFormOpen(true)}
cover={<PlusOutlined className="add-icon" />}
>
<Meta
description={
<div className="product-info product-price">
Add Product
</div>
}
/>
</Card>
</Col>
)}
</Row>
<Row justify="center" className="pagination">
<Button
Expand Down Expand Up @@ -211,6 +244,53 @@ const Shop: React.FC<ShopProps> = (props) => {
</Form.Item>
</Form>
</Modal>
<Modal
title="Add New Product"
open={isNewProductFormOpen}
footer={null}
onCancel={() => setIsNewProductFormOpen(false)}
>
<Form
name="basic"
initialValues={{
remember: true,
}}
onFinish={onNewProductFinish}
>
<Form.Item
name="name"
rules={[{ required: true, message: PRODUCT_DETAILS_REQUIRED }]}
>
<Input placeholder="Product Name" />
</Form.Item>
<Form.Item
name="price"
rules={[
{ required: true, message: PRODUCT_DETAILS_REQUIRED },
{
pattern: /^\d+$/,
message: "Please enter a valid price!",
},
]}
>
<Input placeholder="Price" type="number" step="1" />
</Form.Item>
<Form.Item
name="image_url"
rules={[{ required: true, message: PRODUCT_DETAILS_REQUIRED }]}
>
<Input placeholder="Image URL (e.g., images/product.svg)" />
</Form.Item>
<Form.Item>
{newProductHasErrored && (
<div className="error-message">{newProductErrorMessage}</div>
)}
<Button type="primary" htmlType="submit" className="form-button">
Add
</Button>
</Form.Item>
</Form>
</Modal>
</Layout>
);
};
Expand All @@ -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);
Expand Down
73 changes: 71 additions & 2 deletions services/web/src/components/shop/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions services/web/src/constants/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion services/web/src/constants/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ";
Expand Down Expand Up @@ -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!";
Expand Down
33 changes: 33 additions & 0 deletions services/web/src/containers/shop/shop.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
getProductsAction,
buyProductAction,
applyCouponAction,
newProductAction,
} from "../../actions/shopActions";
import Shop from "../../components/shop/shop";
import { useNavigate } from "react-router-dom";
Expand All @@ -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) {
Expand Down Expand Up @@ -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 (
<Shop
onBuyProduct={handleBuyProduct}
Expand All @@ -107,6 +133,11 @@ const ShopContainer = (props) => {
errorMessage={errorMessage}
onFinish={handleFormFinish}
onOffsetChange={handleOffsetChange}
isNewProductFormOpen={isNewProductFormOpen}
setIsNewProductFormOpen={setIsNewProductFormOpen}
newProductHasErrored={newProductHasErrored}
newProductErrorMessage={newProductErrorMessage}
onNewProductFinish={handleNewProductFormFinish}
{...props}
/>
);
Expand All @@ -122,13 +153,15 @@ const mapDispatchToProps = {
getProducts: getProductsAction,
buyProduct: buyProductAction,
applyCoupon: applyCouponAction,
newProduct: newProductAction,
};

ShopContainer.propTypes = {
accessToken: PropTypes.string,
getProducts: PropTypes.func,
buyProduct: PropTypes.func,
applyCoupon: PropTypes.func,
newProduct: PropTypes.func,
nextOffset: PropTypes.number,
prevOffset: PropTypes.number,
onOffsetChange: PropTypes.func,
Expand Down
Loading
Loading