Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ft-shop-page #84

Merged
merged 1 commit into from
Aug 26, 2024
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
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
Loading