diff --git a/src/App.scss b/src/App.scss
index c415562e..15dd7ec1 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -51,4 +51,6 @@
@import "./assets/styles/SellerRegistration.scss";
@import "./assets/styles/ServicesPage.scss";
@import "./assets/styles/Settings.scss";
-@import "./assets/styles/HomePage.scss";
\ No newline at end of file
+@import "./assets/styles/HomePage.scss";
+@import "./assets/styles/ShopCard.scss";
+@import "./assets/styles/ShopPage.scss";
\ No newline at end of file
diff --git a/src/assets/styles/ShopCard.scss b/src/assets/styles/ShopCard.scss
new file mode 100644
index 00000000..99db7ba7
--- /dev/null
+++ b/src/assets/styles/ShopCard.scss
@@ -0,0 +1,57 @@
+.shop-card {
+ background-color: white;
+ padding: 20px;
+ border-radius: 8px;
+ text-align: left;
+ box-shadow: 0.1rem 0.1rem 0.5rem rgba(0, 0, 0, 0.2);
+ background-color: $secondary-color-light;
+ transform: none;
+ &:hover {
+ transform: scale(1.02) !important;
+ box-shadow: 1rem 1rem 1rem rgba(0, 0, 0, 0.3);
+ transition: transform 0.1s ease !important;
+ }
+
+ h2 {
+ font-size: 1.5em;
+ margin-bottom: 1em;
+ }
+
+ .items {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ margin-bottom: 20px;
+
+ .item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+
+ img {
+ width: 100%;
+ height: 10rem;
+ object-fit: cover;
+ }
+
+ p {
+ margin-top: 10px;
+ font-size: 1em;
+ color: #333;
+ }
+ }
+ }
+
+ .see-more {
+ color: $primary-color;
+ text-decoration: none;
+ font-weight: bold;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/src/assets/styles/ShopPage.scss b/src/assets/styles/ShopPage.scss
new file mode 100644
index 00000000..4bf30df2
--- /dev/null
+++ b/src/assets/styles/ShopPage.scss
@@ -0,0 +1,13 @@
+.shop-container{
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
+ gap: 3rem;
+ padding: 4rem;
+ .loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ width: 100vw;
+ }
+}
\ No newline at end of file
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index da8c6912..1e806a77 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -107,10 +107,10 @@ function Footer() {
Email:
- ecommerceninjas@gmail.com
+ ecommerceninjas45@gmail.com
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 9b660745..a447c71b 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -145,7 +145,7 @@ const Header: React.FC = () => {
Email us
- support@ecommerce-ninjas.com
+ ecommerceninjas45@gmail.com
diff --git a/src/components/layout/Sample.tsx b/src/components/layout/Sample.tsx
index 2975a3e6..86b09c1a 100644
--- a/src/components/layout/Sample.tsx
+++ b/src/components/layout/Sample.tsx
@@ -11,13 +11,13 @@ import rightTop from "../../../public/assets/images/right-top.png";
import leftBottom from "../../../public/assets/images/left-bottom.png";
import rightBottom from "../../../public/assets/images/right-bottom.png";
const images = [
- '/assets/middle.png',
- '/assets/images/1293.jpg',
- '/assets/images/add-cart-buy-now-online-commerce-graphic-concept.jpg',
- '/assets/images/cropped-image-woman-inputting-card-information-key-phone-laptop-while-shopping-online.jpg',
- '/assets/images/cyber-monday-shopping-sales.jpg',
- '/assets/images/happy-man-with-handbags-dancing-after-shopping-spree.jpg',
- '/assets/images/laptop-shopping-bags-online-shopping-concept.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663918/middle_pmpcqw.png',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663915/add-cart-buy-now-online-commerce-graphic-concept_mvuvex.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663912/happy-man-with-handbags-dancing-after-shopping-spree_kieiwn.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663909/laptop-shopping-bags-online-shopping-concept_pytoky.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663908/cropped-image-woman-inputting-card-information-key-phone-laptop-while-shopping-online_l7ioph.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663906/1293_b6kg3u.jpg',
+ 'https://res.cloudinary.com/djrmfg6k9/image/upload/v1724663890/cyber-monday-shopping-sales_d1gjm6.jpg',
];
const Sample: React.FC = () => {
diff --git a/src/components/product/ShopCard.tsx b/src/components/product/ShopCard.tsx
new file mode 100644
index 00000000..a3a53152
--- /dev/null
+++ b/src/components/product/ShopCard.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable */
+import React, { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../../store/store';
+import { fetchProductsByShopId } from '../../store/features/product/shopSlice';
+import { useNavigate } from 'react-router-dom';
+
+interface ShopCardProps {
+ shopId: string;
+}
+
+const ShopCard: React.FC
= ({ shopId }) => {
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+
+ // const shop = useAppSelector((state) => state.shop.shops.find((shop) => shop.id === shopId));
+ // const products = useAppSelector((state) => state.shop.shopProductsByShop?.[shopId] || []);
+ const {
+ shops,
+ shopProductsByShop
+ } = useAppSelector((state: any) => state.shop);
+
+ const shop = shops.find((shop: any) => shop.id === shopId);
+ const products = shopProductsByShop?.[shopId] || [];
+
+ useEffect(() => {
+ if (shopId) {
+ dispatch(fetchProductsByShopId(shopId));
+ }
+ }, [dispatch, shopId]);
+
+ const truncateText = (text: string, length: number) => {
+ return text.length > length ? `${text.substring(0, length)}...` : text;
+ };
+
+ if (products.length < 4) {
+ return null;
+ }
+
+ return (
+
+
{shop?.name || 'Shop Name'}
+
+ {products.slice(0, 4).map((product) => (
+
navigate(`/product/${product.id}`)}
+ >
+
+
{truncateText(product.name, 20)}
+
+ ))}
+
+
+
+ );
+};
+
+export default ShopCard;
diff --git a/src/pages/AboutUs.tsx b/src/pages/AboutUs.tsx
index 2f66d596..d552a161 100644
--- a/src/pages/AboutUs.tsx
+++ b/src/pages/AboutUs.tsx
@@ -25,7 +25,7 @@ export const AboutUs = () => {
image:
"https://res.cloudinary.com/djrmfg6k9/image/upload/v1723551875/SaddockAime1_bqtq7b.jpg",
position: "Full Stack Developer",
- linkedIn: "https://github.com/SaddockAime",
+ linkedIn: "https://www.linkedin.com/in/saddock-kabandana-89b914237/",
github: "https://github.com/SaddockAime",
},
{
diff --git a/src/pages/ProductsByShopPage.tsx b/src/pages/ProductsByShopPage.tsx
new file mode 100644
index 00000000..b66d99d7
--- /dev/null
+++ b/src/pages/ProductsByShopPage.tsx
@@ -0,0 +1,142 @@
+/* eslint-disable */
+import React, { useEffect, useState } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+import { useAppDispatch, useAppSelector } from "../store/store";
+import { fetchProductsByShopId } from "../store/features/product/shopSlice";
+import Product from "../components/product/Product";
+import { PuffLoader } from "react-spinners";
+import { Meta } from "../components/Meta";
+import { toast } from "react-toastify";
+import { createCart, getUserCarts } from "../store/features/carts/cartSlice";
+
+const ProductsByShopPage: React.FC = () => {
+ const { shopId } = useParams<{ shopId: string }>();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const [cartResponseData, setCartResponseData] = useState(null);
+
+ const { shops, shopProductsByShop, isLoadingProducts, isErrorProducts, isSuccessProducts, message } = useAppSelector(
+ (state: any) => state.shop
+ );
+
+ const [priceRange, setPriceRange] = useState([0, 0]);
+ const [maxPrice, setMaxPrice] = useState(0);
+ const [minPrice, setMinPrice] = useState(0);
+ const [visibleProducts, setVisibleProducts] = useState(20);
+
+ useEffect(() => {
+ if (shopId) {
+ dispatch(fetchProductsByShopId(shopId));
+ }
+ }, [dispatch, shopId]);
+
+ const shop = shops.find((shop: any) => shop.id === shopId);
+ const products = shopProductsByShop ? shopProductsByShop[shopId] : [];
+
+ useEffect(() => {
+ if (products && products.length > 0) {
+ const calculatedMaxPrice = products.reduce((max, product) => Math.max(max, product.price), 0);
+ const calculatedMinPrice = products.reduce((min, product) => Math.min(min, product.price), calculatedMaxPrice);
+
+ setMaxPrice(calculatedMaxPrice);
+ setMinPrice(calculatedMinPrice);
+ setPriceRange([calculatedMinPrice, calculatedMaxPrice]);
+ }
+ }, [products]);
+
+ const handleAddProductToCart = async (productId: string, quantity = 1) => {
+ try {
+ const response = await dispatch(
+ createCart({ productId, quantity })
+ ).unwrap();
+
+ if (response.data) {
+ toast.success(response.message);
+ const updatedResponse = await dispatch(getUserCarts()).unwrap();
+ setCartResponseData(updatedResponse.data);
+ } else {
+ toast.error(response.message);
+ }
+ } catch (error: any) {
+ if (error === "Not authorized") {
+ localStorage.setItem("pendingCartProduct", productId);
+ toast.error("Please login first");
+ navigate("/login");
+ } else {
+ toast.error("Something went wrong. Please try again later.");
+ }
+ }
+ };
+
+ const filteredProducts = products.filter((product: any) => {
+ const price = parseFloat(product.price);
+ return price >= priceRange[0] && price <= priceRange[1];
+ });
+
+ const handleLoadMore = () => {
+ setVisibleProducts((prevVisibleProducts) => prevVisibleProducts + 20);
+ };
+
+ return (
+ <>
+
+
+ {isLoadingProducts ? (
+
+ ) : isErrorProducts ? (
+
+
{message || "Something went wrong. Please try again later."}
+
+ ) : (
+
+
+
{shop?.name || 'Shop'}
+
+
+
+
+
+ setPriceRange([priceRange[0], Number(e.target.value)])
+ }
+ />
+ {priceRange[0]}RWF - {priceRange[1]}RWF
+
+
+
+ {isSuccessProducts &&
+ Array.isArray(filteredProducts) &&
+ filteredProducts
+ .slice(0,visibleProducts)
+ .map((product: any) => (
+
+ ))}
+
+ {visibleProducts < products.length && (
+
+
+
+ )}
+
+ )}
+
+ >
+ );
+};
+
+export default ProductsByShopPage;
diff --git a/src/pages/ShopPage.tsx b/src/pages/ShopPage.tsx
new file mode 100644
index 00000000..06d28f35
--- /dev/null
+++ b/src/pages/ShopPage.tsx
@@ -0,0 +1,39 @@
+/* eslint-disable */
+import React, { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../store/store';
+import { fetchAllShops } from '../store/features/product/shopSlice';
+import { Meta } from "../components/Meta";
+import ShopCard from '../components/product/ShopCard';
+import { PuffLoader } from 'react-spinners';
+
+const ShopPage: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const { shops, isLoadingShops, isErrorShops, isSuccessShops } = useAppSelector((state) => state.shop);
+
+ useEffect(() => {
+ dispatch(fetchAllShops());
+ }, [dispatch]);
+
+ return (
+ <>
+
+
+ {isLoadingShops ? (
+
+ ) : isErrorShops ? (
+
Something went wrong. Please try again later.
+ ) : isSuccessShops && shops && shops.length > 0 ? (
+ shops.map((shop) => (
+
+ ))
+ ) : (
+
No shops available
+ )}
+
+ >
+ );
+};
+
+export default ShopPage;
diff --git a/src/router.tsx b/src/router.tsx
index 4f4146eb..e9540b07 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -34,6 +34,8 @@ import { Requests } from "./pages/admin/Requests";
import { ViewRequest } from "./pages/admin/VewRequest";
import { AboutUs } from "./pages/AboutUs";
import { SellerRegistrationPage } from "./pages/seller/SellerRegistrationPage";
+import ShopPage from './pages/ShopPage';
+import ProductsByShopPage from './pages/ProductsByShopPage';
import { Settings } from "./pages/admin/Settings";
const AppRouter: React.FC = () => {
@@ -60,12 +62,12 @@ const AppRouter: React.FC = () => {
-
}
/>
} />
+ } />
+ } />
{
-
}
/>
} />
diff --git a/src/store/features/product/productService.tsx b/src/store/features/product/productService.tsx
index b0a7d7f1..a7f05bc5 100644
--- a/src/store/features/product/productService.tsx
+++ b/src/store/features/product/productService.tsx
@@ -102,6 +102,16 @@ const deleteProduct = async (productId) => {
return response.data;
}
+const fetchAllShops = async () => {
+ const response = await axiosInstance.get(`/api/shop/get-all-shops`);
+ return response.data;
+};
+
+const fetchProductsByShopId = async (id: string) => {
+ const response = await axiosInstance.get(`/api/shop/get-products-by-shop/${id}`);
+ return response.data;
+};
+
const productService = {
fetchProducts,
fetchSingleProduct,
@@ -116,5 +126,7 @@ const productService = {
sellerGetAllProducts,
sellerGetOrderHistory,
deleteProduct,
+ fetchAllShops,
+ fetchProductsByShopId,
}
export default productService;
\ No newline at end of file
diff --git a/src/store/features/product/shopSlice.tsx b/src/store/features/product/shopSlice.tsx
new file mode 100644
index 00000000..fbf82cc6
--- /dev/null
+++ b/src/store/features/product/shopSlice.tsx
@@ -0,0 +1,99 @@
+/* eslint-disable */
+import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
+import productService from "./productService";
+import { IShop, IProduct } from "../../../utils/types/product";
+import { getErrorMessage } from "../../../utils/axios/axiosInstance";
+
+const initialState: {
+ shops: IShop[] | null;
+ shopProductsByShop: { [key: string]: IProduct[] } | null;
+ isLoadingShops: boolean;
+ isErrorShops: boolean;
+ isSuccessShops: boolean;
+ isLoadingProducts: boolean;
+ isErrorProducts: boolean;
+ isSuccessProducts: boolean;
+ message: string;
+} = {
+ shops: [],
+ shopProductsByShop: null,
+ isLoadingShops: false,
+ isErrorShops: false,
+ isSuccessShops: false,
+ isLoadingProducts: false,
+ isErrorProducts: false,
+ isSuccessProducts: false,
+ message: '',
+};
+
+export const fetchAllShops = createAsyncThunk("shop/fetchAllShops", async (_, thunkApi) => {
+ try {
+ const response = await productService.fetchAllShops();
+ return response.data.shops;
+ } catch (error) {
+ return thunkApi.rejectWithValue(getErrorMessage(error));
+ }
+});
+
+export const fetchProductsByShopId = createAsyncThunk("shop/fetchProductsByShopId", async (id, thunkApi) => {
+ try {
+ const response = await productService.fetchProductsByShopId(id);
+ return response.data.products;
+ } catch (error) {
+ return thunkApi.rejectWithValue(getErrorMessage(error));
+ }
+});
+
+const shopSlice = createSlice({
+ name: "shop",
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder
+ // Fetch All Shops
+ .addCase(fetchAllShops.pending, (state) => {
+ state.isLoadingShops = true;
+ state.isErrorShops = false;
+ state.isSuccessShops = false;
+ state.message = ''; // Clear previous messages
+ })
+ .addCase(fetchAllShops.fulfilled, (state, action: PayloadAction) => {
+ state.isLoadingShops = false;
+ state.isSuccessShops = true;
+ state.isErrorShops = false;
+ state.shops = action.payload;
+ })
+ .addCase(fetchAllShops.rejected, (state, action: PayloadAction) => {
+ state.isLoadingShops = false;
+ state.isErrorShops = true;
+ state.isSuccessShops = false;
+ state.message = action.payload;
+ })
+
+ // Fetch Products by Shop ID
+ .addCase(fetchProductsByShopId.pending, (state) => {
+ state.isLoadingProducts = true;
+ state.isErrorProducts = false;
+ state.isSuccessProducts = false;
+ state.message = ''; // Clear previous messages
+ })
+ .addCase(fetchProductsByShopId.fulfilled, (state, action) => {
+ state.isLoadingProducts = false;
+ state.isSuccessProducts = true;
+ state.isErrorProducts = false;
+ const shopId = action.meta.arg;
+ if (!state.shopProductsByShop) {
+ state.shopProductsByShop = {};
+ }
+ state.shopProductsByShop[shopId] = action.payload;
+ })
+ .addCase(fetchProductsByShopId.rejected, (state, action: PayloadAction) => {
+ state.isLoadingProducts = false;
+ state.isErrorProducts = true;
+ state.isSuccessProducts = false;
+ state.message = action.payload;
+ });
+ },
+});
+
+export default shopSlice.reducer;
diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts
index f53fa139..9af15a23 100644
--- a/src/store/reducers/index.ts
+++ b/src/store/reducers/index.ts
@@ -13,6 +13,7 @@ import cartReducer from "../features/carts/cartSlice";
import chatReducer from '../features/chat/chatSlice';
import userReducer from "../features/user/userSlice"
import { CLEAR_IMAGES, RESET_STATE } from '../actions/resetAction';
+import shopReducer from "../features/product/shopSlice";
const appReducer = combineReducers({
initialMessage: welcomeReducer,
@@ -26,7 +27,8 @@ const appReducer = combineReducers({
admin: adminReducer,
cart: cartReducer,
chat: chatReducer,
- user: userReducer
+ user: userReducer,
+ shop: shopReducer
});
const rootReducer = (state, action) => {
diff --git a/src/utils/axios/axiosInstance.ts b/src/utils/axios/axiosInstance.ts
index 322b4095..78d76040 100644
--- a/src/utils/axios/axiosInstance.ts
+++ b/src/utils/axios/axiosInstance.ts
@@ -1,8 +1,8 @@
/* eslint-disable */
import axios from "axios";
import { getToken } from "../protectRoute/ProtectedRoute";
-export const URL = "https://e-commerce-ninjas-platform-backend.onrender.com";
-// export const URL = "http://localhost:5001";
+// export const URL = "https://e-commerce-ninjas-platform-backend.onrender.com";
+export const URL = "http://localhost:5001";
const axiosInstance = axios.create({
baseURL: `${URL}`,
headers: {