Skip to content

Commit

Permalink
Merge pull request #33 from atlp-rwanda/ft-search-187419017
Browse files Browse the repository at this point in the history
#187419017  Users should be able to search
  • Loading branch information
leandreAlly committed Jul 8, 2024
2 parents f8f2479 + 3b35972 commit dc83975
Show file tree
Hide file tree
Showing 15 changed files with 572 additions and 131 deletions.
15 changes: 15 additions & 0 deletions src/@types/SearchType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DynamicData } from './DynamicData';

export type searchState = {
isLoading: boolean;
error: string | null;
searchInputs: searchInputs;
data: DynamicData[];
};

export type searchInputs = {
name?: string | null;
minPrice?: string | null;
maxPrice?: string | null;
categoryName?: string | null;
};
27 changes: 24 additions & 3 deletions src/components/CategoryModel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { useEffect } from 'react';
import { fetchCategories } from '../redux/features/categorySlice';
import { useAppDispatch, useAppSelector } from '../redux/hooks/hooks';
import { ScaleLoader } from 'react-spinners';
import { manipulateSearchInput, search } from '../redux/features/SearchSlice';

interface ModalProps {
openModel: boolean;
setOpenModel: React.Dispatch<React.SetStateAction<boolean>>;
}

const CategoryModel: React.FC<ModalProps> = ({ openModel }) => {
const CategoryModel: React.FC<ModalProps> = ({ openModel, setOpenModel }) => {
const { isLoading, categories } = useAppSelector((state) => state.categories);
const { searchInputs } = useAppSelector((state) => state.search);
const dispatch = useAppDispatch();

useEffect(() => {
Expand All @@ -17,18 +20,36 @@ const CategoryModel: React.FC<ModalProps> = ({ openModel }) => {
}
}, [dispatch, categories]);

const HandleSearch = async (categoryName: string | null) => {
dispatch(manipulateSearchInput({ categoryName }));
};

useEffect(() => {
searchInputs.categoryName && dispatch(search(searchInputs));
}, [dispatch, searchInputs]);

return (
<>
{isLoading ? (
<div className="w-full h-full flex items-center justify-center absolute">
<ScaleLoader color="#256490" />
<ScaleLoader role="progressbar" color="#256490" />
</div>
) : (
<>
{openModel && (
<div className=" absolute bg-[#eff4f8] flex flex-col top-12 right-0 z-10 w-full px-4 py-4 rounded-b-xl">
<div
onClick={() => setOpenModel(!openModel)}
className=" absolute bg-[#eff4f8] flex flex-col top-12 right-0 z-10 w-full px-4 py-4 rounded-b-xl"
>
<span
onClick={() => HandleSearch(null)}
className="a_category a_link p-2 text-xl font-semibold text-primary-lightblue cursor-pointer"
>
All categories
</span>
{categories.map((item, idx) => (
<span
onClick={() => HandleSearch(item.name)}
className="a_category a_link p-2 text-xl font-semibold text-primary-lightblue cursor-pointer"
key={idx}
>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Forms/InputText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface InputProps {
placeholder: string;
otherStyles?: string;
error?: FieldError | undefined;
value?: string | number;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const FormInput = forwardRef(
Expand Down
8 changes: 8 additions & 0 deletions src/components/LandingPageModel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useAppDispatch } from '../redux/hooks/hooks';
import { manipulateSearchInput } from '../redux/features/SearchSlice';

interface ModalProps {
openModel: boolean;
toggleModel: () => void;
}

const LandingPageModel: React.FC<ModalProps> = ({ openModel, toggleModel }) => {
const dispatch = useAppDispatch();
return (
<AnimatePresence>
{openModel && (
Expand All @@ -28,6 +31,11 @@ const LandingPageModel: React.FC<ModalProps> = ({ openModel, toggleModel }) => {
type="text"
className="border-primary-lightblue border-2 inline-block rounded-r-full rounded-l-full h-8 w-full mobile:w-[60%] px-4"
placeholder="Search ..."
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
e.target.value
? dispatch(manipulateSearchInput({ name: e.target.value }))
: dispatch(manipulateSearchInput({ name: null }))
}
/>
<button onClick={toggleModel}>Search</button>
</form>
Expand Down
8 changes: 8 additions & 0 deletions src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ProfileDropdown from './ProfileDropdown';
import { fetchUserProfile } from '../redux/features/userUpdateSlice';
import { useEffect } from 'react';
import Notification from './notification/Notification';
import { manipulateSearchInput } from '../redux/features/SearchSlice';

const Nav = () => {
const accessToken = localStorage.getItem('access_token') || '';
Expand Down Expand Up @@ -61,6 +62,13 @@ const Nav = () => {
type="text"
placeholder="Search..."
className="rounded-l-full rounded-r-full border-2 border-primary-lightblue flex-1 py-1 px-4"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
e.target.value
? dispatch(
manipulateSearchInput({ name: e.target.value }),
)
: dispatch(manipulateSearchInput({ name: null }))
}
/>
<ButtonIcon className="mobile:py-1 px-10"> Search</ButtonIcon>
</form>
Expand Down
202 changes: 144 additions & 58 deletions src/pages/LandingProduct.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { motion } from 'framer-motion';
import depart_icon from '../assets/departments_icon.svg';
import Button from '../components/buttons/Button';
import { FaHeart, FaStar, FaCartPlus, FaCaretDown } from 'react-icons/fa';
Expand All @@ -8,10 +9,20 @@ import { ScaleLoader } from 'react-spinners';
import { DynamicData } from '../@types/DynamicData';
import CategoryModel from '../components/CategoryModel';
import fetchInfo from '../utils/userDetails';
import {
getSearchedProducts,
manipulateSearchInput,
search,
} from '../redux/features/SearchSlice';
import { searchInputs } from '../@types/SearchType';
import FormInput from '../components/Forms/InputText';
import { IoFilter } from 'react-icons/io5';

const LandingProduct = () => {
const { isLoading, products } = useAppSelector((state) => state.product);
const { data, searchInputs } = useAppSelector((state) => state.search);
const [openModel, setOpenModel] = useState(false);
const [openFilter, setOpenFilter] = useState(false);
const dispatch = useAppDispatch();

const toggleModel = () => {
Expand All @@ -26,6 +37,17 @@ const LandingProduct = () => {
}
}, [dispatch, products]);

useEffect(() => {
dispatch(getSearchedProducts(products));
}, [products, dispatch]);

const HandleSearch = async (searchInput: searchInputs) => {
dispatch(manipulateSearchInput(searchInput));
};

useEffect(() => {
dispatch(search(searchInputs));
}, [dispatch, searchInputs]);
return (
<>
<div className="perent_products_container min-h-screen relative">
Expand All @@ -34,82 +56,146 @@ const LandingProduct = () => {
<ScaleLoader color="#256490" role="progressbar" />
</div>
) : (
<div className="products_container flex flex-col items-center laptop:flex-row laptop:items-start pl-[5%] w-full mt-16 laptop:gap-16">
<div className="departments flex p-4 w-[80%] mobile:w-[60%] ipad:w-[40%] laptop:w-[20%]">
<div className="depart_icon w-[90%] bg-primary-lightblue p-2 flex items-center h-12 justify-between gap-2 mt-6 rounded-t-md relative">
<div className="products_container flex flex-col items-center laptop:flex-row laptop:items-start pl-[5%] w-full mt-16 laptop:gap-5">
<div className="departments cursor-pointer flex ipad:p-4 w-[100%] mobile:w-[100%] ipad:w-[40%] laptop:w-[28%]">
<div className="depart_icon z-20 w-[60%] mobile:w-[50%] laptop:w-[90%] bg-primary-lightblue p-2 flex items-center h-12 justify-between gap-2 mt-6 rounded-t-md relative">
<img src={depart_icon} alt="depart_icon" className="w-6 h-10" />
<h1 className="text-md font-semibld text-neutral-white">
All departments
{searchInputs.categoryName
? searchInputs.categoryName
: 'All categories'}
</h1>
<FaCaretDown
className="text-3xl text-neutral-white"
onClick={toggleModel}
/>
<CategoryModel openModel={openModel} />
<CategoryModel
setOpenModel={setOpenModel}
openModel={openModel}
/>
</div>
</div>

<div className="product_card_container grid mobile:grid-cols-2 laptop:grid-cols-3 desktop:grid-cols-4 w-[80%] gap-10 h-full py-10 mr-[5%] place-items-center">
{Array.isArray(products) &&
products.slice(0, 8).map((item: DynamicData, idx: number) => (
<div
className="product_card bg-neutral-white p-4 flex flex-col rounded-md shadow laptop:max-w-[100%] h-full"
key={idx}
>
<div className="card_profile p-2 w-full flex-grow">
<div className="w-full h-48 relative">
<div className="w-full overflow-hidden flex shadow h-48">
<img
src={item.images && item.images[0]}
alt="card_profile"
className="w-full h-full object-cover rounded-lg"
/>
</div>
<div className="cart_icon cart_btn absolute right-3 bottom-3 text-neutral-white bg-primary-lightblue p-2 text-2xl rounded-full flex items-center justify-center cursor-pointer">
<FaCartPlus />
</div>
{item.discount > 0 && (
<div className="discount absolute p-1 rounded bg-action-warning text-neutral-white -right-2 -top-2 font-bold">
{item.discount}%
<div className="mr-[5%] w-full">
<div className=" mobile:relative absolute tablet:absolute laptop:relative w-full top-6 mobile:top-0 tablet:top-6 laptop:top-0 pr-[10%] mobile:pr-0 tablet:pr-[10%] laptop:pr-0 flex justify-end cursor-pointer">
<div
onClick={() => setOpenFilter(!openFilter)}
className=" text-primary-lightblue py-1 w-[30%] h-12 mobile:h-8 tablet:h-12 laptop:h-10 mobile:w-[10%] tablet:w-[20%] laptop:w-[12%] right-0 flex items-center gap-2 justify-center rounded-md border-2 border-primary-lightblue"
>
{' '}
<IoFilter color="#266491" size="20px" />
<p className="text-md">Filters</p>
</div>
</div>
{openFilter && (
<motion.form
initial={{ y: '100%', opacity: 1 }}
animate={{ y: '0%' }}
transition={{ ease: 'easeOut', duration: 1 }}
className=" flex w-full gap-5 py-3"
>
<FormInput
type="number"
placeholder="Minimum price"
otherStyles="h-12 rounded-md pl-3 primary-lightblue"
value={`${searchInputs.minPrice && searchInputs.minPrice}`}
onChange={(e) =>
e.target.value
? HandleSearch({ minPrice: e.target.value })
: HandleSearch({ minPrice: null })
}
/>
<FormInput
type="number"
placeholder="Maximum price"
otherStyles="h-12 rounded-md pl-3 primary-lightblue"
value={`${searchInputs.maxPrice && searchInputs.maxPrice}`}
onChange={(e) =>
e.target.value
? HandleSearch({ maxPrice: e.target.value })
: HandleSearch({ maxPrice: null })
}
/>
</motion.form>
)}
<div
className={` ${!data ? 'flex flex-1 justify-end items-center' : 'grid mobile:grid-cols-2 laptop:grid-cols-3 desktop:grid-cols-4 gap-10'} product_card_container max-h-[95vh] overflow-hidden overflow-y-scroll h-full py-2 place-items-center`}
>
{Array.isArray(data) ? (
data.slice(0, 8).map((item: DynamicData, idx: number) => (
<div
className="product_card bg-neutral-white p-4 flex flex-col rounded-md shadow laptop:max-w-[100%] h-full"
key={idx}
>
<div className="card_profile p-2 w-full flex-grow">
<div className="w-full h-48 relative">
<div className="w-full overflow-hidden flex shadow h-48">
<img
src={item.images && item.images[0]}
alt="card_profile"
className="w-full h-full object-cover rounded-lg"
/>
</div>
)}
</div>
</div>
<div className="card_description pl-2 flex-grow">
<h1 className="py-2">
{item.name.length > 20
? item.name.slice(0, 20) + '...'
: item.name}
</h1>
<div className="ratings flex">
<span className="ml-2 flex items-center gap-2">
<FaStar />
{item.ratings} ratings
</span>
<div className="cart_icon cart_btn absolute right-3 bottom-3 text-neutral-white bg-primary-lightblue p-2 text-2xl rounded-full flex items-center justify-center cursor-pointer">
<FaCartPlus />
</div>
{item.discount > 0 && (
<div className="discount absolute p-1 rounded bg-action-warning text-neutral-white -right-2 -top-2 font-bold">
{item.discount}%
</div>
)}
</div>
</div>
<div className="price_wish flex justify-between items-center mt-2 gap-2 flex-wrap py-2">
<h1 className="text-2xl font-bold">
{item.price}
<small className="text-base font-normal"> RWF</small>
<div className="card_description pl-2 flex-grow">
<h1 className="py-2">
{item.name.length > 20
? item.name.slice(0, 20) + '...'
: item.name}
</h1>
<div className="wish flex items-center cursor-pointer">
<span className="mr-1">add to wish</span>
<FaHeart className=" text-action-error text-2xl cursor-pointer wish_btn" />
<div className="ratings flex">
<span className="ml-2 flex items-center gap-2">
<FaStar />
{item.ratings} ratings
</span>
</div>
<div className="price_wish flex justify-between items-center mt-2 gap-2 flex-wrap py-2">
<h1 className="text-2xl font-bold">
{item.price}
<small className="text-base font-normal">
{' '}
RWF
</small>
</h1>
<div className="wish flex items-center cursor-pointer">
<span className="mr-1">add to wish</span>
<FaHeart className=" text-action-error text-2xl cursor-pointer wish_btn" />
</div>
</div>
</div>
<div className="btn flex justify-center">
<Button
title="preview product"
url={`/products/${item.id}`}
otherStyles={
' rounded-l-full rounded-r-full mt-2 font-semibold'
}
buttonType={'button'}
/>
</div>
</div>
<div className="btn flex justify-center">
<Button
title="preview product"
url={`/products/${item.id}`}
otherStyles={
' rounded-l-full rounded-r-full mt-2 font-semibold'
}
buttonType={'button'}
/>
))
) : (
<div className="w-full h-64 flex items-center justify-center">
<div className=" text-primary-lightblue">
<h1 className="text-xl text-center">No results found</h1>
<p>
It seems we can not find any results based on your
search.
</p>
</div>
</div>
))}
)}
</div>
</div>
</div>
)}
Expand Down
Loading

0 comments on commit dc83975

Please sign in to comment.