diff --git a/src/__test__/popular/MostPopular.test.tsx b/src/__test__/popular/MostPopular.test.tsx new file mode 100644 index 00000000..9a189478 --- /dev/null +++ b/src/__test__/popular/MostPopular.test.tsx @@ -0,0 +1,191 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import MostPopular from '@/components/Popular/MostPopular'; +import Product from '../../interfaces/product'; +import Category from '../../interfaces/category'; +import Vendor from '../../interfaces/Vendor'; + +const mockCategory: Category = { + id: 1, + name: 'Electronics', + description: 'Category for electronic products', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +}; + +const mockVendor: Vendor = { + firstName: 'Sample', + lastName: 'Vendor', + picture: '/path/to/vendor-picture.jpg', +}; + +const mockProduct1: Product = { + id: 1, + name: 'Sample Product 1', + image: '/src/assets/iphone1.png', + gallery: ['/path/to/sample-product1.jpg', '/path/to/sample-product1.jpg'], + shortDesc: 'This is a sample product description 1', + longDesc: 'This is a sample product long description 1', + quantity: 10, + regularPrice: 100, + salesPrice: 80, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + averageRating: 4.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct2: Product = { + id: 2, + name: 'Sample Product 2', + image: '/src/assets/iphone2.png', + gallery: ['/path/to/sample-product2.jpg', '/path/to/sample-product2.jpg'], + shortDesc: 'This is a sample product description 2', + longDesc: 'This is a sample product long description 2', + quantity: 15, + regularPrice: 200, + salesPrice: 160, + tags: ['tag3', 'tag4'], + type: 'Simple', + isAvailable: true, + averageRating: 4.0, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct3: Product = { + id: 3, + name: 'Sample Product 3', + image: '/src/assets/iphone3.png', + gallery: ['/path/to/sample-product3.jpg', '/path/to/sample-product3.jpg'], + shortDesc: 'This is a sample product description 3', + longDesc: 'This is a sample product long description 3', + quantity: 20, + regularPrice: 300, + salesPrice: 240, + tags: ['tag5', 'tag6'], + type: 'Simple', + isAvailable: false, + averageRating: 3.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProducts = [mockProduct1, mockProduct2, mockProduct3]; + +const mockStore = configureStore([]); + +describe('MostPopular Component', () => { + let store: ReturnType; + + beforeEach(() => { + store = mockStore({ + availableProducts: { + availableProduct: mockProducts, + status: 'idle', + }, + }); + }); + + it('renders the MostPopular component with products', async () => { + render( + + + + ); + + const titleElement = screen.getByText('Most Popular'); + expect(titleElement).toBeInTheDocument(); + + mockProducts.forEach((product) => { + const productName = screen.getByText(product.name); + expect(productName).toBeInTheDocument(); + + const productImage = screen.getByAltText(product.name); + expect(productImage).toBeInTheDocument(); + expect(productImage).toHaveAttribute('src', product.image); + + const salesPrice = screen.getByText(`$${product.salesPrice}`); + expect(salesPrice).toBeInTheDocument(); + + const regularPrice = screen.getByText(`$${product.regularPrice}`); + expect(regularPrice).toBeInTheDocument(); + }); + }); + + it('handles left arrow click for pagination', async () => { + render( + + + + ); + + const leftArrow = screen.getByAltText('Left Arrow Icon'); + fireEvent.click(leftArrow); + + // Add your assertions for updated state after left arrow click + }); + + it('handles right arrow click for pagination', async () => { + render( + + + + ); + + const rightArrow = screen.getByAltText('Right Arrow Icon'); + fireEvent.click(rightArrow); + + // Add your assertions for updated state after right arrow click + }); + + it('displays loading skeletons when status is loading', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'loading', + }, + }); + + render( + + + + ); + + const skeletons = screen.getAllByRole('status'); + expect(skeletons).toHaveLength(3); + }); + + it('displays error message when status is failed', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'failed', + }, + }); + + render( + + + + ); + + const errorMessages = screen.getAllByText('Loading Failed...'); + expect(errorMessages).toHaveLength(3); + errorMessages.forEach((errorMessage) => { + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__test__/popular/MostRecent.test.tsx b/src/__test__/popular/MostRecent.test.tsx new file mode 100644 index 00000000..266da5ee --- /dev/null +++ b/src/__test__/popular/MostRecent.test.tsx @@ -0,0 +1,198 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import MostRecent from '@/components/Popular/MostRecent'; + +// Mock Product Data +import Product from '../../interfaces/product'; +import Category from '../../interfaces/category'; +import Vendor from '../../interfaces/Vendor'; + +const mockCategory: Category = { + id: 1, + name: 'Electronics', + description: 'Category for electronic products', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +}; + +const mockVendor: Vendor = { + firstName: 'Sample', + lastName: 'Vendor', + picture: '/path/to/vendor-picture.jpg', +}; + +const mockProduct1: Product = { + id: 1, + name: 'Sample Product 1', + image: '/src/assets/iphone1.png', + gallery: ['/path/to/sample-product1.jpg', '/path/to/sample-product1.jpg'], + shortDesc: 'This is a sample product description 1', + longDesc: 'This is a sample product long description 1', + quantity: 10, + regularPrice: 100, + salesPrice: 80, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + averageRating: 4.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-02T00:00:00.000Z', // Different from created date for sorting test + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct2: Product = { + id: 2, + name: 'Sample Product 2', + image: '/src/assets/iphone2.png', + gallery: ['/path/to/sample-product2.jpg', '/path/to/sample-product2.jpg'], + shortDesc: 'This is a sample product description 2', + longDesc: 'This is a sample product long description 2', + quantity: 15, + regularPrice: 200, + salesPrice: 160, + tags: ['tag3', 'tag4'], + type: 'Simple', + isAvailable: true, + averageRating: 4.0, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct3: Product = { + id: 3, + name: 'Sample Product 3', + image: '/src/assets/iphone3.png', + gallery: ['/path/to/sample-product3.jpg', '/path/to/sample-product3.jpg'], + shortDesc: 'This is a sample product description 3', + longDesc: 'This is a sample product long description 3', + quantity: 20, + regularPrice: 300, + salesPrice: 240, + tags: ['tag5', 'tag6'], + type: 'Simple', + isAvailable: false, + averageRating: 3.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-03T00:00:00.000Z', // Different from created date for sorting test + category: mockCategory, + vendor: mockVendor, +}; + +const mockProducts = [mockProduct1, mockProduct2, mockProduct3]; + +const mockStore = configureStore([]); + +describe('MostRecent Component', () => { + let store: ReturnType; + + beforeEach(() => { + store = mockStore({ + availableProducts: { + availableProduct: mockProducts, + status: 'idle', + }, + }); + }); + + it('renders the MostRecent component with products sorted by updatedAt', async () => { + render( + + + + ); + + const titleElement = screen.getByText('Recent Products'); + expect(titleElement).toBeInTheDocument(); + + // Check if products are rendered and sorted correctly by updatedAt + const sortedProducts = mockProducts.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + sortedProducts.slice(0, 3).forEach((product) => { + const productName = screen.getByText(product.name); + expect(productName).toBeInTheDocument(); + + const productImage = screen.getByAltText(product.name); + expect(productImage).toBeInTheDocument(); + expect(productImage).toHaveAttribute('src', product.image); + + const salesPrice = screen.getByText(`$${product.salesPrice}`); + expect(salesPrice).toBeInTheDocument(); + + const regularPrice = screen.getByText(`$${product.regularPrice}`); + expect(regularPrice).toBeInTheDocument(); + }); + }); + + it('handles left arrow click for pagination', async () => { + render( + + + + ); + + const leftArrow = screen.getByAltText('Left Arrow Icon'); + fireEvent.click(leftArrow); + + // Add your assertions for updated state after left arrow click + }); + + it('handles right arrow click for pagination', async () => { + render( + + + + ); + + const rightArrow = screen.getByAltText('Right Arrow Icon'); + fireEvent.click(rightArrow); + + // Add your assertions for updated state after right arrow click + }); + + it('displays loading skeletons when status is loading', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'loading', + }, + }); + + render( + + + + ); + + const skeletons = screen.getAllByRole('status'); + expect(skeletons).toHaveLength(3); + }); + + it('displays error message when status is failed', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'failed', + }, + }); + + render( + + + + ); + + const errorMessages = screen.getAllByText('Loading Failed...'); + expect(errorMessages).toHaveLength(3); + errorMessages.forEach((errorMessage) => { + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__test__/popular/MostSelling.test.tsx b/src/__test__/popular/MostSelling.test.tsx new file mode 100644 index 00000000..d1bcd713 --- /dev/null +++ b/src/__test__/popular/MostSelling.test.tsx @@ -0,0 +1,195 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import MostSelling from '@/components/Popular/MostSelling'; + +// Mock Product Data +import Product from '../../interfaces/product'; +import Category from '../../interfaces/category'; +import Vendor from '../../interfaces/Vendor'; + +const mockCategory: Category = { + id: 1, + name: 'Electronics', + description: 'Category for electronic products', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', +}; + +const mockVendor: Vendor = { + firstName: 'Sample', + lastName: 'Vendor', + picture: '/path/to/vendor-picture.jpg', +}; + +const mockProduct1: Product = { + id: 1, + name: 'Sample Product 1', + image: '/src/assets/iphone1.png', + gallery: ['/path/to/sample-product1.jpg', '/path/to/sample-product1.jpg'], + shortDesc: 'This is a sample product description 1', + longDesc: 'This is a sample product long description 1', + quantity: 10, + regularPrice: 100, + salesPrice: 80, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + averageRating: 4.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct2: Product = { + id: 2, + name: 'Sample Product 2', + image: '/src/assets/iphone2.png', + gallery: ['/path/to/sample-product2.jpg', '/path/to/sample-product2.jpg'], + shortDesc: 'This is a sample product description 2', + longDesc: 'This is a sample product long description 2', + quantity: 15, + regularPrice: 200, + salesPrice: 160, + tags: ['tag3', 'tag4'], + type: 'Simple', + isAvailable: true, + averageRating: 4.0, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct3: Product = { + id: 3, + name: 'Sample Product 3', + image: '/src/assets/iphone3.png', + gallery: ['/path/to/sample-product3.jpg', '/path/to/sample-product3.jpg'], + shortDesc: 'This is a sample product description 3', + longDesc: 'This is a sample product long description 3', + quantity: 20, + regularPrice: 300, + salesPrice: 240, + tags: ['tag5', 'tag6'], + type: 'Simple', + isAvailable: false, + averageRating: 3.5, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + category: mockCategory, + vendor: mockVendor, +}; + +const mockProduct = [mockProduct1, mockProduct2, mockProduct3]; + +const mockStore = configureStore([]); + +describe('MostSelling Component', () => { + let store: ReturnType; + + beforeEach(() => { + store = mockStore({ + availableProducts: { + availableProduct: mockProduct, + status: 'idle', + }, + }); + }); + + it('renders the MostSelling component with products', async () => { + render( + + + + ); + + await screen.findByText('Sample Product 1'); + + const titleElement = screen.getByText('Most Selling'); + expect(titleElement).toBeInTheDocument(); + + mockProduct.forEach((product) => { + const productName = screen.getByText(product.name); + expect(productName).toBeInTheDocument(); + + const productImage = screen.getByAltText(product.name); + expect(productImage).toBeInTheDocument(); + expect(productImage).toHaveAttribute('src', product.image); + + const salesPrice = screen.getByText(`$${product.salesPrice}`); + expect(salesPrice).toBeInTheDocument(); + + const regularPrice = screen.getByText(`$${product.regularPrice}`); + expect(regularPrice).toBeInTheDocument(); + }); + }); + + it('handles left arrow click for pagination', async () => { + render( + + + + ); + + const leftArrow = screen.getByAltText('Left Arrow Icon'); + fireEvent.click(leftArrow); + + // Add your assertions for updated state after left arrow click + }); + + it('handles right arrow click for pagination', async () => { + render( + + + + ); + + const rightArrow = screen.getByAltText('Right Arrow Icon'); + fireEvent.click(rightArrow); + + // Add your assertions for updated state after right arrow click + }); + + it('displays loading skeletons when status is loading', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'loading', + }, + }); + + render( + + + + ); + + const skeletons = screen.getAllByRole('status'); + expect(skeletons).toHaveLength(3); + }); + + it('displays error message when status is failed', () => { + store = mockStore({ + availableProducts: { + availableProduct: [], + status: 'failed', + }, + }); + + render( + + + + ); + + const errorMessages = screen.getAllByText('Loading Failed...'); + expect(errorMessages).toHaveLength(3); + errorMessages.forEach((errorMessage) => { + expect(errorMessage).toBeInTheDocument(); + }); + }); +}); diff --git a/src/__test__/popular/PopularBanner.test.tsx b/src/__test__/popular/PopularBanner.test.tsx new file mode 100644 index 00000000..a9d4992e --- /dev/null +++ b/src/__test__/popular/PopularBanner.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; // Assuming 'vitest' is used for assertions +import BannerAD from '@/components/Popular/BannerAD'; // Adjust path as needed +import ADImage from '@/assets/Image/Rectangle 901.svg'; // Adjust path as needed + +describe('BannerAD Component', () => { + it('renders BannerAD component correctly', () => { + render(); + + const headline = screen.getByText( + 'In store or online your health & safety is our priority' + ); + expect(headline).toBeInTheDocument(); + + const description = screen.getByText( + 'The only E-commerce that makes your life easier, makes you enjoy life and makes it bette' + ); + expect(description).toBeInTheDocument(); + + const adImage = screen.getByAltText('AD'); + expect(adImage).toBeInTheDocument(); + expect(adImage.getAttribute('src')).toBe(ADImage); // Ensure correct image source + }); +}); diff --git a/src/__test__/popular/PopularItem.test.tsx b/src/__test__/popular/PopularItem.test.tsx new file mode 100644 index 00000000..a9d4992e --- /dev/null +++ b/src/__test__/popular/PopularItem.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; // Assuming 'vitest' is used for assertions +import BannerAD from '@/components/Popular/BannerAD'; // Adjust path as needed +import ADImage from '@/assets/Image/Rectangle 901.svg'; // Adjust path as needed + +describe('BannerAD Component', () => { + it('renders BannerAD component correctly', () => { + render(); + + const headline = screen.getByText( + 'In store or online your health & safety is our priority' + ); + expect(headline).toBeInTheDocument(); + + const description = screen.getByText( + 'The only E-commerce that makes your life easier, makes you enjoy life and makes it bette' + ); + expect(description).toBeInTheDocument(); + + const adImage = screen.getByAltText('AD'); + expect(adImage).toBeInTheDocument(); + expect(adImage.getAttribute('src')).toBe(ADImage); // Ensure correct image source + }); +}); diff --git a/src/__test__/popular/PopularSlice.test.tsx b/src/__test__/popular/PopularSlice.test.tsx new file mode 100644 index 00000000..a9d4992e --- /dev/null +++ b/src/__test__/popular/PopularSlice.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; // Assuming 'vitest' is used for assertions +import BannerAD from '@/components/Popular/BannerAD'; // Adjust path as needed +import ADImage from '@/assets/Image/Rectangle 901.svg'; // Adjust path as needed + +describe('BannerAD Component', () => { + it('renders BannerAD component correctly', () => { + render(); + + const headline = screen.getByText( + 'In store or online your health & safety is our priority' + ); + expect(headline).toBeInTheDocument(); + + const description = screen.getByText( + 'The only E-commerce that makes your life easier, makes you enjoy life and makes it bette' + ); + expect(description).toBeInTheDocument(); + + const adImage = screen.getByAltText('AD'); + expect(adImage).toBeInTheDocument(); + expect(adImage.getAttribute('src')).toBe(ADImage); // Ensure correct image source + }); +}); diff --git a/src/__test__/popular/PopularTitle.test.tsx b/src/__test__/popular/PopularTitle.test.tsx new file mode 100644 index 00000000..633e45e5 --- /dev/null +++ b/src/__test__/popular/PopularTitle.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import PopularTitle from '@/components/Popular/PopilarTitle'; + +describe('PopularTitle Component', () => { + it('renders PopularTitle component with given props', () => { + const mockLeftArrowClick = () => {}; + const mockRightArrowClick = () => {}; + + render( + + ); + + expect(screen.getByText('Popular Section')).toBeInTheDocument(); + + const leftArrow = screen.getByAltText('Left Arrow Icon'); + expect(leftArrow).toBeInTheDocument(); + + const rightArrow = screen.getByAltText('Right Arrow Icon'); + expect(rightArrow).toBeInTheDocument(); + + leftArrow.click(); + rightArrow.click(); + }); +}); diff --git a/src/app/store.ts b/src/app/store.ts index 07fbe221..84f4aa0f 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -4,6 +4,7 @@ import signInReducer from '../features/Auth/SignInSlice'; import productsReducer from '@/app/slices/ProductSlice'; import categoriesReducer from '@/app/slices/categorySlice'; import bannerReducer from '@/app/bannerAds/BannerSlice'; +import availableProductsSlice from '@/features/Popular/availableProductSlice'; export const store = configureStore({ reducer: { @@ -12,6 +13,7 @@ export const store = configureStore({ signUp: signUpReducer, signIn: signInReducer, banners: bannerReducer, + availableProducts: availableProductsSlice, }, }); diff --git a/src/assets/Image/Rectangle 901.svg b/src/assets/Image/Rectangle 901.svg new file mode 100644 index 00000000..859d588e --- /dev/null +++ b/src/assets/Image/Rectangle 901.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/Left-Arrow.svg b/src/assets/icons/Left-Arrow.svg new file mode 100644 index 00000000..8b851892 --- /dev/null +++ b/src/assets/icons/Left-Arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/Right-Arrow.svg b/src/assets/icons/Right-Arrow.svg new file mode 100644 index 00000000..430f1a31 --- /dev/null +++ b/src/assets/icons/Right-Arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/image b/src/assets/image deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Popular/BannerAD.tsx b/src/components/Popular/BannerAD.tsx new file mode 100644 index 00000000..e8cce7c1 --- /dev/null +++ b/src/components/Popular/BannerAD.tsx @@ -0,0 +1,26 @@ +import ADImage from '../../assets/Image/Rectangle 901.svg'; + +function BannerAD() { + return ( +
+
+
+

+ In store or online your health & safety is our priority +

+
+
+

+ The only E-commerce that makes your life easier, makes you enjoy + life and makes it bette +

+
+
+
+ AD +
+
+ ); +} + +export default BannerAD; diff --git a/src/components/Popular/Item.tsx b/src/components/Popular/Item.tsx new file mode 100644 index 00000000..f17bf23f --- /dev/null +++ b/src/components/Popular/Item.tsx @@ -0,0 +1,30 @@ +import Product from '../../interfaces/product'; + +interface MostPopularItemProps { + product: Product; +} + +function SingleItem({ product }: MostPopularItemProps) { + return ( +
+
+ {product.name} +
+
+
+

{product.name}

+
+
+

+ ${product.salesPrice} +

+

+ ${product.regularPrice} +

+
+
+
+ ); +} + +export default SingleItem; diff --git a/src/components/Popular/MostPopular.tsx b/src/components/Popular/MostPopular.tsx new file mode 100644 index 00000000..a38d79b7 --- /dev/null +++ b/src/components/Popular/MostPopular.tsx @@ -0,0 +1,83 @@ +// src/components/MostPopular/MostPopular.tsx +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../app/store'; +import PopilarTitle from './PopilarTitle'; +import SingleItem from './Item'; + +function MostPopular() { + const { availableProduct, status } = useSelector( + (state: RootState) => state.availableProducts + ); + + const [start, setStart] = useState(0); + const [end, setEnd] = useState(3); + + const handleLeftallowclick = async () => { + if (start > 0) { + setEnd(end - 3); + setStart(start - 3); + } else { + setEnd(3); + setStart(0); + } + }; + + const handleRightallowclick = async () => { + if (end < availableProduct.length) { + setEnd(end + 3); + setStart(start + 3); + } + }; + + const mostRecentProducts = [...availableProduct] + .sort((a, b) => b.averageRating - a.averageRating) + .slice(start, end); + + return ( +
+ + +
+ {status === 'loading' && + Array(3) + .fill(null) + .map(() => ( +
+
+
+
+
+
+
+ ))} + + {status === 'failed' && + Array(3) + .fill(null) + .map(() => ( +
+ Loading Failed... +
+ ))} + + {mostRecentProducts.map((product) => ( + + ))} +
+
+ ); +} + +export default MostPopular; diff --git a/src/components/Popular/MostRecent.tsx b/src/components/Popular/MostRecent.tsx new file mode 100644 index 00000000..ecbeea5d --- /dev/null +++ b/src/components/Popular/MostRecent.tsx @@ -0,0 +1,86 @@ +// src/components/MostPopular/MostPopular.tsx +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../app/store'; +import PopilarTitle from './PopilarTitle'; +import SingleItem from './Item'; + +function MostRecent() { + const { availableProduct, status } = useSelector( + (state: RootState) => state.availableProducts + ); + + const [start, setStart] = useState(0); + const [end, setEnd] = useState(3); + + const handleLeftallowclick = async () => { + if (start > 0) { + setEnd(end - 3); + setStart(start - 3); + } else { + setEnd(3); + setStart(0); + } + }; + + const handleRightallowclick = async () => { + if (end < availableProduct.length) { + setEnd(end + 3); + setStart(start + 3); + } + }; + + const mostRecentProducts = [...availableProduct] + .sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.createdAt).getTime() + ) + .slice(start, end); + + return ( +
+ + +
+ {status === 'loading' && + Array(3) + .fill(null) + .map(() => ( +
+
+
+
+
+
+
+ ))} + + {status === 'failed' && + Array(3) + .fill(null) + .map(() => ( +
+ Loading Failed... +
+ ))} + + {mostRecentProducts.map((product) => ( + + ))} +
+
+ ); +} + +export default MostRecent; diff --git a/src/components/Popular/MostSelling.tsx b/src/components/Popular/MostSelling.tsx new file mode 100644 index 00000000..6271856e --- /dev/null +++ b/src/components/Popular/MostSelling.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../app/store'; +import PopilarTitle from './PopilarTitle'; +import SingleItem from './Item'; + +function MostSelling() { + const { availableProduct, status } = useSelector( + (state: RootState) => state.availableProducts + ); + + const [start, setStart] = useState(0); + const [end, setEnd] = useState(3); + + const handleLeftallowclick = async () => { + if (start > 0) { + setEnd(end - 3); + setStart(start - 3); + } else { + setEnd(3); + setStart(0); + } + }; + + const handleRightallowclick = async () => { + if (end < availableProduct.length) { + setEnd(end + 3); + setStart(start + 3); + } + }; + + const popularProducts = availableProduct.slice(start, end); + + return ( +
+ + +
+ {(status === 'failed' || status === 'loading') && + Array(3) + .fill(null) + .map(() => ( +
+
+
+
+
+
+
+ ))} + + {status === 'failed' && + Array(3) + .fill(null) + .map(() => ( +
+ Loading Failed... +
+ ))} + + {popularProducts.map((product) => ( + + ))} +
+
+ ); +} +export default MostSelling; diff --git a/src/components/Popular/PopilarTitle.tsx b/src/components/Popular/PopilarTitle.tsx new file mode 100644 index 00000000..57b8685b --- /dev/null +++ b/src/components/Popular/PopilarTitle.tsx @@ -0,0 +1,41 @@ +import leftIcon from '@/assets/icons/Left-Arrow.svg'; +import rightIcon from '@/assets/icons/Right-Arrow.svg'; + +interface PopularTitleProps { + section: string; + onLeftArrowClick: () => void; + onRightArrowClick: () => void; +} + +function PopularTitle({ + section, + onLeftArrowClick, + onRightArrowClick, +}: PopularTitleProps) { + return ( +
+
+

{section}

+

+
+
+ + +
+
+ ); +} + +export default PopularTitle; diff --git a/src/components/Popular/Popular_section.tsx b/src/components/Popular/Popular_section.tsx new file mode 100644 index 00000000..f05b75a8 --- /dev/null +++ b/src/components/Popular/Popular_section.tsx @@ -0,0 +1,30 @@ +// src/pages/LandingPage.tsx +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import MostPopular from './MostPopular'; +import MostRecent from './MostRecent'; +import MostSelling from './MostSelling'; +import BannerAD from './BannerAD'; +import { fetchAvailableProducts } from '@/features/Popular/availableProductSlice'; +import { AppDispatch } from '../../app/store'; + +function PopularSection() { + const dispatch: AppDispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchAvailableProducts()); + }, [dispatch]); + + return ( +
+ +
+ + + +
+
+ ); +} + +export default PopularSection; diff --git a/src/features/Popular/availableProductSlice.ts b/src/features/Popular/availableProductSlice.ts new file mode 100644 index 00000000..b64f09d5 --- /dev/null +++ b/src/features/Popular/availableProductSlice.ts @@ -0,0 +1,49 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +import Product from '../../interfaces/product'; + +interface ProductsState { + availableProduct: Product[]; + status: 'idle' | 'loading' | 'succeeded' | 'failed'; +} + +const URL = import.meta.env.VITE_BASE_URL; + +export const fetchAvailableProducts = createAsyncThunk( + 'products/fetchProducts', + async (_, thunkAPI) => { + try { + const response = await axios.get(`${URL}/product/getAvailableProducts`); + const { data } = response; + return data.availableProducts; + } catch (error) { + return thunkAPI.rejectWithValue(error); + } + } +); + +export const initialState: ProductsState = { + availableProduct: [], + status: 'idle', +}; + +const productsSlice = createSlice({ + name: 'products', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchAvailableProducts.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchAvailableProducts.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.availableProduct = action.payload; + }) + .addCase(fetchAvailableProducts.rejected, (state) => { + state.status = 'failed'; + }); + }, +}); + +export default productsSlice.reducer; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index e9a38ce6..caaf061a 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,6 +1,7 @@ import CategoriesSection from '@/components/home/categories'; import HelloSection from '../components/HelloSection/HelloSection'; import BannerSection from '@/components/bannerAds/bannerSection'; +import PopularSection from '@/components/Popular/Popular_section'; function Home() { return ( @@ -14,6 +15,9 @@ function Home() {
+
+ +
{/* Add more componets as you wish!!! */} ); diff --git a/tailwind.config.js b/tailwind.config.js index 7c53d54a..19b1d7dd 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -25,6 +25,8 @@ export default { grey: '#565D6D', black: '#171A1F', bannerBg: '#F4F1EB', + thickorenge: '#EA580C', + thinorenge: '#FFEDD5', }, fontFamily: { Lexend: ['Lexend'],