├── src
├── vite-env.d.ts
├── Components
│ ├── Form
│ │ ├── FilterProduct
│ │ │ ├── Types.ts
│ │ │ └── index.tsx
│ │ ├── CreateProduct
│ │ │ ├── Types.ts
│ │ │ └── index.tsx
│ │ ├── UpdateProduct
│ │ │ ├── Types.ts
│ │ │ └── index.tsx
│ │ ├── FormikControl
│ │ │ ├── index.tsx
│ │ │ ├── textArea.tsx
│ │ │ ├── input.tsx
│ │ │ └── select.tsx
│ │ └── DeleteProduct
│ │ │ └── index.tsx
│ ├── Header
│ │ ├── index.tsx
│ │ └── NavLinks.tsx
│ ├── Product
│ │ ├── ProductTable
│ │ │ ├── Rows.tsx
│ │ │ ├── Cells.tsx
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ └── Pagination
│ │ │ └── index.tsx
│ ├── Ui
│ │ ├── Error.tsx
│ │ └── Spinner.tsx
│ └── Modal
│ │ └── index.tsx
├── Pages
│ ├── About
│ │ └── index.tsx
│ ├── Layout
│ │ └── index.tsx
│ └── Home
│ │ └── index.tsx
├── StateManagment
│ ├── Base
│ │ └── index.tsx
│ ├── Client
│ │ └── index.tsx
│ └── Service
│ │ └── Product.tsx
├── index.css
├── main.tsx
├── Model
│ └── Product.ts
├── Utils
│ └── Toast.tsx
├── Hook
│ ├── useDeleteProduct
│ │ └── index.tsx
│ ├── useCreateProduct
│ │ └── index.tsx
│ ├── useUpdateProduct
│ │ └── index.tsx
│ └── useGetProduct
│ │ └── index.tsx
├── App.tsx
└── assets
│ └── react.svg
├── .Prettierrc
├── public
├── 1.jpg
└── vite.svg
├── postcss.config.js
├── vite.config.ts
├── Readme.md
├── tailwind.config.js
├── tsconfig.node.json
├── .gitignore
├── .eslintrc.cjs
├── index.html
├── tsconfig.json
├── package.json
└── db.json
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.Prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/public/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Alireza-WebDeveloper/ReactQuery-Dashboard/HEAD/public/1.jpg
--------------------------------------------------------------------------------
/src/Components/Form/FilterProduct/Types.ts:
--------------------------------------------------------------------------------
1 | export type initialValuesState = {
2 | price: string;
3 | };
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/Pages/About/index.tsx:
--------------------------------------------------------------------------------
1 | const AboutPage = () => {
2 | return <>about page>;
3 | };
4 |
5 | export default AboutPage;
6 |
--------------------------------------------------------------------------------
/src/StateManagment/Base/index.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const BaseApi = axios.create({
4 | baseURL: 'http://localhost:5007',
5 | });
6 |
7 | export default BaseApi;
8 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | a {
7 | text-decoration: none;
8 | color: inherit;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import App from './App.tsx';
3 | import './index.css';
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # How to install the program
2 | # 1 : npm install
3 | # 2 : npm run db
4 | # 3 : npm run dev
5 |
6 |
7 |
8 | # 1.Note : You Can CRUD With Pagination Data Table
9 | # 2.Note : Cache Data When Change Route
10 |
11 |
--------------------------------------------------------------------------------
/src/Model/Product.ts:
--------------------------------------------------------------------------------
1 | export interface ProductState {
2 | id: number;
3 | name: string;
4 | description: string;
5 | year_of_creation: number;
6 | rating: number;
7 | views: number;
8 | country: string;
9 | price: number;
10 | }
11 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: 'class',
4 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | };
10 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/StateManagment/Client/index.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient } from '@tanstack/react-query';
2 | const queryClientStore = new QueryClient({
3 | defaultOptions: {
4 | // 5 * 1000
5 | queries: {
6 | staleTime: 0,
7 | },
8 | },
9 | });
10 |
11 | export default queryClientStore;
12 |
--------------------------------------------------------------------------------
/src/Components/Form/CreateProduct/Types.ts:
--------------------------------------------------------------------------------
1 | // Types
2 | export type initialValuesState = {
3 | name: string;
4 | description: string;
5 | year_of_creation: number;
6 | rating: number;
7 | views: number;
8 | country: string;
9 | price: number;
10 | };
11 | export interface CreateProductProps {
12 | onClose(): void;
13 | }
14 |
--------------------------------------------------------------------------------
/src/Pages/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Header from '../../Components/Header';
3 |
4 | const Layout = () => {
5 | return (
6 | <>
7 |
8 | {}
9 | >
10 | );
11 | };
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/src/Utils/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { ToastContainerProps } from 'react-toastify';
2 | // Types
3 | const toastOptions: ToastContainerProps = {
4 | position: 'top-center',
5 | autoClose: 2000,
6 | hideProgressBar: false,
7 | closeOnClick: true,
8 | pauseOnHover: true,
9 | draggable: true,
10 | theme: 'dark',
11 | };
12 |
13 | export { toastOptions };
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/src/Components/Form/UpdateProduct/Types.ts:
--------------------------------------------------------------------------------
1 | import { ProductState } from '../../../Model/Product';
2 |
3 | // Types
4 | export type initialValuesState = {
5 | name: string;
6 | description: string;
7 | year_of_creation: number;
8 | rating: number;
9 | views: number;
10 | country: string;
11 | price: number;
12 | };
13 | export interface UpdateProductProps {
14 | statusDelete: any;
15 | onClose(): void;
16 | selectProduct: ProductState;
17 | }
18 |
--------------------------------------------------------------------------------
/src/Components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import NavLinks from './NavLinks';
2 |
3 | const Header = () => {
4 | return (
5 |
15 | );
16 | };
17 |
18 | export default Header;
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/src/Components/Header/NavLinks.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | interface NavLinksProps {
5 | routs: { id: number; text: string; to: string }[];
6 | }
7 |
8 | const NavLinks: React.FC = ({ routs }) => {
9 | const createLink = () => {
10 | return routs.map((route) => {
11 | return (
12 |
17 | {route.text}
18 |
19 | );
20 | });
21 | };
22 | return {createLink()}
;
23 | };
24 |
25 | export default NavLinks;
26 |
--------------------------------------------------------------------------------
/src/Components/Product/ProductTable/Rows.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ProductState } from '../../../Model/Product';
3 |
4 | interface RowsState {
5 | products: ProductState[];
6 | }
7 | const Rows: React.FC = ({ products }) => {
8 | return (
9 |
10 |
11 | {products &&
12 | Object.keys(products[0])
13 | .concat('options')
14 | .map((title, index) => {
15 | return (
16 | |
17 | {title}
18 | |
19 | );
20 | })}
21 |
22 |
23 | );
24 | };
25 |
26 | export default Rows;
27 |
--------------------------------------------------------------------------------
/src/Pages/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import Product from '../../Components/Product';
2 | import Error from '../../Components/Ui/Error';
3 | import Spinner from '../../Components/Ui/Spinner';
4 | import useGetProduct from '../../Hook/useGetProduct';
5 |
6 | const HomePage = () => {
7 | const { data, status } = useGetProduct();
8 |
9 | if (status === 'loading') return ;
10 | if (status === 'error') return ;
11 |
12 | return (
13 |
14 | {data && data?.length >= 1 ? (
15 |
16 | ) : (
17 |
18 | There is no product available
19 |
20 | )}
21 |
22 | );
23 | };
24 |
25 | export default HomePage;
26 |
--------------------------------------------------------------------------------
/src/Components/Form/FormikControl/index.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { FC } from 'react';
3 | import Input from './input';
4 | import TextArea from './textArea';
5 | import Select from './select';
6 |
7 | type FormikControlProps = {
8 | control: 'input' | 'range' | 'select' | 'textarea' | 'file';
9 | name: string;
10 | type?: string;
11 | label?: string;
12 | options?: { key: string; value: string }[];
13 | };
14 |
15 | const FormikControl: FC = ({ control, ...otherProps }) => {
16 | switch (control) {
17 | case 'input':
18 | return ;
19 | case 'textarea':
20 | return ;
21 | case 'select':
22 | return ;
23 | default:
24 | return null;
25 | }
26 | };
27 |
28 | export default FormikControl;
29 |
--------------------------------------------------------------------------------
/src/Hook/useDeleteProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQueryClient, useMutation } from '@tanstack/react-query';
2 | import { asyncDeleteProduct } from '../../StateManagment/Service/Product';
3 | import { toast } from 'react-toastify';
4 | import { toastOptions } from '../../Utils/Toast';
5 |
6 | const useDeleteProduct = () => {
7 | const queryClient = useQueryClient();
8 | const { mutate, status } = useMutation({
9 | mutationFn: (id: number) => asyncDeleteProduct(id),
10 | onSuccess: () => {
11 | toast.success('The deletion was successful', toastOptions);
12 | // Revalidate Cached From Server Update
13 | queryClient.invalidateQueries({
14 | queryKey: ['product'],
15 | });
16 | },
17 | onError: (error: Error) => {
18 | toast.error(error.message, toastOptions);
19 | },
20 | });
21 | return { mutate, status };
22 | };
23 |
24 | export default useDeleteProduct;
25 |
--------------------------------------------------------------------------------
/src/Hook/useCreateProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQueryClient, useMutation } from '@tanstack/react-query';
2 | import { asyncCreateProduct } from '../../StateManagment/Service/Product';
3 | import { toast } from 'react-toastify';
4 | import { toastOptions } from '../../Utils/Toast';
5 | import { ProductState } from '../../Model/Product';
6 |
7 | const useUpdateProduct = () => {
8 | const queryClient = useQueryClient();
9 | const { mutate, status } = useMutation({
10 | mutationFn: (product: ProductState) => asyncCreateProduct(product),
11 | onSuccess: () => {
12 | toast.success('The Create was successful', toastOptions);
13 | // Revalidate Cached From Server Create
14 | queryClient.invalidateQueries({
15 | queryKey: ['product'],
16 | });
17 | },
18 | onError: (error: Error) => {
19 | toast.error(error.message, toastOptions);
20 | },
21 | });
22 | return { mutate, status };
23 | };
24 |
25 | export default useUpdateProduct;
26 |
--------------------------------------------------------------------------------
/src/Hook/useUpdateProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQueryClient, useMutation } from '@tanstack/react-query';
2 | import { asyncUpdateProduct } from '../../StateManagment/Service/Product';
3 | import { toast } from 'react-toastify';
4 | import { toastOptions } from '../../Utils/Toast';
5 | import { ProductState } from '../../Model/Product';
6 |
7 | const useUpdateProduct = () => {
8 | const queryClient = useQueryClient();
9 | const { mutate, status } = useMutation({
10 | mutationFn: (product: ProductState) => asyncUpdateProduct(product),
11 | onSuccess: () => {
12 | toast.success('The Update was successful', toastOptions);
13 | // Revalidate Cached From Server Update
14 | queryClient.invalidateQueries({
15 | queryKey: ['product'],
16 | });
17 | },
18 | onError: (error: Error) => {
19 | toast.error(error.message, toastOptions);
20 | },
21 | });
22 | return { mutate, status };
23 | };
24 |
25 | export default useUpdateProduct;
26 |
--------------------------------------------------------------------------------
/src/Components/Ui/Error.tsx:
--------------------------------------------------------------------------------
1 | interface ErrorProps {
2 | message?: string;
3 | }
4 |
5 | const Error: React.FC = ({ message = 'something went error' }) => {
6 | return (
7 |
11 |
20 |
Info
21 |
22 | Danger alert! {message}
23 |
24 |
25 | );
26 | };
27 |
28 | export default Error;
29 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
2 | import Layout from './Pages/Layout';
3 | import HomePage from './Pages/Home';
4 | import { QueryClientProvider } from '@tanstack/react-query';
5 | import queryClientStore from './StateManagment/Client';
6 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
7 | import { ToastContainer } from 'react-toastify';
8 | import 'react-toastify/dist/ReactToastify.css';
9 | import AboutPage from './Pages/About';
10 |
11 | const App = () => {
12 | return (
13 |
14 |
15 |
16 | }>
17 | } />
18 | } />
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Components/Product/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ProductState } from '../../Model/Product';
3 | import ProductTable from './ProductTable';
4 | import Modal from '../Modal';
5 | import CreateProduct from '../Form/CreateProduct';
6 | import FilterProduct from '../Form/FilterProduct';
7 | import Pagination from './Pagination';
8 |
9 | interface ProductProps {
10 | products?: ProductState[];
11 | }
12 |
13 | const Product: React.FC = ({ products }) => {
14 | const [isOpenModal, setIsOpenModal] = useState(false);
15 | const onClose = () => {
16 | setIsOpenModal(false);
17 | };
18 | const onOpen = () => {
19 | setIsOpenModal(true);
20 | };
21 | return (
22 |
23 |
24 |
25 | List of all available products
26 |
27 |
28 |
29 |
30 |
31 |
37 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Product;
49 |
--------------------------------------------------------------------------------
/src/Hook/useGetProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import {
3 | asyncGetProductByPagination,
4 | asyncGetProductByFilterAndPagination,
5 | } from '../../StateManagment/Service/Product';
6 | import { useSearchParams } from 'react-router-dom';
7 |
8 | const useGetProduct = () => {
9 | // Fetch filter parameters from URL search params
10 | const [searchParams] = useSearchParams();
11 | const sort = searchParams.get('_sort');
12 | const order = searchParams.get('_order');
13 | const page = Number(searchParams.get('_page')) || 1;
14 | const limit = Number(searchParams.get('_limit')) || 2;
15 | const hasFilter = Boolean(sort && order);
16 |
17 | // Define the query key based on filter and pagination presence
18 | const queryKey = hasFilter
19 | ? [
20 | 'product',
21 | `sort:${sort}`,
22 | `order:${order}`,
23 | `page:${page}`,
24 | `limit:${limit}`,
25 | ]
26 | : ['product', `page:${page}`, `limit:${limit}`];
27 |
28 | // Define the query function based on filter presence
29 | const queryFn = () => {
30 | const fetchFunction = hasFilter
31 | ? asyncGetProductByFilterAndPagination
32 | : asyncGetProductByPagination;
33 |
34 | return fetchFunction({ order, page, limit });
35 | };
36 |
37 | // Use the useQuery hook to fetch data
38 | const { data, status } = useQuery(queryKey, queryFn);
39 |
40 | return { data, status, page, limit };
41 | };
42 |
43 | export default useGetProduct;
44 |
--------------------------------------------------------------------------------
/src/Components/Form/DeleteProduct/index.tsx:
--------------------------------------------------------------------------------
1 | interface DeleteProductProps {
2 | statusDelete: any;
3 | handleAsyncDeleteProduct(): void;
4 | onClose(): void;
5 | }
6 | const DeleteProduct: React.FC = ({
7 | statusDelete,
8 | handleAsyncDeleteProduct,
9 | onClose,
10 | }) => {
11 | return (
12 | <>
13 |
14 |
15 | By deleting you no longer have access
16 |
17 |
18 |
19 |
27 |
34 |
35 | >
36 | );
37 | };
38 |
39 | export default DeleteProduct;
40 |
--------------------------------------------------------------------------------
/src/Components/Form/FormikControl/textArea.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Field, FieldProps } from 'formik';
3 | import { FC } from 'react';
4 | type TextAreaProps = {
5 | otherProps: any;
6 | };
7 |
8 | const TextArea: FC = ({ otherProps }) => {
9 | const { name, label } = otherProps;
10 |
11 | return (
12 |
13 | {(fieldProps: FieldProps) => {
14 | const { field, meta } = fieldProps;
15 | const { error: errorMessage, touched } = meta;
16 | return (
17 |
18 |
19 |
20 |
26 |
27 | {errorMessage && touched ? (
28 |
29 | {errorMessage}
30 |
31 | ) : (
32 | ''
33 | )}
34 |
35 | );
36 | }}
37 |
38 | );
39 | };
40 |
41 | export default TextArea;
42 |
--------------------------------------------------------------------------------
/src/Components/Form/FormikControl/input.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Field, FieldProps } from 'formik';
3 | import { FC } from 'react';
4 | type InputProps = {
5 | otherProps: any;
6 | };
7 |
8 | const Input: FC = ({ otherProps }) => {
9 | const { name, type, label } = otherProps;
10 |
11 | return (
12 |
13 | {(fieldProps: FieldProps) => {
14 | const { field, meta } = fieldProps;
15 | const { error: errorMessage, touched } = meta;
16 | return (
17 |
36 | );
37 | }}
38 |
39 | );
40 | };
41 |
42 | export default Input;
43 |
--------------------------------------------------------------------------------
/src/Components/Ui/Spinner.tsx:
--------------------------------------------------------------------------------
1 | const Spinner = () => {
2 | return (
3 |
4 |
20 |
Loading...
21 |
22 | );
23 | };
24 |
25 | export default Spinner;
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "db": "json-server -p 5007 -w db.json"
12 | },
13 | "dependencies": {
14 | "@headlessui/react": "^1.7.17",
15 | "@reduxjs/toolkit": "^1.9.5",
16 | "@supabase/supabase-js": "^2.32.0",
17 | "@tanstack/react-query": "^4.32.6",
18 | "@tanstack/react-query-devtools": "^4.32.6",
19 | "axios": "^1.4.0",
20 | "formik": "^2.4.3",
21 | "framer-motion": "^10.13.1",
22 | "json-server": "^0.17.3",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-icons": "^4.10.1",
26 | "react-redux": "^8.1.2",
27 | "react-router-dom": "^6.14.2",
28 | "react-toastify": "^9.1.3",
29 | "redux": "^4.2.1",
30 | "redux-devtools-extension": "^2.13.9",
31 | "redux-thunk": "^2.4.2",
32 | "styled-components": "^6.0.7",
33 | "use-immer": "^0.9.0",
34 | "yup": "^1.2.0"
35 | },
36 | "devDependencies": {
37 | "@types/react": "^18.0.37",
38 | "@types/react-dom": "^18.0.11",
39 | "@typescript-eslint/eslint-plugin": "^5.59.0",
40 | "@typescript-eslint/parser": "^5.59.0",
41 | "@vitejs/plugin-react-swc": "^3.0.0",
42 | "autoprefixer": "^10.4.14",
43 | "eslint": "^8.38.0",
44 | "eslint-plugin-react-hooks": "^4.6.0",
45 | "eslint-plugin-react-refresh": "^0.3.4",
46 | "postcss": "^8.4.24",
47 | "tailwindcss": "^3.3.2",
48 | "typescript": "^5.0.2",
49 | "vite": "^4.3.9"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Components/Form/FilterProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import { Formik, Form as Formik_Form } from 'formik';
2 | import FormikControl from '../FormikControl';
3 | import { initialValuesState } from './Types';
4 | import { useSearchParams } from 'react-router-dom';
5 | // Types
6 |
7 | // InitialValue
8 |
9 | const FilterProduct = () => {
10 | const [searchParam, setSearchParam] = useSearchParams();
11 | const initialValues: initialValuesState = {
12 | price: searchParam.get('_order') || 'asc',
13 | };
14 | const handleSubmitForm = (values: initialValuesState) => {
15 | searchParam.set('_sort', 'price');
16 | searchParam.set('_order', values.price);
17 | setSearchParam(searchParam);
18 | };
19 | return (
20 |
21 | {() => {
22 | return (
23 |
24 |
36 |
37 |
45 |
46 | );
47 | }}
48 |
49 | );
50 | };
51 |
52 | export default FilterProduct;
53 |
--------------------------------------------------------------------------------
/src/Components/Form/FormikControl/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Field, FieldProps } from 'formik';
3 | import { FC } from 'react';
4 | type SelectProps = {
5 | otherProps: any;
6 | };
7 |
8 | const Select: FC = ({ otherProps }) => {
9 | const { name, options, label } = otherProps;
10 |
11 | return (
12 |
13 | {(fieldProps: FieldProps) => {
14 | const { field, meta } = fieldProps;
15 | const { error: errorMessage, touched } = meta;
16 |
17 | return (
18 |
19 |
20 |
26 |
36 |
37 | {errorMessage && touched ? (
38 |
39 | {errorMessage}
40 |
41 | ) : (
42 | ''
43 | )}
44 |
45 | );
46 | }}
47 |
48 | );
49 | };
50 |
51 | export default Select;
52 |
--------------------------------------------------------------------------------
/src/Components/Product/Pagination/index.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'react-router-dom';
2 | import * as Icons from 'react-icons/md';
3 | import useGetProduct from '../../../Hook/useGetProduct';
4 |
5 | const Pagination = () => {
6 | const [searchParams, setSearchParam] = useSearchParams();
7 | const { limit, page, status } = useGetProduct();
8 |
9 | const handlePageChange = (newPage: number) => {
10 | searchParams.set('_page', newPage.toString());
11 | searchParams.set('_limit', limit.toString());
12 | setSearchParam(searchParams);
13 | };
14 |
15 | const isPrevButtonDisabled = status === 'loading' || page === 1;
16 | const isNextButtonDisabled = status === 'loading';
17 |
18 | return (
19 | 1 ? 'justify-evenly' : 'justify-end'}`}>
20 | {page > 1 && (
21 |
29 | )}
30 |
38 |
39 | );
40 | };
41 |
42 | export default Pagination;
43 |
--------------------------------------------------------------------------------
/src/Components/Product/ProductTable/Cells.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ProductState } from '../../../Model/Product';
3 | import * as Icons from 'react-icons/md';
4 |
5 | interface CellsState {
6 | products: ProductState[];
7 | handleSelectProductToDelete(product: ProductState): void;
8 | handleSelectProductToUpdate(product: ProductState): void;
9 | }
10 |
11 | const Cells: React.FC = ({
12 | products,
13 | handleSelectProductToDelete,
14 | handleSelectProductToUpdate,
15 | }) => {
16 | return (
17 |
18 | {products?.map((product) => (
19 |
20 | | {product.id} |
21 | {product.name} |
22 | {product.description} |
23 | {product.year_of_creation} |
24 | {product.rating} |
25 | {product.views} |
26 | {product.country} |
27 | ${product.price.toFixed(2)} |
28 |
29 |
30 |
36 |
42 |
43 | |
44 |
45 | ))}
46 |
47 | );
48 | };
49 |
50 | export default Cells;
51 |
--------------------------------------------------------------------------------
/src/StateManagment/Service/Product.tsx:
--------------------------------------------------------------------------------
1 | import { ProductState } from '../../Model/Product';
2 | import BaseApi from '../Base';
3 |
4 | // Product By Pagination
5 |
6 | const asyncGetProductByPagination = async ({
7 | page,
8 | limit,
9 | }: {
10 | page: number;
11 | limit: number;
12 | }): Promise => {
13 | try {
14 | const response = await BaseApi.get(
15 | `/product?_page=${page}&_limit=${limit}`
16 | );
17 | return response.data;
18 | } catch (error: any) {
19 | throw new Error(error.message);
20 | }
21 | };
22 |
23 | // Product By Pagination And Filter (Sort)
24 | const asyncGetProductByFilterAndPagination = async ({
25 | order,
26 | page,
27 | limit,
28 | }: {
29 | order: any;
30 | page: any;
31 | limit: any;
32 | }) => {
33 | try {
34 | const response = await BaseApi.get(
35 | `/product?_sort=price&_order=${order}&_page=${page}&_limit=${limit}`
36 | );
37 | return response.data;
38 | } catch (error: any) {
39 | throw new Error(error.message);
40 | }
41 | };
42 |
43 | /// Options for each product (Delete , Update , Create)
44 |
45 | const asyncDeleteProduct = async (id: number) => {
46 | try {
47 | const response = await BaseApi.delete(`/product/${id}`);
48 | return response.data;
49 | } catch (error: any) {
50 | throw new Error(error.message);
51 | }
52 | };
53 |
54 | const asyncUpdateProduct = async (product: ProductState) => {
55 | try {
56 | const response = await BaseApi.patch(`/product/${product.id}`, product);
57 | return response.data;
58 | } catch (error: any) {
59 | throw new Error(error.message);
60 | }
61 | };
62 |
63 | const asyncCreateProduct = async (product: Partial) => {
64 | try {
65 | const response = await BaseApi.post(`/product`, product);
66 | return response.data;
67 | } catch (error: any) {
68 | throw new Error(error.message);
69 | }
70 | };
71 |
72 | export {
73 | asyncGetProductByPagination,
74 | asyncGetProductByFilterAndPagination,
75 | asyncDeleteProduct,
76 | asyncUpdateProduct,
77 | asyncCreateProduct,
78 | };
79 |
--------------------------------------------------------------------------------
/src/Components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Fragment } from 'react';
4 | import { Dialog, Transition } from '@headlessui/react';
5 | import * as Icons from 'react-icons/md';
6 | interface ModalProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | children?: React.ReactNode;
10 | title?: string;
11 | }
12 |
13 | const Modal: React.FC = ({ isOpen, onClose, children, title }) => {
14 | return ReactDOM.createPortal(
15 |
16 |
61 | ,
62 | document.getElementById('modal-root')!
63 | );
64 | };
65 |
66 | export default Modal;
67 |
--------------------------------------------------------------------------------
/src/Components/Product/ProductTable/index.tsx:
--------------------------------------------------------------------------------
1 | import { ProductState } from '../../../Model/Product';
2 | import Rows from './Rows';
3 | import Cells from './Cells';
4 | import Modal from '../../Modal';
5 | import { useState } from 'react';
6 | import useDeleteProduct from '../../../Hook/useDeleteProduct';
7 | import DeleteProduct from '../../Form/DeleteProduct';
8 | import UpdateProduct from '../../Form/UpdateProduct';
9 | interface ProductTableProps {
10 | products?: ProductState[];
11 | }
12 |
13 | const ProductTable: React.FC = ({ products }) => {
14 | // State Modal
15 | const [isOpenDeleteModal, setIsOpenDeleteModal] = useState(false);
16 | const [isOpenUpdateModal, setIsOpenUpdateModal] = useState(false);
17 |
18 | // State Select Product (Update,Delete)
19 | const [selectProductToDelete, setSelectProductToDelete] =
20 | useState({} as ProductState);
21 | const [selectProductToUpdate, setSelectProductToUpdate] =
22 | useState({} as ProductState);
23 |
24 | // Select Product To Delete
25 | const handleSelectProductToDelete = (product: ProductState) => {
26 | setSelectProductToDelete(product);
27 | setIsOpenDeleteModal(true);
28 | };
29 |
30 | const handleSelectProductToUpdate = (product: ProductState) => {
31 | setSelectProductToUpdate(product);
32 | setIsOpenUpdateModal(true);
33 | };
34 |
35 | // Close Modal
36 | const onCloseDeleteModal = () => {
37 | setIsOpenDeleteModal(false);
38 | setSelectProductToDelete({} as ProductState);
39 | };
40 |
41 | const onCloseUpdateModal = () => {
42 | setIsOpenUpdateModal(false);
43 | setSelectProductToUpdate({} as ProductState);
44 | };
45 |
46 | // Action Delete Product (Request)
47 | const { mutate: mutateDelete, status: statusDelete } = useDeleteProduct();
48 | const handleAsyncDeleteProduct = () => {
49 | mutateDelete(selectProductToDelete.id);
50 | setIsOpenDeleteModal(false);
51 | setSelectProductToDelete({} as ProductState);
52 | };
53 | return (
54 |
55 |
56 | {products && (
57 | <>
58 |
59 |
64 | >
65 | )}
66 |
67 | {/* Modal */}
68 |
73 |
78 |
79 |
84 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default ProductTable;
95 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Components/Form/CreateProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik, Form as Formik_Form } from 'formik';
3 | import FormikControl from '../FormikControl';
4 | import * as Yup from 'yup';
5 | import { ProductState } from '../../../Model/Product';
6 | import useCreateProduct from '../../../Hook/useCreateProduct';
7 |
8 | import { CreateProductProps } from './Types';
9 |
10 | // Validation
11 | const validationSchema = Yup.object().shape({
12 | name: Yup.string().required('this field is required'),
13 | description: Yup.string().required('this field is required'),
14 | rating: Yup.string().required('this field is required'),
15 | views: Yup.string().required('this field is required'),
16 | country: Yup.string().required('this field is required'),
17 | price: Yup.string().required('this field is required'),
18 | });
19 |
20 | const CreateProduct: React.FC = ({ onClose }) => {
21 | // Initial Values
22 | const initialValues: any = {
23 | name: '',
24 | description: '',
25 | year_of_creation: 0,
26 | rating: 0,
27 | views: 0,
28 | country: '',
29 | price: 0,
30 | };
31 | // Async Action To Update Product
32 | const { mutate, status } = useCreateProduct();
33 | const handleSubmitForm = (values: ProductState) => {
34 | mutate(values);
35 | onClose();
36 | //
37 | };
38 | return (
39 | <>
40 | {/* Description */}
41 |
42 |
43 | By creating the product in the database, it is added to the site page
44 |
45 |
46 | {/* Form */}
47 |
48 |
53 | {() => {
54 | return (
55 |
56 |
62 |
68 |
74 |
80 |
86 |
92 |
98 |
99 |
106 |
107 |
108 | );
109 | }}
110 |
111 |
112 | {/* Options */}
113 |
114 |
121 |
122 | >
123 | );
124 | };
125 |
126 | export default CreateProduct;
127 |
--------------------------------------------------------------------------------
/src/Components/Form/UpdateProduct/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik, Form as Formik_Form } from 'formik';
3 | import FormikControl from '../FormikControl';
4 | import * as Yup from 'yup';
5 | import { ProductState } from '../../../Model/Product';
6 | import useUpdateProduct from '../../../Hook/useUpdateProduct';
7 | import { UpdateProductProps } from './Types';
8 |
9 | // Validation
10 | const validationSchema = Yup.object().shape({
11 | name: Yup.string().required('this field is required'),
12 | description: Yup.string().required('this field is required'),
13 | rating: Yup.string().required('this field is required'),
14 | views: Yup.string().required('this field is required'),
15 | country: Yup.string().required('this field is required'),
16 | price: Yup.string().required('this field is required'),
17 | });
18 |
19 | const UpdateProduct: React.FC = ({
20 | onClose,
21 | selectProduct,
22 | }) => {
23 | // Initial Values
24 | const initialValues: any = {
25 | name: selectProduct.name,
26 | description: selectProduct.description,
27 | year_of_creation: selectProduct.year_of_creation,
28 | rating: selectProduct.rating,
29 | views: selectProduct.views,
30 | country: selectProduct.country,
31 | price: selectProduct.price,
32 | };
33 | // Async Action To Update Product
34 | const { mutate, status } = useUpdateProduct();
35 | const handleSubmitForm = (values: ProductState) => {
36 | const newForm = { ...values, id: selectProduct.id };
37 | mutate(newForm);
38 | onClose();
39 | //
40 | };
41 | return (
42 | <>
43 | {/* Description */}
44 |
45 |
46 | The previous information is not available after the update
47 |
48 |
49 | {/* Form */}
50 |
51 |
56 | {() => {
57 | return (
58 |
59 |
65 |
71 |
77 |
83 |
89 |
95 |
101 |
102 |
109 |
110 |
111 | );
112 | }}
113 |
114 |
115 | {/* Options */}
116 |
117 |
124 |
125 | >
126 | );
127 | };
128 |
129 | export default UpdateProduct;
130 |
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "product": [
3 | {
4 | "id": 1,
5 | "name": "TechPhone Y 12",
6 | "description": "The ultimate flagship smartphone with cutting-edge features.",
7 | "year_of_creation": 2023,
8 | "rating": 4.8,
9 | "views": 12500,
10 | "country": "United States",
11 | "price": 420.75
12 | },
13 | {
14 | "id": 2,
15 | "name": "SuperPhone X",
16 | "description": "A revolutionary smartphone with advanced features and powerful performance.",
17 | "year_of_creation": 2023,
18 | "rating": 4.9,
19 | "views": 9800,
20 | "country": "Japan",
21 | "price": 599.99
22 | },
23 | {
24 | "id": 3,
25 | "name": "SmartGadget Z",
26 | "description": "An intelligent gadget with versatile functionalities for everyday use.",
27 | "year_of_creation": 2022,
28 | "rating": 4.5,
29 | "views": 7500,
30 | "country": "Germany",
31 | "price": 199.5
32 | },
33 | {
34 | "id": 4,
35 | "name": "ProTab X",
36 | "description": "A high-performance tablet for work and entertainment.",
37 | "year_of_creation": 2023,
38 | "rating": 4.7,
39 | "views": 8900,
40 | "country": "South Korea",
41 | "price": 349.99
42 | },
43 | {
44 | "id": 5,
45 | "name": "VisionWatch S",
46 | "description": "An innovative smartwatch with health monitoring features.",
47 | "year_of_creation": 2022,
48 | "rating": 4.6,
49 | "views": 6200,
50 | "country": "China",
51 | "price": 179
52 | },
53 | {
54 | "id": 6,
55 | "name": "LuxeBook Air",
56 | "description": "An ultra-slim laptop with exceptional performance and design.",
57 | "year_of_creation": 2023,
58 | "rating": 4.9,
59 | "views": 11500,
60 | "country": "United States",
61 | "price": 1299
62 | },
63 | {
64 | "id": 7,
65 | "name": "SoundWave Plus",
66 | "description": "A premium noise-canceling headphone with superior audio quality.",
67 | "year_of_creation": 2022,
68 | "rating": 4.7,
69 | "views": 5400,
70 | "country": "Sweden",
71 | "price": 249.99
72 | },
73 | {
74 | "id": 8,
75 | "name": "MegaSpeaker XL",
76 | "description": "A powerful wireless speaker for immersive sound experience.",
77 | "year_of_creation": 2023,
78 | "rating": 4.8,
79 | "views": 7800,
80 | "country": "Canada",
81 | "price": 179.99
82 | },
83 | {
84 | "id": 9,
85 | "name": "UltraCam 4K",
86 | "description": "A compact action camera with stunning 4K video recording.",
87 | "year_of_creation": 2022,
88 | "rating": 4.4,
89 | "views": 6900,
90 | "country": "France",
91 | "price": 199
92 | },
93 | {
94 | "id": 10,
95 | "name": "FitPulse Pro",
96 | "description": "An advanced fitness tracker with heart rate monitoring and GPS.",
97 | "year_of_creation": 2023,
98 | "rating": 4.6,
99 | "views": 8400,
100 | "country": "United Kingdom",
101 | "price": 129.99
102 | },
103 | {
104 | "id": 11,
105 | "name": "GigaDrive SSD",
106 | "description": "A lightning-fast external SSD for seamless data storage.",
107 | "year_of_creation": 2022,
108 | "rating": 4.9,
109 | "views": 12500,
110 | "country": "United States",
111 | "price": 249
112 | },
113 | {
114 | "id": 12,
115 | "name": "PureClean Robot",
116 | "description": "An intelligent robot vacuum for automated cleaning.",
117 | "year_of_creation": 2023,
118 | "rating": 4.5,
119 | "views": 7100,
120 | "country": "South Korea",
121 | "price": 299
122 | },
123 | {
124 | "id": 13,
125 | "name": "GamerX Pro",
126 | "description": "A high-performance gaming laptop for hardcore gamers.",
127 | "year_of_creation": 2022,
128 | "rating": 4.8,
129 | "views": 9800,
130 | "country": "Taiwan",
131 | "price": 1699
132 | },
133 | {
134 | "id": 14,
135 | "name": "HomeGuard 360",
136 | "description": "A comprehensive smart security system for home protection.",
137 | "year_of_creation": 2023,
138 | "rating": 4.7,
139 | "views": 8500,
140 | "country": "United States",
141 | "price": 399.99
142 | },
143 | {
144 | "id": 15,
145 | "name": "ChefMaster Pro",
146 | "description": "An advanced kitchen appliance for professional chefs.",
147 | "year_of_creation": 2022,
148 | "rating": 4.6,
149 | "views": 6700,
150 | "country": "Italy",
151 | "price": 349
152 | },
153 | {
154 | "id": 16,
155 | "name": "UrbanRide E-Scooter",
156 | "description": "A stylish electric scooter for urban commuting.",
157 | "year_of_creation": 2023,
158 | "rating": 4.5,
159 | "views": 7100,
160 | "country": "France",
161 | "price": 299
162 | },
163 | {
164 | "id": 17,
165 | "name": "HealthSync Watch",
166 | "description": "A smartwatch with health and fitness tracking features.",
167 | "year_of_creation": 2022,
168 | "rating": 4.4,
169 | "views": 5900,
170 | "country": "United Kingdom",
171 | "price": 149
172 | },
173 | {
174 | "id": 18,
175 | "name": "PhotoPro Camera 12",
176 | "description": "A professional-grade digital camera for photography enthusiasts.",
177 | "year_of_creation": 2023,
178 | "rating": 4.9,
179 | "views": 10200,
180 | "country": "Japan",
181 | "price": 799
182 | },
183 | {
184 | "id": 19,
185 | "name": "EcoBreeze Fan",
186 | "description": "An energy-efficient fan with adjustable settings.",
187 | "year_of_creation": 2022,
188 | "rating": 4.7,
189 | "views": 7200,
190 | "country": "Germany",
191 | "price": 89.99
192 | },
193 | {
194 | "id": 20,
195 | "name": "AdventurePro Backpack",
196 | "description": "A durable backpack with multiple compartments for adventurers.",
197 | "year_of_creation": 2023,
198 | "rating": 4.6,
199 | "views": 6800,
200 | "country": "Canada",
201 | "price": 149
202 | },
203 | {
204 | "id": 21,
205 | "name": "TravelMate Suitcase",
206 | "description": "A stylish suitcase designed for frequent travelers.",
207 | "year_of_creation": 2022,
208 | "rating": 4.6,
209 | "views": 5400,
210 | "country": "United Kingdom",
211 | "price": 159.99
212 | },
213 | {
214 | "id": 22,
215 | "name": "MusicFlow Earbuds",
216 | "description": "Wireless earbuds with immersive sound and long battery life.",
217 | "year_of_creation": 2023,
218 | "rating": 4.8,
219 | "views": 7100,
220 | "country": "South Korea",
221 | "price": 129
222 | },
223 | {
224 | "id": 23,
225 | "name": "GourmetChef Cookware Set",
226 | "description": "A premium cookware set for professional chefs and home cooks.",
227 | "year_of_creation": 2023,
228 | "rating": 4.9,
229 | "views": 8500,
230 | "country": "France",
231 | "price": 299.99
232 | },
233 | {
234 | "id": 24,
235 | "name": "ActiveFit Smartwatch",
236 | "description": "A smartwatch designed for tracking fitness and health activities.",
237 | "year_of_creation": 2022,
238 | "rating": 4.5,
239 | "views": 6200,
240 | "country": "Japan",
241 | "price": 199
242 | }
243 | ]
244 | }
--------------------------------------------------------------------------------