Skip to content

Commit 8fc5c30

Browse files
committed
Merge branch 'develop' of github.com:atlp-rwanda/e-commerce-ninjas-fe into ft-become-seller
2 parents 0687647 + 85a82b1 commit 8fc5c30

File tree

17 files changed

+455
-22
lines changed

17 files changed

+455
-22
lines changed

Diff for: src/App.scss

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@
5151
@import "./assets/styles/SellerRegistration.scss";
5252
@import "./assets/styles/ServicesPage.scss";
5353
@import "./assets/styles/Settings.scss";
54-
@import "./assets/styles/HomePage.scss";
54+
@import "./assets/styles/HomePage.scss";
55+
@import "./assets/styles/ShopCard.scss";
56+
@import "./assets/styles/ShopPage.scss";

Diff for: src/assets/styles/ShopCard.scss

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
.shop-card {
2+
background-color: white;
3+
padding: 20px;
4+
border-radius: 8px;
5+
text-align: left;
6+
box-shadow: 0.1rem 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
7+
background-color: $secondary-color-light;
8+
transform: none;
9+
&:hover {
10+
transform: scale(1.02) !important;
11+
box-shadow: 1rem 1rem 1rem rgba(0, 0, 0, 0.3);
12+
transition: transform 0.1s ease !important;
13+
}
14+
15+
h2 {
16+
font-size: 1.5em;
17+
margin-bottom: 1em;
18+
}
19+
20+
.items {
21+
display: grid;
22+
grid-template-columns: repeat(2, 1fr);
23+
gap: 20px;
24+
margin-bottom: 20px;
25+
26+
.item {
27+
display: flex;
28+
flex-direction: column;
29+
align-items: center;
30+
cursor: pointer;
31+
32+
img {
33+
width: 100%;
34+
height: 10rem;
35+
object-fit: cover;
36+
}
37+
38+
p {
39+
margin-top: 10px;
40+
font-size: 1em;
41+
color: #333;
42+
}
43+
}
44+
}
45+
46+
.see-more {
47+
color: $primary-color;
48+
text-decoration: none;
49+
font-weight: bold;
50+
cursor: pointer;
51+
52+
&:hover {
53+
text-decoration: underline;
54+
}
55+
}
56+
}
57+

Diff for: src/assets/styles/ShopPage.scss

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.shop-container{
2+
display: grid;
3+
grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
4+
gap: 3rem;
5+
padding: 4rem;
6+
.loader {
7+
display: flex;
8+
justify-content: center;
9+
align-items: center;
10+
height: 100vh;
11+
width: 100vw;
12+
}
13+
}

Diff for: src/components/ResendEmail.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const validationSchema = yup.object({
1414
})
1515
export const ResendEmail = () => {
1616
const dispatch = useAppDispatch();
17-
const { isSuccess, isEmailResend, isLoading, message } = useAppSelector((state) => state?.auth)
17+
const { isEmailSuccess, isEmailResend, isLoading, message } = useAppSelector((state) => state?.auth)
1818
const initialValues = {
1919
email: "",
2020
};
@@ -27,13 +27,13 @@ export const ResendEmail = () => {
2727
})
2828
useEffect(() => {
2929
dispatch(resetAuth());
30-
if(isSuccess) {
30+
if(isEmailSuccess) {
3131
toast.success(message);
3232
}
3333
if (isEmailResend) {
3434
toast.error(message)
3535
}
36-
}, [dispatch,isSuccess,isEmailResend,message]);
36+
}, [dispatch,isEmailSuccess,isEmailResend,message]);
3737
return (
3838
<>
3939
<Meta title="Resend Email - E-Commerce Ninjas" />

Diff for: src/components/layout/Footer.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ function Footer() {
107107
</a>
108108
<span className="footer__text">Email:</span>
109109
<a
110-
href="mailto:ecommerceninjas@gmail.com"
110+
href="mailto:ecommerceninjas45@gmail.com"
111111
className="footer__link"
112112
>
113-
ecommerceninjas@gmail.com
113+
ecommerceninjas45@gmail.com
114114
</a>
115115
</li>
116116
</ul>

Diff for: src/components/layout/Header.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const Header: React.FC = () => {
145145
<IoMdMailUnread className="header__icon" />
146146
</div>
147147
<p className="header__text">Email us</p>
148-
<p className="header__description">support@ecommerce-ninjas.com</p>
148+
<p className="header__description">ecommerceninjas45@gmail.com</p>
149149
</div>
150150
<div className="header__box header__contact">
151151
<FaPhoneVolume className="header__icon" />

Diff for: src/components/layout/Sample.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import rightTop from "../../../public/assets/images/right-top.png";
1111
import leftBottom from "../../../public/assets/images/left-bottom.png";
1212
import rightBottom from "../../../public/assets/images/right-bottom.png";
1313
const images = [
14-
'/assets/middle.png',
15-
'/assets/images/1293.jpg',
16-
'/assets/images/add-cart-buy-now-online-commerce-graphic-concept.jpg',
17-
'/assets/images/cropped-image-woman-inputting-card-information-key-phone-laptop-while-shopping-online.jpg',
18-
'/assets/images/cyber-monday-shopping-sales.jpg',
19-
'/assets/images/happy-man-with-handbags-dancing-after-shopping-spree.jpg',
20-
'/assets/images/laptop-shopping-bags-online-shopping-concept.jpg',
14+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663918/middle_pmpcqw.png',
15+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663915/add-cart-buy-now-online-commerce-graphic-concept_mvuvex.jpg',
16+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663912/happy-man-with-handbags-dancing-after-shopping-spree_kieiwn.jpg',
17+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663909/laptop-shopping-bags-online-shopping-concept_pytoky.jpg',
18+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663908/cropped-image-woman-inputting-card-information-key-phone-laptop-while-shopping-online_l7ioph.jpg',
19+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663906/1293_b6kg3u.jpg',
20+
'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663890/cyber-monday-shopping-sales_d1gjm6.jpg',
2121
];
2222

2323
const Sample: React.FC = () => {

Diff for: src/components/product/ShopCard.tsx

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* eslint-disable */
2+
import React, { useEffect } from 'react';
3+
import { useAppDispatch, useAppSelector } from '../../store/store';
4+
import { fetchProductsByShopId } from '../../store/features/product/shopSlice';
5+
import { useNavigate } from 'react-router-dom';
6+
7+
interface ShopCardProps {
8+
shopId: string;
9+
}
10+
11+
const ShopCard: React.FC<ShopCardProps> = ({ shopId }) => {
12+
const dispatch = useAppDispatch();
13+
const navigate = useNavigate();
14+
15+
// const shop = useAppSelector((state) => state.shop.shops.find((shop) => shop.id === shopId));
16+
// const products = useAppSelector((state) => state.shop.shopProductsByShop?.[shopId] || []);
17+
const {
18+
shops,
19+
shopProductsByShop
20+
} = useAppSelector((state: any) => state.shop);
21+
22+
const shop = shops.find((shop: any) => shop.id === shopId);
23+
const products = shopProductsByShop?.[shopId] || [];
24+
25+
useEffect(() => {
26+
if (shopId) {
27+
dispatch(fetchProductsByShopId(shopId));
28+
}
29+
}, [dispatch, shopId]);
30+
31+
const truncateText = (text: string, length: number) => {
32+
return text.length > length ? `${text.substring(0, length)}...` : text;
33+
};
34+
35+
if (products.length < 4) {
36+
return null;
37+
}
38+
39+
return (
40+
<div className="shop-card">
41+
<h2>{shop?.name || 'Shop Name'}</h2>
42+
<div className="items">
43+
{products.slice(0, 4).map((product) => (
44+
<div
45+
key={product.id}
46+
className="item"
47+
onClick={() => navigate(`/product/${product.id}`)}
48+
>
49+
<img src={product.images[0]} alt={product.name} />
50+
<p>{truncateText(product.name, 20)}</p>
51+
</div>
52+
))}
53+
</div>
54+
<button
55+
className="see-more"
56+
onClick={() => navigate(`/shops/${shopId}/products`)}
57+
>
58+
See more
59+
</button>
60+
</div>
61+
);
62+
};
63+
64+
export default ShopCard;

Diff for: src/pages/AboutUs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const AboutUs = () => {
2525
image:
2626
"https://res.cloudinary.com/djrmfg6k9/image/upload/v1723551875/SaddockAime1_bqtq7b.jpg",
2727
position: "Full Stack Developer",
28-
linkedIn: "https://github.com/SaddockAime",
28+
linkedIn: "https://www.linkedin.com/in/saddock-kabandana-89b914237/",
2929
github: "https://github.com/SaddockAime",
3030
},
3131
{

Diff for: src/pages/ProductsByShopPage.tsx

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* eslint-disable */
2+
import React, { useEffect, useState } from "react";
3+
import { useParams, useNavigate } from "react-router-dom";
4+
import { useAppDispatch, useAppSelector } from "../store/store";
5+
import { fetchProductsByShopId } from "../store/features/product/shopSlice";
6+
import Product from "../components/product/Product";
7+
import { PuffLoader } from "react-spinners";
8+
import { Meta } from "../components/Meta";
9+
import { toast } from "react-toastify";
10+
import { createCart, getUserCarts } from "../store/features/carts/cartSlice";
11+
12+
const ProductsByShopPage: React.FC = () => {
13+
const { shopId } = useParams<{ shopId: string }>();
14+
const dispatch = useAppDispatch();
15+
const navigate = useNavigate();
16+
const [cartResponseData, setCartResponseData] = useState<any>(null);
17+
18+
const { shops, shopProductsByShop, isLoadingProducts, isErrorProducts, isSuccessProducts, message } = useAppSelector(
19+
(state: any) => state.shop
20+
);
21+
22+
const [priceRange, setPriceRange] = useState([0, 0]);
23+
const [maxPrice, setMaxPrice] = useState(0);
24+
const [minPrice, setMinPrice] = useState(0);
25+
const [visibleProducts, setVisibleProducts] = useState<number>(20);
26+
27+
useEffect(() => {
28+
if (shopId) {
29+
dispatch(fetchProductsByShopId(shopId));
30+
}
31+
}, [dispatch, shopId]);
32+
33+
const shop = shops.find((shop: any) => shop.id === shopId);
34+
const products = shopProductsByShop ? shopProductsByShop[shopId] : [];
35+
36+
useEffect(() => {
37+
if (products && products.length > 0) {
38+
const calculatedMaxPrice = products.reduce((max, product) => Math.max(max, product.price), 0);
39+
const calculatedMinPrice = products.reduce((min, product) => Math.min(min, product.price), calculatedMaxPrice);
40+
41+
setMaxPrice(calculatedMaxPrice);
42+
setMinPrice(calculatedMinPrice);
43+
setPriceRange([calculatedMinPrice, calculatedMaxPrice]);
44+
}
45+
}, [products]);
46+
47+
const handleAddProductToCart = async (productId: string, quantity = 1) => {
48+
try {
49+
const response = await dispatch(
50+
createCart({ productId, quantity })
51+
).unwrap();
52+
53+
if (response.data) {
54+
toast.success(response.message);
55+
const updatedResponse = await dispatch(getUserCarts()).unwrap();
56+
setCartResponseData(updatedResponse.data);
57+
} else {
58+
toast.error(response.message);
59+
}
60+
} catch (error: any) {
61+
if (error === "Not authorized") {
62+
localStorage.setItem("pendingCartProduct", productId);
63+
toast.error("Please login first");
64+
navigate("/login");
65+
} else {
66+
toast.error("Something went wrong. Please try again later.");
67+
}
68+
}
69+
};
70+
71+
const filteredProducts = products.filter((product: any) => {
72+
const price = parseFloat(product.price);
73+
return price >= priceRange[0] && price <= priceRange[1];
74+
});
75+
76+
const handleLoadMore = () => {
77+
setVisibleProducts((prevVisibleProducts) => prevVisibleProducts + 20);
78+
};
79+
80+
return (
81+
<>
82+
<Meta title={`Products - Shop ${shop?.name || 'Shop'}`} />
83+
<div className="landing-container">
84+
{isLoadingProducts ? (
85+
<div className="loader">
86+
<PuffLoader color="#ff6d18" size={300} loading={isLoadingProducts} />
87+
</div>
88+
) : isErrorProducts ? (
89+
<div className="error-message">
90+
<p>{message || "Something went wrong. Please try again later."}</p>
91+
</div>
92+
) : (
93+
<div>
94+
<div className="head">
95+
<h1>{shop?.name || 'Shop'}</h1>
96+
</div>
97+
<div className="filters">
98+
<div>
99+
<label>Price Range: </label>
100+
<input
101+
type="range"
102+
min={minPrice}
103+
max={maxPrice}
104+
value={priceRange[1]}
105+
onChange={(e) =>
106+
setPriceRange([priceRange[0], Number(e.target.value)])
107+
}
108+
/>
109+
<span className="span">{priceRange[0]}RWF - {priceRange[1]}RWF</span>
110+
</div>
111+
</div>
112+
<div className="product-list">
113+
{isSuccessProducts &&
114+
Array.isArray(filteredProducts) &&
115+
filteredProducts
116+
.slice(0,visibleProducts)
117+
.map((product: any) => (
118+
<Product
119+
key={product.id}
120+
id={product.id}
121+
images={product.images}
122+
name={product.name}
123+
price={product.price}
124+
stock={Number(product.quantity)}
125+
description={product.description}
126+
discount={Number(product.discount.replace("%", ""))}
127+
/>
128+
))}
129+
</div>
130+
{visibleProducts < products.length && (
131+
<div className="load-more">
132+
<button onClick={handleLoadMore}>Load More</button>
133+
</div>
134+
)}
135+
</div>
136+
)}
137+
</div>
138+
</>
139+
);
140+
};
141+
142+
export default ProductsByShopPage;

0 commit comments

Comments
 (0)