diff --git a/src/__test__/dashBoard/ConfirmationCard.test.tsx b/src/__test__/dashBoard/ConfirmationCard.test.tsx new file mode 100644 index 00000000..82cf3365 --- /dev/null +++ b/src/__test__/dashBoard/ConfirmationCard.test.tsx @@ -0,0 +1,98 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; // Assuming 'vitest' is used for assertions +import ConfirmationCard from '@/components/dashBoard/ConfirmationCard'; // Adjust path as needed + +describe('ConfirmationCard Component', () => { + it('does not render when isVisible is false', () => { + render( + {}} + onConfirm={() => {}} + message="Are you sure?" + /> + ); + + const modal = screen.queryByText('Are you sure?'); + expect(modal).not.toBeInTheDocument(); + }); + + it('renders correctly when isVisible is true', () => { + render( + {}} + onConfirm={() => {}} + message="Are you sure?" + /> + ); + + const modal = screen.getByText('Are you sure?'); + expect(modal).toBeInTheDocument(); + }); + + it('calls onClose when the close button is clicked', () => { + const onClose = vi.fn(); + render( + {}} + message="Are you sure?" + /> + ); + + const closeButton = screen.getByRole('button', { name: /close modal/i }); + fireEvent.click(closeButton); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('calls onClose when the "No, cancel" button is clicked', () => { + const onClose = vi.fn(); + render( + {}} + message="Are you sure?" + /> + ); + + const cancelButton = screen.getByRole('button', { name: /no, cancel/i }); + fireEvent.click(cancelButton); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('calls onConfirm when the "Yes, I\'m sure" button is clicked', () => { + const onConfirm = vi.fn(); + render( + {}} + onConfirm={onConfirm} + message="Are you sure?" + /> + ); + + const confirmButton = screen.getByRole('button', { + name: /yes, i'm sure/i, + }); + fireEvent.click(confirmButton); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it('displays the correct message', () => { + const message = 'Are you absolutely sure?'; + render( + {}} + onConfirm={() => {}} + message={message} + /> + ); + + const modalMessage = screen.getByText(message); + expect(modalMessage).toBeInTheDocument(); + }); +}); diff --git a/src/__test__/dashBoard/NavigateonPage.test.tsx b/src/__test__/dashBoard/NavigateonPage.test.tsx new file mode 100644 index 00000000..757771c7 --- /dev/null +++ b/src/__test__/dashBoard/NavigateonPage.test.tsx @@ -0,0 +1,151 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import CircularPagination from '@/components/dashBoard/NavigateonPage'; + +describe('CircularPagination Component', () => { + const onPageChange = vi.fn(); + + it('renders the correct number of pages when totalPages <= 5', () => { + render( + + ); + + const pageButtons = screen.getAllByRole('button'); + expect(pageButtons).toHaveLength(7); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + expect(screen.getByText('4')).toBeInTheDocument(); + expect(screen.getByText('5')).toBeInTheDocument(); + }); + + it('renders correctly when currentPage is at the beginning', () => { + render( + + ); + + const pageButtons = screen.getAllByRole('button'); + expect(pageButtons).toHaveLength(9); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + expect(screen.getByText('4')).toBeInTheDocument(); + expect(screen.getByText('...')).toBeInTheDocument(); + expect(screen.getByText('9')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + }); + + it('renders correctly when currentPage is at the end', () => { + render( + + ); + + const pageButtons = screen.getAllByRole('button'); + expect(pageButtons).toHaveLength(9); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('...')).toBeInTheDocument(); + expect(screen.getByText('7')).toBeInTheDocument(); + expect(screen.getByText('8')).toBeInTheDocument(); + expect(screen.getByText('9')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + }); + + it('renders correctly when currentPage is in the middle', () => { + render( + + ); + + const pageButtons = screen.getAllByRole('button'); + expect(pageButtons).toHaveLength(10); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('4')).toBeInTheDocument(); + expect(screen.getByText('5')).toBeInTheDocument(); + expect(screen.getByText('6')).toBeInTheDocument(); + expect(screen.getByText('...')).toBeInTheDocument(); + expect(screen.getByText('9')).toBeInTheDocument(); + expect(screen.getByText('10')).toBeInTheDocument(); + }); + + it('calls onPageChange with the correct page number when a page button is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByText('6')); + expect(onPageChange).toHaveBeenCalledWith(6); + }); + + it('calls onPageChange with the correct page number when the next button is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByLabelText('next page')); + expect(onPageChange).toHaveBeenCalledWith(6); + }); + + it('calls onPageChange with the correct page number when the previous button is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByLabelText('Previous page')); + expect(onPageChange).toHaveBeenCalledWith(4); + }); + + it('disables the previous button when on the first page', () => { + render( + + ); + + const prevButton = screen.getByLabelText('Previous page'); + expect(prevButton).toBeDisabled(); + }); + + it('disables the next button when on the last page', () => { + render( + + ); + + const nextButton = screen.getByLabelText('next page'); + expect(nextButton).toBeDisabled(); + }); +}); diff --git a/src/__test__/dashBoard/dashboardProductSlice.test.tsx b/src/__test__/dashBoard/dashboardProductSlice.test.tsx new file mode 100644 index 00000000..0ca21cb4 --- /dev/null +++ b/src/__test__/dashBoard/dashboardProductSlice.test.tsx @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import DeshboardProductsSlice, { + initialState, + fetchDashboardProduct, +} from '@/features/Dashboard/dashboardProductsSlice'; // Adjust path as needed + +describe('DeshboardProductsSlice reducer', () => { + it('should return the initial state', () => { + const action = { type: 'unknown/action' }; + expect(DeshboardProductsSlice(undefined, action)).toEqual(initialState); + }); + + it('should handle fetchDashboardProduct.pending', () => { + expect( + DeshboardProductsSlice(initialState, { + type: fetchDashboardProduct.pending.type, + }) + ).toEqual({ + ...initialState, + status: 'loading', + }); + }); + + it('should handle fetchDashboardProduct.fulfilled', () => { + const mockProducts = [ + { id: 1, title: 'Product A' }, + { id: 2, title: 'Product B' }, + ]; + expect( + DeshboardProductsSlice(initialState, { + type: fetchDashboardProduct.fulfilled.type, + payload: mockProducts, + }) + ).toEqual({ + ...initialState, + status: 'succeeded', + DashboardProduct: mockProducts, + }); + }); + + it('should handle fetchDashboardProduct.rejected', () => { + expect( + DeshboardProductsSlice(initialState, { + type: fetchDashboardProduct.rejected.type, + }) + ).toEqual({ + ...initialState, + status: 'failed', + }); + }); +}); diff --git a/src/app/store.ts b/src/app/store.ts index 70113541..2da7d850 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -5,6 +5,7 @@ import productsReducer from '@/app/slices/ProductSlice'; import categoriesReducer from '@/app/slices/categorySlice'; import bannerReducer from '@/app/bannerAds/BannerSlice'; import availableProductsSlice from '@/features/Popular/availableProductSlice'; +import DeshboardProductsSlice from '../features/Dashboard/dashboardProductsSlice'; import subscribeReducer from '@/app/Footer/Subscribe'; export const store = configureStore({ @@ -16,6 +17,7 @@ export const store = configureStore({ banners: bannerReducer, availableProducts: availableProductsSlice, footer: subscribeReducer, + DeshboardProducts: DeshboardProductsSlice, }, }); diff --git a/src/components/dashBoard/ConfirmationCard.tsx b/src/components/dashBoard/ConfirmationCard.tsx new file mode 100644 index 00000000..dd45ed0b --- /dev/null +++ b/src/components/dashBoard/ConfirmationCard.tsx @@ -0,0 +1,76 @@ +interface ConfirmationCardProps { + isVisible: boolean; + onClose: () => void; + onConfirm: () => void; + message: string; +} + +function ConfirmationCard({ + isVisible, + onClose, + onConfirm, + message, +}: ConfirmationCardProps) { + if (!isVisible) return null; + + return ( +
+
+
+ + +

{message}

+
+ + +
+
+
+
+ ); +} + +export default ConfirmationCard; diff --git a/src/components/dashBoard/DashboardSideNav.tsx b/src/components/dashBoard/DashboardSideNav.tsx index baba810f..a8eb9018 100644 --- a/src/components/dashBoard/DashboardSideNav.tsx +++ b/src/components/dashBoard/DashboardSideNav.tsx @@ -30,7 +30,7 @@ const sideBarItems = [ icon: , subItems: [ { - path: '/products/all', + path: '/adminDashboard/products', name: 'All Products', }, { diff --git a/src/components/dashBoard/NavigateonPage.tsx b/src/components/dashBoard/NavigateonPage.tsx new file mode 100644 index 00000000..632e3459 --- /dev/null +++ b/src/components/dashBoard/NavigateonPage.tsx @@ -0,0 +1,98 @@ +import { MdKeyboardArrowRight, MdKeyboardArrowLeft } from 'react-icons/md'; + +interface CircularPaginationProps { + totalPages: number; + currentPage: number; + onPageChange: (page: number) => void; +} + +function CircularPagination({ + totalPages, + currentPage, + onPageChange, +}: CircularPaginationProps) { + const generatePages = () => { + const pages = []; + if (totalPages <= 5) { + for (let i = 1; i <= totalPages; i += 1) { + pages.push(i); + } + } else if (currentPage < 3) { + pages.push(1, 2, 3, 4, '...', totalPages - 1, totalPages); + } else if (currentPage >= totalPages - 2) { + pages.push( + 1, + 2, + '...', + totalPages - 3, + totalPages - 2, + totalPages - 1, + totalPages + ); + } else { + pages.push( + 1, + 2, + currentPage - 1, + currentPage, + currentPage + 1, + '...', + totalPages - 1, + totalPages + ); + } + return pages; + }; + + const pages = generatePages(); + // ----------------------------------------------------------------- + + const next = () => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1); + } + }; + + const prev = () => { + if (currentPage > 1) { + onPageChange(currentPage - 1); + } + }; + + return ( +
+ +
+ {pages.map((page, index) => ( + + ))} +
+ +
+ ); +} + +export default CircularPagination; diff --git a/src/components/dashBoard/Table.tsx b/src/components/dashBoard/Table.tsx new file mode 100644 index 00000000..70a21322 --- /dev/null +++ b/src/components/dashBoard/Table.tsx @@ -0,0 +1,281 @@ +import { useState, useEffect } from 'react'; +import { FaRegTrashAlt } from 'react-icons/fa'; +import { MdOutlineEdit } from 'react-icons/md'; +import { useSelector, useDispatch } from 'react-redux'; +import ConfirmationCard from './ConfirmationCard'; +import CircularPagination from './NavigateonPage'; +import { AppDispatch, RootState } from '../../app/store'; +import { fetchDashboardProduct } from '@/features/Dashboard/dashboardProductsSlice'; + +interface Column { + Header: string; + accessor: string; +} + +interface Column { + Header: string; + accessor: string; +} + +const columns: Column[] = [ + { Header: 'ID', accessor: 'id' }, + { Header: 'IMAGE', accessor: 'image' }, + { Header: 'TITLE', accessor: 'title' }, + { Header: 'QUANTITY', accessor: 'quantity' }, + { Header: 'PRICE', accessor: 'price' }, + { Header: 'DATE', accessor: 'date' }, + { Header: 'CATEGORY', accessor: 'category' }, + { Header: 'ACTION', accessor: 'action' }, +]; + +function Table() { + const dispatch: AppDispatch = useDispatch(); + + const { DashboardProduct, status } = useSelector( + (state: RootState) => state.DeshboardProducts + ); + + const data = [...DashboardProduct]; + + useEffect(() => { + dispatch(fetchDashboardProduct()); + }, [dispatch]); + + const [currentPage, setCurrentPage] = useState(1); + const PRODUCTS_PER_PAGE = 6; + + const totalPages = Math.ceil(data.length / PRODUCTS_PER_PAGE); + const startIndex = (currentPage - 1) * PRODUCTS_PER_PAGE; + const paginatedData = data.slice(startIndex, startIndex + PRODUCTS_PER_PAGE); + // ----------------------------------------------------------- + const [isDeleteModalVisible, setModalVisible] = useState(false); + const [itemSelected, setItemToselected] = useState(null); + const [mode, setmode] = useState(''); + // --------------------------------------------------- + + const handleDelete = (id: number) => { + setItemToselected(id); + setmode('delete'); + setModalVisible(true); + }; + // ----------------------------------- + + const handleUpdate = (id: number) => { + setItemToselected(id); + setmode('update'); + setModalVisible(true); + }; + // ----------------------------------- + + const confirmDelete = () => { + if (itemSelected !== null) { + // Logic to delete the item + } + setModalVisible(false); + }; + // --------------------------------------- + + const confirmUpdate = () => { + if (itemSelected !== null) { + // Logic to update the item + } + setModalVisible(false); + }; + // ---------------------------------------- + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + return ( +
+ + + + {columns.map((column, index) => ( + + ))} + + + + + + {status === 'loading' && + Array(6) + .fill(null) + .map((_, index) => ( + + + + + + + + + + + ))} + {status === 'failed' && + Array(8) + .fill(null) + .map((_, index) => ( + + + + + + + + + + + ))} + {status === 'succeeded' && + paginatedData.map((product, index) => ( + + + + + + + + + + + ))} + +
+ {column.Header} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+
+ Failed.. +
+
+ {product.id} + + product + + {product.name} + + {product.quantity} + + ${product.regularPrice} + + {product.updatedAt.slice(0, 10)} + + {product.category.name} + +
+
+ handleUpdate(product.id)} + /> +
+
+ handleDelete(product.id)} + /> +
+
+
+ +
+ +
+ + {mode === 'delete' && ( +
+ setModalVisible(false)} + onConfirm={confirmDelete} + message="Are you sure you want to delete this item?" + /> +
+ )} + + {mode === 'update' && ( +
+ setModalVisible(false)} + onConfirm={confirmUpdate} + message="Are you sure you want to Update this item ?" + /> +
+ )} +
+ ); +} + +export default Table; diff --git a/src/features/Dashboard/dashboardProductsSlice.ts b/src/features/Dashboard/dashboardProductsSlice.ts new file mode 100644 index 00000000..a764e1f2 --- /dev/null +++ b/src/features/Dashboard/dashboardProductsSlice.ts @@ -0,0 +1,49 @@ +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; +import Product from '../../interfaces/product'; + +interface ProductsState { + DashboardProduct: Product[]; + status: 'idle' | 'loading' | 'succeeded' | 'failed'; +} + +const URL = import.meta.env.VITE_BASE_URL; + +export const fetchDashboardProduct = createAsyncThunk( + 'DashboardProduct', + async (_, thunkAPI) => { + try { + const response = await axios.get(`${URL}/product`); + const { data } = response; + return data.data; + } catch (error) { + return thunkAPI.rejectWithValue(error); + } + } +); + +export const initialState: ProductsState = { + DashboardProduct: [], + status: 'idle', +}; + +const DeshboardProductsSlice = createSlice({ + name: 'products', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchDashboardProduct.pending, (state) => { + state.status = 'loading'; + }) + .addCase(fetchDashboardProduct.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.DashboardProduct = action.payload; + }) + .addCase(fetchDashboardProduct.rejected, (state) => { + state.status = 'failed'; + }); + }, +}); + +export default DeshboardProductsSlice.reducer; diff --git a/src/features/Popular/availableProductSlice.ts b/src/features/Popular/availableProductSlice.ts index b64f09d5..ceb57c79 100644 --- a/src/features/Popular/availableProductSlice.ts +++ b/src/features/Popular/availableProductSlice.ts @@ -10,7 +10,7 @@ interface ProductsState { const URL = import.meta.env.VITE_BASE_URL; export const fetchAvailableProducts = createAsyncThunk( - 'products/fetchProducts', + '/AvailableProducts', async (_, thunkAPI) => { try { const response = await axios.get(`${URL}/product/getAvailableProducts`); diff --git a/src/layout/DashbordLayout.tsx b/src/layout/DashbordLayout.tsx index db88d255..74ecff2e 100644 --- a/src/layout/DashbordLayout.tsx +++ b/src/layout/DashbordLayout.tsx @@ -5,14 +5,16 @@ import Navbar from '@/components/Navbar'; function DashboardLayout() { return (
-
+
-
- -
-
- +
+
+ +
+
+ +
); diff --git a/src/pages/DesplayProductPage.tsx b/src/pages/DesplayProductPage.tsx new file mode 100644 index 00000000..dfbfd719 --- /dev/null +++ b/src/pages/DesplayProductPage.tsx @@ -0,0 +1,11 @@ +import Table from '@/components/dashBoard/Table'; + +function DesplayProductPage() { + return ( +
+ + + ); +} + +export default DesplayProductPage; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 6584d44f..0b8cd562 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -6,6 +6,7 @@ import SignUp from '@/pages/SignUp'; import SignIn from '@/pages/SignIn'; import TwoFactorAuthForm from '@/pages/TwoFactorAuthForm'; import DashboardLayout from '@/layout/DashbordLayout'; +import DesplayProductPage from '@/pages/DesplayProductPage'; function AppRoutes() { return ( @@ -16,7 +17,13 @@ function AppRoutes() { } /> } /> } /> - } /> + }> + } + /> + } /> ); diff --git a/tailwind.config.js b/tailwind.config.js index 19b1d7dd..b7c6dafe 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,7 @@ export default { xs: '320px', sm: '640px', md: '768px', + mdx:'920px', lg: '1024px', }, extend: {