Skip to content

Commit

Permalink
ft(search):A buyer should be able to search for products
Browse files Browse the repository at this point in the history
  • Loading branch information
PrinceRWIGIMBA committed Jul 2, 2024
1 parent 5e6047c commit faef270
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 191 deletions.
78 changes: 78 additions & 0 deletions src/__tests__/landingpage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import Page from '@/app/products/page';
import * as nextNavigation from 'next/navigation';
import request from '@/utils/axios';

// Mock dependencies
jest.mock('next/navigation', () => ({
useSearchParams: jest.fn(),
}));

jest.mock('@/utils/axios', () => ({
get: jest.fn(),
}));

jest.mock('@/components/Side', () => () => <div>Side Component</div>);
jest.mock('@/components/Header', () => () => <div>Header Component</div>);
jest.mock('@/components/Footer', () => () => <div>Footer Component</div>);
jest.mock('@/components/ProductList', () => ({ searchResults }: { searchResults: any[] }) => (
<div>Product List with {searchResults.length} results</div>
));

describe('Page Component', () => {
it('renders without crashing and fetches search results', async () => {
const mockSearchParams = {
toString: jest.fn().mockReturnValue(''),
};
const mockResponse = [{ id: 1, name: 'Product 1' }, { id: 2, name: 'Product 2' }];

(nextNavigation.useSearchParams as jest.Mock).mockReturnValue(mockSearchParams);
(request.get as jest.Mock).mockResolvedValue(mockResponse);

render(<Page />);

// Wait for search results to be fetched and rendered
await waitFor(() => {
expect(screen.getByText('Header Component')).toBeInTheDocument();
expect(screen.getByText('Footer Component')).toBeInTheDocument();
expect(screen.getByText('Product List with 2 results')).toBeInTheDocument();
expect(screen.getByText('Side Component')).toBeInTheDocument();
});

// Verify that the select options are rendered
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Popular' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Recent' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Clothes' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Electronics' })).toBeInTheDocument();
});

it('handles error in fetching search results', async () => {
const mockSearchParams = {
toString: jest.fn().mockReturnValue(''),
};
const mockError = new Error('Network Error');

(nextNavigation.useSearchParams as jest.Mock).mockReturnValue(mockSearchParams);
(request.get as jest.Mock).mockRejectedValue(mockError);

render(<Page />);

// Wait for error handling
await waitFor(() => {
expect(screen.getByText('Header Component')).toBeInTheDocument();
expect(screen.getByText('Footer Component')).toBeInTheDocument();
expect(screen.getByText('Product List with 0 results')).toBeInTheDocument();
expect(screen.getByText('Side Component')).toBeInTheDocument();
});

// Verify that the select options are rendered
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Popular' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Recent' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Clothes' })).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'Electronics' })).toBeInTheDocument();
});
});
27 changes: 1 addition & 26 deletions src/__tests__/productList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
import SideBar from "@/components/Side"
import Provider from '@/app/providers';
import Page from '@/app/products/page';


const queryClient = new QueryClient();
const ProductListTest = () => {
Expand All @@ -25,28 +24,4 @@ describe('ProductList', () => {
);
expect(await findByText('test')).toBeInTheDocument();
});
it('renders Page without crashing', async () => {
const { findByText } = render(
<Provider>
<Page />
</Provider>
);
expect(await findByText('All Products')).toBeInTheDocument();
});
it('renders single Page without crashing', async () => {
const { findByText } = render(
<Provider>
<Page />
</Provider>
);
expect(await findByText('All Products')).toBeInTheDocument();
});
it('should render productList', async ()=> {
const { getByText } = render(
<QueryClientProvider client={queryClient}>
<SideBar />
</QueryClientProvider>
);
expect(getByText('Filter Product')).toBeInTheDocument()
})
});
115 changes: 95 additions & 20 deletions src/app/products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,121 @@
'use client';
import React from 'react';
'use client'
import React, { useState, useEffect, Suspense } from 'react';
import { AiOutlineSearch } from 'react-icons/ai';
import Side from '../../components/Side';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ProductList from '@/components/ProductList';
import { checkIsAdmin } from '@/components/isAdmin';
import { useSearchParams } from 'next/navigation';
import request from '@/utils/axios';

function SuspenseWrapper() {
const searchParams = useSearchParams();
const [Value, setValue] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [searchQuery, setSearchQuery] = useState('');

function Page() {
// const isAdmin = checkIsAdmin();

// if (isAdmin) {
// console.log('User is an admin!');
// } else {
// console.log('User is not an admin.');
// }
useEffect(() => {
const fetchSearchResults = async () => {
const queryParams = new URLSearchParams(searchParams.toString());
if (searchQuery) {
queryParams.set('query', searchQuery);
}
try {
const response:any = await request.get(`/search?${queryParams.toString()}`);
setSearchResults(response);
} catch (error) {
console.error('Error fetching search results:', error);
}
};

fetchSearchResults();
}, [searchParams, searchQuery]);

const handleSearch = async (e: { preventDefault: () => void; }) => {
e.preventDefault();
console.log("Search query:", Value);

try {
let queryParams: any = {};
const trimmedValue = Value.trim();
const numericValue = parseFloat(trimmedValue);

if (!isNaN(numericValue)) {
queryParams.minPrice = numericValue;
}
else if (!isNaN(numericValue)) {
queryParams.maxPrice = numericValue; }
else if (!isNaN(numericValue)) {
queryParams.minPrice = numericValue;
queryParams.maxPrice = numericValue;
} else if(trimmedValue.length > 0 ) {

queryParams.name = Value;

}else if(trimmedValue.length > 0 ) {

} else if(trimmedValue.length > 0 ) {

queryParams.name = Value;
queryParams.category = Value;
}

const queryString = new URLSearchParams(queryParams).toString();
console.log(queryString)
const url = `/search?${new URLSearchParams(queryParams).toString()}`;

const response:any = await request.get(url);

setSearchResults(response);

}
catch (error) {
console.error('Error fetching search results:', error);
}
};

return (
<>
<Header/>

<div className="w-full h-[50%] flex flex-col px-10 py-5">
<div className="w-full flex justify-between px-10">
<h1>All Products</h1>
<div className="flex self-end">
<select name="" id="" className="border">
<option value="volvo">Popular</option>
<option value="saab">Recent</option>
<option value="opel">Clothes</option>
<option value="audi">Electronics</option>
<form onSubmit={handleSearch} className="flex items-center">
<div className="relative flex items-center border px-2 py-1">
<input type="text" name="search" placeholder="Search products" className="border-none outline-none"
value={Value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit" className="flex items-center justify-center bg-transparent text-[#8F8F8F] absolute right-2">
<AiOutlineSearch size={24} />
</button>
</div>
</form>
<select name="" id="" className="border ml-2">
<option value="popular">Popular</option>
<option value="recent">Recent</option>
<option value="clothes">Clothes</option>
<option value="electronics">Electronics</option>
</select>
</div>
</div>
<div className="w-full flex mx-auto px-10">
<Side />
<ProductList />
<div className="flex flex-wrap">
<ProductList searchResults={searchResults} />
</div>
</div>
</div>
<Footer />

</>
);
}

export default Page;
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SuspenseWrapper />
</Suspense>
);
}
87 changes: 38 additions & 49 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import { averageReviews } from '@/utils/averageReviews';
import { RootState, useAppDispatch, useAppSelector } from '@/redux/store';
import { handleUserAddCart } from '@/redux/slices/userCartSlice';

// import { useRouter } from 'next/router';

function Card({
productName,
productDescription,
Expand All @@ -20,63 +18,54 @@ function Card({
id,
reviews,
}: Cards) {

const productId=id

const dispatch = useAppDispatch();
const handleNewItem=()=>{
const productId = id;
const dispatch = useAppDispatch();
const handleNewItem = () => {
dispatch(handleUserAddCart({ productPrice, productId }));
}

dispatch(handleUserAddCart({productPrice,productId}));
}
return (
<div className="w-full max-w-[80%] sm:max-w-48 sm:mb-10 min-w-[200px] mr-3 ml-0 my-3 sm:h-[17rem] h-[19rem] flex flex-col bg-white border border-gray-100 shadow relative">
<div className="flex justify-center h-[180px]">
{productThumbnail && productThumbnail.length > 0 ? (
<img
src={productThumbnail}
alt="default image"
className="w-full h-[150px] text-[12px]"
<div className="relative w-full max-w-[80%] sm:max-w-48 sm:mb-10 min-w-[200px] mx-3 my-6 sm:h-[17rem] h-[19rem] bg-white border border-gray-200 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 ease-in-out transform hover:scale-105">
<Link href={`/products/${id}`}>
<div className="overflow-hidden p-3">
<img
src={productThumbnail || './force.png'}
alt={productName}
className="w-full h-32 object-contain transition-transform duration-300"
/>
) : (
<img src="./force.png" alt={'no image found'} />
)}
</div>
<div className="sm:px-4 flex flex-col gap-1">
<h5 className="max-w-1xl sm:text-[12px] text-[30px] sm:text-left sm:mx-0 mx-3 w font-semibold tracking-tight text-black-900">
{productName.length < 30
? productName
: productName.substring(0, 30) + '...'}
</h5>
<div className="block text-[12px] text-muted">
{productDescription.length < 50
? productDescription
: productDescription.substring(0, 50) + '...'}
</div>
<div className="flex items-center justify-between pb-3">
<span className="text-1xl sm:m-0 m-3 font-bold text-green-400">
</Link>
<div className="px-3 pb-3">
<h5 className="text-sm font-semibold mb-1 text-gray-900 truncate">
{productName.length < 30 ? productName : `${productName.substring(0, 30)}...`}
</h5>
<p className="text-xs text-gray-700 mb-2 truncate">
{productDescription.length < 50 ? productDescription : `${productDescription.substring(0, 50)}...`}
</p>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-green-500">
{productPrice} RWF
</span>
<span className="block">
<ReactStars
count={5}
value={averageReviews(reviews)}
isHalf={true}
size={13}
activeColor="#ffd700"
edit={false}
/>
</span>
<ReactStars
count={5}
value={averageReviews(reviews)}
isHalf={true}
size={14}
activeColor="#ffd700"
edit={false}
/>
</div>
<div className="flex justify-end space-x-4">
<Link href={`/products/${id}`}>
<MdOutlineRemoveRedEye className="text-gray-600 hover:text-blue-500 cursor-pointer" size={20} />
</Link>
<FaRegHeart className="text-gray-600 hover:text-red-500 cursor-pointer" size={20} />
<MdOutlineShoppingCart className="text-gray-600 hover:text-green-500 cursor-pointer" size={20} onClick={handleNewItem} />
</div>
</div>
<div className="absolute inset-0 flex justify-center items-center opacity-0 hover:opacity-100 transition duration-300 ease-in-out bg-gray-200 bg-opacity-75">
<Link href={`/products/${id}`}>
<MdOutlineRemoveRedEye className="text-gray-700 mr-4 hover:text-blue-400 cursor-pointer" />
</Link>
<FaRegHeart className="text-gray-700 mr-4 hover:text-red-500 cursor-pointer" />
<MdOutlineShoppingCart className="text-gray-700 hover:text-green-500 cursor-pointer" onClick={() =>{handleNewItem()}} />
</div>
</div>
);
}

export default Card;
export default Card;
Loading

0 comments on commit faef270

Please sign in to comment.