diff --git a/package-lock.json b/package-lock.json
index 03205a6..33641f3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -149,6 +149,12 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.10.tgz",
"integrity": "sha512-73xxTSYJNKfiJ7C1Ajg+sz5l8y+blb/vNgHYg7O3yem5zLBnfPpidJ1UGg4W4d2Y+jwUVJbZKh8SKJarqAJVUQ==",
"peer": true,
+ "node_modules/@angular/common": {
+ "version": "6.1.10",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-6.1.10.tgz",
+ "integrity": "sha512-73xxTSYJNKfiJ7C1Ajg+sz5l8y+blb/vNgHYg7O3yem5zLBnfPpidJ1UGg4W4d2Y+jwUVJbZKh8SKJarqAJVUQ==",
+ "license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^1.9.0"
},
@@ -161,6 +167,7 @@
"version": "6.1.10",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-6.1.10.tgz",
"integrity": "sha512-61l3rIQTVdT45eOf6/fBJIeVmV10mcrxqS4N/1OWkuDT29YSJTZSxGcv8QjAyyutuhcqWWpO6gVRkN07rWmkPg==",
+ "license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^1.9.0"
@@ -170,6 +177,19 @@
"zone.js": "~0.8.26"
}
},
+ "node_modules/@aw-web-design/x-default-browser": {
+ "version": "1.4.126",
+ "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz",
+ "integrity": "sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "6.1.10",
+ "rxjs": "^6.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
@@ -29572,4 +29592,5 @@
"peer": true
}
}
+ }
}
diff --git a/public/product-default.png b/public/product-default.png
new file mode 100644
index 0000000..1a0d06d
Binary files /dev/null and b/public/product-default.png differ
diff --git a/src/__tests__/landingpage.test.tsx b/src/__tests__/landingpage.test.tsx
new file mode 100644
index 0000000..6673262
--- /dev/null
+++ b/src/__tests__/landingpage.test.tsx
@@ -0,0 +1,67 @@
+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', () => () =>
Side Component
);
+jest.mock('@/components/Header', () => () => Header Component
);
+jest.mock('@/components/Footer', () => () => Footer Component
);
+jest.mock('@/components/ProductList', () => ({ searchResults }: { searchResults: any[] }) => (
+ Product List with {searchResults.length} results
+));
+
+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();
+
+ // 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();
+ });
+
+ });
+
+ 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();
+
+ // 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();
+ });
+
+
+ });
+});
diff --git a/src/__tests__/productList.test.tsx b/src/__tests__/productList.test.tsx
index c2e0e12..938e091 100644
--- a/src/__tests__/productList.test.tsx
+++ b/src/__tests__/productList.test.tsx
@@ -37,29 +37,5 @@ describe('ProductList', () => {
,
);
expect(await findByText('test')).toBeInTheDocument();
- });
- it('renders Page without crashing', async () => {
- const { findByText } = render(
-
-
- ,
- );
- expect(await findByText('All Products')).toBeInTheDocument();
- });
- it('renders single Page without crashing', async () => {
- const { findByText } = render(
-
-
- ,
- );
- expect(await findByText('All Products')).toBeInTheDocument();
- });
- it('should render productList', async () => {
- const { getByText } = render(
-
-
- ,
- );
- expect(getByText('Filter Product')).toBeInTheDocument();
- });
+});
});
diff --git a/src/__tests__/reset.test.tsx b/src/__tests__/reset.test.tsx
index b2e00ce..449ad5c 100644
--- a/src/__tests__/reset.test.tsx
+++ b/src/__tests__/reset.test.tsx
@@ -61,4 +61,4 @@ describe('ResetPassword component', () => {
);
expect(getByText("Please insert your new password you'd like to use")).toBeInTheDocument();
});
-});
\ No newline at end of file
+});
diff --git a/src/__tests__/userCart.test.tsx b/src/__tests__/userCart.test.tsx
new file mode 100644
index 0000000..9ab0d80
--- /dev/null
+++ b/src/__tests__/userCart.test.tsx
@@ -0,0 +1,50 @@
+import { renderHook } from '@testing-library/react';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { useDispatch } from 'react-redux';
+import { useQueryClient } from '@tanstack/react-query';
+import { handleFetchUserCart, handleCartInfoManipulation } from '@/hooks/userCart';
+import { ProductType } from '@/types/Product';
+
+// Mocking useDispatch and useQueryClient
+jest.mock('react-redux', () => ({
+ useDispatch: jest.fn(),
+}));
+
+jest.mock('@tanstack/react-query', () => ({
+ useQueryClient: jest.fn(),
+ useQuery: jest.fn(),
+}));
+
+
+describe('handleCartInfoManipulation', () => {
+ it('manipulates cart information correctly', () => {
+ const mockCart: any = {
+ product: [{ product: '1', quantity: 2 }],
+ };
+ const mockProducts: ProductType[] = [
+ {
+ id: '1', productName: 'Product 1', productPrice: 10, productDescription: 'Description 1', productThumbnail: 'thumb1.jpg',
+ productCurrency: '',
+ productPictures: [],
+ reviews: [],
+ related: undefined
+ },
+ {
+ id: '2', productName: 'Product 2', productPrice: 20, productDescription: 'Description 2', productThumbnail: 'thumb2.jpg',
+ productCurrency: '',
+ productPictures: [],
+ reviews: [],
+ related: undefined
+ },
+ ];
+
+ const manipulatedCart = handleCartInfoManipulation(mockCart, mockProducts);
+
+ expect(manipulatedCart).toHaveLength(1);
+ expect(manipulatedCart[0].name).toEqual('Product 1');
+ expect(manipulatedCart[0].unitPrice).toEqual(10);
+ expect(manipulatedCart[0].description).toEqual('Description 1');
+ expect(manipulatedCart[0].thumbnail).toEqual('thumb1.jpg');
+ });
+});
\ No newline at end of file
diff --git a/src/app/products/[id]/page.tsx b/src/app/products/[id]/page.tsx
index 09bcad4..474d593 100644
--- a/src/app/products/[id]/page.tsx
+++ b/src/app/products/[id]/page.tsx
@@ -230,4 +230,4 @@ function Page() {
);
}
-export default Page;
+export default Page;
\ No newline at end of file
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
index 9441974..ba14ff6 100644
--- a/src/app/products/page.tsx
+++ b/src/app/products/page.tsx
@@ -1,46 +1,112 @@
-'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('');
+ const [searched, setSearched] = useState(false); // New state for tracking if a search has been performed
-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);
+ setSearched(true);
+ } 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) {
+ 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);
+ setSearched(true);
+ } catch (error) {
+ console.error('Error fetching search results:', error);
+ }
+ };
return (
<>
-
-
-
-
-
All Products
-
-
+
+
+
-
+
-
+
+ {searched && searchResults.length === 0 ? (
+
No product found
+ ) : (
+
+ )}
+
-
>
);
}
-export default Page;
+export default function Page() {
+ return (
+
Loading...}>
+
+
+ );
+}
diff --git a/src/components/Card.tsx b/src/components/Card.tsx
index e48a4ef..48cab15 100644
--- a/src/components/Card.tsx
+++ b/src/components/Card.tsx
@@ -5,13 +5,12 @@ import ReactStars from 'react-rating-stars-component';
import { FaRegHeart } from 'react-icons/fa6';
import { Cards } from '../types/Product';
import image from '../../public/product.png';
+import defaultProductImage from '../../public/product-default.png';
import Link from 'next/link';
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,
@@ -20,35 +19,36 @@ function Card({
id,
reviews,
}: Cards) {
- const productId = id;
+ const productId = id;
const dispatch = useAppDispatch();
const handleNewItem = () => {
dispatch(handleUserAddCart({ productPrice, productId }));
+ }
+ const handleImageError = (event: React.SyntheticEvent
) => {
+ event.currentTarget.src = defaultProductImage.src;
};
return (
-
-
-
(e.currentTarget.src = '/product.png')}
- alt="default image"
- className="w-full h-[150px] text-[12px] object-cover"
- />
-
-
-
- {productName.length < 30
- ? productName
- : productName.substring(0, 30) + '...'}
-
-
- {productDescription.length < 50
- ? productDescription
- : productDescription.substring(0, 50) + '...'}
+
+
+
+
-
-
+
+
+
+ {productName.length < 30 ? productName : `${productName.substring(0, 30)}...`}
+
+
+ {productDescription.length < 50 ? productDescription : `${productDescription.substring(0, 50)}...`}
+
+
+
{productPrice} RWF
@@ -62,21 +62,17 @@ function Card({
/>
-
-
-
-
-
-
-
{
- handleNewItem();
- }}
- />
+
+
+
+
+
+
+
);
}
export default Card;
+
diff --git a/src/components/ProductList.tsx b/src/components/ProductList.tsx
index c4a1511..301ee4d 100644
--- a/src/components/ProductList.tsx
+++ b/src/components/ProductList.tsx
@@ -9,11 +9,13 @@ import { unstable_noStore as noStore } from 'next/cache';
import { SkeletonProduct } from '@/components/Skeleton';
import { storeAllProduct } from '@/redux/slices/userCartSlice';
import { useAppDispatch, useAppSelector } from '../redux/store';
+import defaultProductImage from '../../public/product-default.png';
interface ProdductProps {
activeNav?: String;
+ searchResults: ProductObj[];
}
-const ProductList: React.FC
= ({ activeNav }) => {
+const ProductList: React.FC = ({ activeNav,searchResults }) => {
const [activeButton, setActiveButton] = useState(1);
const fetchUrl = (nav: string) => {
@@ -88,7 +90,7 @@ const ProductList: React.FC = ({ activeNav }) => {
setActiveButton((prev) => (prev === 1 ? prev : prev - 1));
};
if (isLoading) {
- return {/* */}
;
+ return
;
}
if (error) {
@@ -97,16 +99,18 @@ const ProductList: React.FC = ({ activeNav }) => {
;
}
- if (!data) {
+ if (!data && !searchResults.length) {
return
No products found.
;
}
+ const productsToDisplay = searchResults.length ? searchResults : data;
+ console.log(searchResults)
return (
-
+
{data && (
<>
- {data.map(
+ {productsToDisplay.map(
(
product: {
id: string;
@@ -122,7 +126,7 @@ const ProductList: React.FC = ({ activeNav }) => {
key={i.toString()}
id={product.id}
productPrice={product.productPrice}
- productThumbnail={product.productThumbnail}
+ productThumbnail={product.productThumbnail || defaultProductImage.src}
productDescription={product.productDescription}
productName={product.productName}
reviews={product.reviews}
diff --git a/src/components/Side.tsx b/src/components/Side.tsx
index b6b86cf..455acd9 100644
--- a/src/components/Side.tsx
+++ b/src/components/Side.tsx
@@ -1,45 +1,129 @@
-import React from 'react'
-import Image from 'next/image'
-import sideImage from "../../public/Rectangle 1403.png"
+'use client';
+import React, { useState, useEffect } from 'react';
+import Image from 'next/image';
+import sideImage from "../../public/Rectangle 1403.png";
+import request from '@/utils/axios';
+import { CategoryResponse, ProductObj } from '@/types/Product';
+import { useRouter } from 'next/navigation';
function Side() {
-return (
- <>
-
-
-
Filter Product
-
-
Categories
-
Foods
-
Property
-
Shoes
-
Price
-
-
-
-
Size
-
-
small
-
large
+ const [categories, setCategories] = useState
([]);
+ const [products, setProducts] = useState([]);
+ const [minPrice, setMinPrice] = useState('');
+ const [maxPrice, setMaxPrice] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('');
+ const [selectedProduct, setSelectedProduct] = useState('');
+
+ const router = useRouter();
+
+ useEffect(() => {
+ async function fetchCategories() {
+ try {
+ const allCategories: any = await request.get('/categories');
+ setCategories(allCategories.categories || []);
+ } catch (error) {
+ console.error('Error fetching categories:', error);
+ }
+ }
+
+ async function fetchProducts() {
+ try {
+ const allProducts: any = await request.get(`/products`);
+ setProducts(allProducts.products || []);
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ }
+ }
+
+ fetchCategories();
+ fetchProducts();
+ }, []);
+
+ useEffect(() => {
+ handleSearchParams();
+ }, [minPrice, maxPrice, selectedCategory, selectedProduct]);
+
+ const handleSearchParams = () => {
+ if (minPrice || maxPrice || selectedCategory || selectedProduct) {
+ const queryParams: { [key: string]: string } = {};
+ if (minPrice) queryParams.minPrice = minPrice;
+ if (maxPrice) queryParams.maxPrice = maxPrice;
+ if (selectedCategory) queryParams.category = selectedCategory;
+ if (selectedProduct) queryParams.name = selectedProduct;
+
+ const queryString = new URLSearchParams(queryParams).toString();
+ console.log('Query Params:', queryString); // Debugging log
+ router.push(`/products?${queryString}`);
+ }
+ };
+
+ const handleCategoryClick = (category: string) => {
+ setSelectedCategory(category);
+ setSelectedProduct('');
+ };
+
+ const handleProductClick = (product: string) => {
+ setSelectedProduct(product);
+ setSelectedCategory('');
+ };
+
+ const handleMinPriceChange = (e: React.ChangeEvent) => {
+ setMinPrice(e.target.value);
+ };
+
+ const handleMaxPriceChange = (e: React.ChangeEvent) => {
+ setMaxPrice(e.target.value);
+ };
+
+ return (
+
+
+
Filter Product
+
+
Categories
+ {categories.length > 0 ? (
+
+ {categories.map((category: any) => (
+
handleCategoryClick(category.id)}
+ >
+ {category.categoryName}
+
+ ))}
+
+ ) : (
+
No categories available
+ )}
+
Price
-
-
-
+
- >
-)
+ );
}
-export default Side
\ No newline at end of file
+export default Side;
diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx
index d1752ba..9533a40 100644
--- a/src/components/Skeleton.tsx
+++ b/src/components/Skeleton.tsx
@@ -66,29 +66,14 @@ export const SideSkeleton = () => {
return (
<>
diff --git a/src/components/allProducts.tsx b/src/components/allProducts.tsx
index 8acc560..f46edfc 100644
--- a/src/components/allProducts.tsx
+++ b/src/components/allProducts.tsx
@@ -1,15 +1,23 @@
'use client';
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect } from 'react';
import ProductList from './ProductList';
+import { AiOutlineSearch } from 'react-icons/ai';
+import { useRouter } from 'next/navigation';
+import request from '@/utils/axios';
+
export const ProductWithFilter = () => {
const [Value, setValue] = useState('');
const [locarstorage, SetLocal] = useState(null);
const [activeButton, setActiveButton] = useState('All');
+ const [searchResults, setSearchResults] = useState([]);
+ const [searched, setSearched] = useState(false); // New state for tracking if a search has been performed
+ const router = useRouter();
const Options = [
{ laber: 'All', value: 1 },
{ laber: 'Rating', value: 2 },
{ laber: 'wishes', value: 3 },
];
+
useEffect(() => {
if (typeof window !== 'undefined') {
const storedToken: string | any = localStorage.getItem('profile');
@@ -17,17 +25,56 @@ export const ProductWithFilter = () => {
SetLocal(ifuser?.User?.Role.name as any);
}
}, []);
+
const handleSelect = (event: React.ChangeEvent) => {
setValue(event.target.value);
setActiveButton(event.target.value);
};
+
const handleButtonClick = (buttonName: string) => {
setActiveButton(buttonName);
};
+
const buttonClass = (buttonName: string) =>
`px-4 py-2 ${
activeButton === buttonName ? 'bg-black text-white' : ''
} focus:outline-none `;
+
+ 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) {
+ 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);
+ setSearched(true); // Set searched to true when a search is performed
+ } catch (error) {
+ console.error('Error fetching search results:', error);
+ }
+ };
+
return (
@@ -90,20 +137,28 @@ export const ProductWithFilter = () => {
>
)}
{/* Search */}
-
-
-
-
+
+
{/* Product Section */}
-
-
+
+ {searched && searchResults.length === 0 ? (
+
No product found
+ ) : (
+
+ )}
diff --git a/src/types/Product.ts b/src/types/Product.ts
index ff75c36..c254515 100644
--- a/src/types/Product.ts
+++ b/src/types/Product.ts
@@ -7,14 +7,14 @@ export interface Seller {
phone?: string;
}
export interface ReviewType {
- buyerId:string;
+ buyerId: string;
rating: number;
feedback: string;
- userProfile:{
- firstName:string;
- lastName:string;
- profileImage:string;
- }
+ userProfile: {
+ firstName: string;
+ lastName: string;
+ profileImage: string;
+ };
}
export interface imageType {
imgId: string;
@@ -34,35 +34,46 @@ export interface ProductType {
productDescription: string;
productDiscount?: number;
productName: string;
- productPictures: imageType[]
+ productPictures: imageType[];
productPrice: number;
productThumbnail: string;
reviews: ReviewType[];
related: any;
seller?: Seller;
- sellerId?: string;
+ sellerId?: string;
stockLevel?: number;
updatedAt?: number;
- }
-
- export interface ProductObj {
- currentPage: number;
- products: ProductType[];
- totalItems: number;
- toatalPages: number;
- }
+}
+
+export interface ProductObj {
+ currentPage: number;
+ products: ProductType[];
+ totalItems: number;
+ toatalPages: number;
+}
+
+export interface Cards {
+ key: string;
+ id: string;
+ productPrice: number;
+ productThumbnail: string;
+ productDescription: string;
+ reviews: ReviewType[];
+ productName: string;
+}
+export interface TableType {
+ product: string;
+ index: number;
+ id: string;
+}
+export interface CategoryType {
+ id: string;
+ categoryName: string;
+ createdAt: string;
+ updatedAt: string;
+}
- export interface Cards {
- key: string;
- id: string;
- productPrice: number;
- productThumbnail: string;
- productDescription: string;
- reviews:ReviewType[];
- productName:string;
- }
- export interface TableType {
- product: string;
- index: number;
- id:string;
- }
+export interface CategoryResponse {
+ message: string;
+ categories: CategoryType[];
+}