Skip to content

Commit

Permalink
ft-shop-page (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaddockAime authored Aug 26, 2024
1 parent cb78f72 commit 85a82b1
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 17 deletions.
4 changes: 3 additions & 1 deletion src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@
@import "./assets/styles/SellerRegistration.scss";
@import "./assets/styles/ServicesPage.scss";
@import "./assets/styles/Settings.scss";
@import "./assets/styles/HomePage.scss";
@import "./assets/styles/HomePage.scss";
@import "./assets/styles/ShopCard.scss";
@import "./assets/styles/ShopPage.scss";
57 changes: 57 additions & 0 deletions src/assets/styles/ShopCard.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

13 changes: 13 additions & 0 deletions src/assets/styles/ShopPage.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 2 additions & 2 deletions src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ function Footer() {
</a>
<span className="footer__text">Email:</span>
<a
href="mailto:ecommerceninjas@gmail.com"
href="mailto:ecommerceninjas45@gmail.com"
className="footer__link"
>
ecommerceninjas@gmail.com
ecommerceninjas45@gmail.com
</a>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const Header: React.FC = () => {
<IoMdMailUnread className="header__icon" />
</div>
<p className="header__text">Email us</p>
<p className="header__description">support@ecommerce-ninjas.com</p>
<p className="header__description">ecommerceninjas45@gmail.com</p>
</div>
<div className="header__box header__contact">
<FaPhoneVolume className="header__icon" />
Expand Down
14 changes: 7 additions & 7 deletions src/components/layout/Sample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
64 changes: 64 additions & 0 deletions src/components/product/ShopCard.tsx
Original file line number Diff line number Diff line change
@@ -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<ShopCardProps> = ({ 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 (
<div className="shop-card">
<h2>{shop?.name || 'Shop Name'}</h2>
<div className="items">
{products.slice(0, 4).map((product) => (
<div
key={product.id}
className="item"
onClick={() => navigate(`/product/${product.id}`)}
>
<img src={product.images[0]} alt={product.name} />
<p>{truncateText(product.name, 20)}</p>
</div>
))}
</div>
<button
className="see-more"
onClick={() => navigate(`/shops/${shopId}/products`)}
>
See more
</button>
</div>
);
};

export default ShopCard;
2 changes: 1 addition & 1 deletion src/pages/AboutUs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{
Expand Down
142 changes: 142 additions & 0 deletions src/pages/ProductsByShopPage.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(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<number>(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 (
<>
<Meta title={`Products - Shop ${shop?.name || 'Shop'}`} />
<div className="landing-container">
{isLoadingProducts ? (
<div className="loader">
<PuffLoader color="#ff6d18" size={300} loading={isLoadingProducts} />
</div>
) : isErrorProducts ? (
<div className="error-message">
<p>{message || "Something went wrong. Please try again later."}</p>
</div>
) : (
<div>
<div className="head">
<h1>{shop?.name || 'Shop'}</h1>
</div>
<div className="filters">
<div>
<label>Price Range: </label>
<input
type="range"
min={minPrice}
max={maxPrice}
value={priceRange[1]}
onChange={(e) =>
setPriceRange([priceRange[0], Number(e.target.value)])
}
/>
<span className="span">{priceRange[0]}RWF - {priceRange[1]}RWF</span>
</div>
</div>
<div className="product-list">
{isSuccessProducts &&
Array.isArray(filteredProducts) &&
filteredProducts
.slice(0,visibleProducts)
.map((product: any) => (
<Product
key={product.id}
id={product.id}
images={product.images}
name={product.name}
price={product.price}
stock={Number(product.quantity)}
description={product.description}
discount={Number(product.discount.replace("%", ""))}
/>
))}
</div>
{visibleProducts < products.length && (
<div className="load-more">
<button onClick={handleLoadMore}>Load More</button>
</div>
)}
</div>
)}
</div>
</>
);
};

export default ProductsByShopPage;
39 changes: 39 additions & 0 deletions src/pages/ShopPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Meta title="Shops - E-Commerce Ninjas" />
<div className="shop-container">
{isLoadingShops ? (
<div className="loader">
<PuffLoader color="#ff6d18" size={300} loading={isLoadingShops} />
</div>
) : isErrorShops ? (
<p>Something went wrong. Please try again later.</p>
) : isSuccessShops && shops && shops.length > 0 ? (
shops.map((shop) => (
<ShopCard key={shop.id} shopId={shop.id} />
))
) : (
<p>No shops available</p>
)}
</div>
</>
);
};

export default ShopPage;
Loading

0 comments on commit 85a82b1

Please sign in to comment.