assignment 5 completed #1
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
38
eslint.config.js
Normal file
38
eslint.config.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react/jsx-no-target-blank': 'off',
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- <title>e-shop</title> -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5600
package-lock.json
generated
Normal file
5600
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
36
package.json
Normal file
36
package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "ecommerce",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.4.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-helmet-async": "^2.0.5",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
|
"react-router": "^7.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.15.0",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.15.0",
|
||||||
|
"eslint-plugin-react": "^7.37.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.16",
|
||||||
|
"vite": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
0
src/App.css
Normal file
0
src/App.css
Normal file
86
src/App.jsx
Normal file
86
src/App.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||||
|
import "./App.css";
|
||||||
|
import Layout from "./components/layout/Layout";
|
||||||
|
import Error from "./components/Error";
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import About from "./pages/About";
|
||||||
|
import Contact from "./pages/Contact";
|
||||||
|
import Shop from "./pages/Shop";
|
||||||
|
import Cart from "./pages/Cart";
|
||||||
|
import Checkout from "./pages/Checkout";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Order from "./pages/Order";
|
||||||
|
import FilterData from "./components/FilterData";
|
||||||
|
import ProductDetail from "./pages/ProductDetail";
|
||||||
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
function App() {
|
||||||
|
const [order, setOrder] = useState({
|
||||||
|
products: [],
|
||||||
|
orderNumber: "",
|
||||||
|
shippingInformation: { address: "", city: "", zip: "" },
|
||||||
|
totalPrice: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Layout />,
|
||||||
|
errorElement: <Error />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Home />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/about",
|
||||||
|
element: <About />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/contact",
|
||||||
|
element: <Contact />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/shop",
|
||||||
|
element: <Shop />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/cart",
|
||||||
|
element: <Cart />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/product/:id",
|
||||||
|
element: <ProductDetail />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/checkout",
|
||||||
|
element: <Checkout setOrder={setOrder} />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/order-confirmation",
|
||||||
|
element: <Order order={order} />,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/filter-data",
|
||||||
|
element: <FilterData />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HelmetProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</HelmetProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
BIN
src/assets/images/hero-banner.jpg
Normal file
BIN
src/assets/images/hero-banner.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
72
src/assets/mockData.jsx
Normal file
72
src/assets/mockData.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
export const categories = [
|
||||||
|
"Electronics",
|
||||||
|
"Fashion",
|
||||||
|
"Home & Kitchen",
|
||||||
|
"Beauty",
|
||||||
|
"Sports",
|
||||||
|
"Automotive",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
image: "https://m.media-amazon.com/images/I/71IYWw-1KBL._SX569_.jpg",
|
||||||
|
name: "Men Upper",
|
||||||
|
price: 29.99,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/51A2iKgaiML._SX300_SY300_QL70_FMwebp_.jpg",
|
||||||
|
name: "Motorcycle Helmet",
|
||||||
|
price: 39.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/31Cdcr1VxEL._SX300_SY300_QL70_FMwebp_.jpg",
|
||||||
|
name: "Fry Pan",
|
||||||
|
price: 49.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/514CfZOJ5rL._SX300_SY300_QL70_FMwebp_.jpg",
|
||||||
|
name: "Smartwatch",
|
||||||
|
price: 59.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/31xXPjbn4jL._SX300_SY300_QL70_FMwebp_.jpg",
|
||||||
|
name: "Bluetooh Earphone",
|
||||||
|
price: 69.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/41A7WrRCOpL._SY445_SX342_QL70_FMwebp_.jpg",
|
||||||
|
name: "Microwave Ovens",
|
||||||
|
price: 79.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
image:
|
||||||
|
"https://m.media-amazon.com/images/I/41BMuuS76SL._SX300_SY300_QL70_FMwebp_.jpg",
|
||||||
|
name: "Wall Clock",
|
||||||
|
price: 89.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
image: "https://m.media-amazon.com/images/I/61SFGUb9y8L._AC_UL320_.jpg",
|
||||||
|
name: "Chess",
|
||||||
|
price: 99.99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
image: "https://m.media-amazon.com/images/I/710SxepIfiL._SL1500_.jpg",
|
||||||
|
name: "dumbbell set",
|
||||||
|
price: 109.99,
|
||||||
|
},
|
||||||
|
];
|
68
src/components/CategorySection.jsx
Normal file
68
src/components/CategorySection.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const category = [
|
||||||
|
{
|
||||||
|
title: "Men",
|
||||||
|
imageUrl:
|
||||||
|
"https://d2ki7eiqd260sq.cloudfront.net/MSLS00005GREENFIGS_2c9ab0616-5513-4e8d-8a78-6e294c31173a.webp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Women",
|
||||||
|
imageUrl:
|
||||||
|
"https://objectstorage.ap-mumbai-1.oraclecloud.com/n/softlogicbicloud/b/cdn/o/category-images/60ab26835e322.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Kids",
|
||||||
|
imageUrl:
|
||||||
|
"https://objectstorage.ap-mumbai-1.oraclecloud.com/n/softlogicbicloud/b/cdn/o/category-images/60b88eb7c6211.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const CategorySection = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex justify-center items-center ">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 md:space-x-14 animate-pulse ">
|
||||||
|
<div className="h-60 w-64 rounded-xl shadow bg-gray-300"></div>
|
||||||
|
<div className="h-60 w-64 rounded-xl shadow bg-gray-300"></div>
|
||||||
|
<div className="h-60 w-64 rounded-xl shadow bg-gray-300"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=" container mx-auto flex justify-center items-center">
|
||||||
|
<div className=" grid grid-cols-1 sm:grid-cols-3 gap-6 md:space-x-14 ">
|
||||||
|
{category.map((elem, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative h-64 transform transition-transform duration-300 hover:scale-105 cursor-pointer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={elem.imageUrl}
|
||||||
|
alt=""
|
||||||
|
className="h-full object-cover rounded-lg shadow-md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute top-20 left-2">
|
||||||
|
<p className="text-md font-bold">{elem.title}</p>
|
||||||
|
<p className="text-gray-600">View All</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategorySection;
|
37
src/components/ChangeAddress.jsx
Normal file
37
src/components/ChangeAddress.jsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const ChangeAddress = ({ setAddress, setIsModelOpen }) => {
|
||||||
|
const onClose = () => {
|
||||||
|
setAddress(newAddress);
|
||||||
|
setIsModelOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [newAddress, setNewAddress] = useState("");
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter new address"
|
||||||
|
className="w-full p-2 border mb-4 outline-none"
|
||||||
|
onChange={(e) => setNewAddress(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="bg-gray-500 hover:bg-gray-700 py-2 px-4 text-white rounded mr-2"
|
||||||
|
onClick={() => setIsModelOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-blue-500 hover:bg-blue-700 py-2 px-4 rounded text-white"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Save Address
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangeAddress;
|
14
src/components/Error.jsx
Normal file
14
src/components/Error.jsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Link, useRouteError } from "react-router";
|
||||||
|
const Error = () => {
|
||||||
|
const error = useRouteError();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Oops! An error occured.</h1>
|
||||||
|
<p> {error.data}</p>
|
||||||
|
<Link to="/">Go Home</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error;
|
24
src/components/FilterData.jsx
Normal file
24
src/components/FilterData.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import ProductCard from "./ProductCard";
|
||||||
|
const FilterData = () => {
|
||||||
|
const filterProducts = useSelector((state) => state.products.filterData);
|
||||||
|
return (
|
||||||
|
<div className="mx-auto py-12 px-4 md:px-16 lg:px-24">
|
||||||
|
{filterProducts.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-center">Shop</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6 cursor-pointer">
|
||||||
|
{filterProducts.map((product) => (
|
||||||
|
<ProductCard product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<h1 className="text-center">not found</h1>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterData;
|
95
src/components/Footer.jsx
Normal file
95
src/components/Footer.jsx
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import React from "react";
|
||||||
|
import { BsTwitter } from "react-icons/bs";
|
||||||
|
import { FaFacebook, FaGithub, FaLinkedin } from "react-icons/fa";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<footer className="bg-gray-800 text-white py-10 px-4 md:px-16 lg:px-24 ">
|
||||||
|
<div className="container mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">e-shop</h3>
|
||||||
|
<p className="mt-4">
|
||||||
|
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Labore ex
|
||||||
|
animi accusamus consectetur harum. Rem mollitia provident
|
||||||
|
consequuntur ducimus exercitationem!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:items-center">
|
||||||
|
<h4 className="text-lg font-semibold">Quick Links</h4>
|
||||||
|
<ul className="mt-4 space-y-2">
|
||||||
|
<li>
|
||||||
|
<Link to="/" className="hover:underline">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link to="/shop" className="hover:underline">
|
||||||
|
Shop
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link to="/about" className="hover:underline">
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Link to="/contact" className="hover:underline">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-semibold">Follow us</h4>
|
||||||
|
<div className="flex space-x-4 mt-4">
|
||||||
|
<a href="/" className="hover:text-gray-400">
|
||||||
|
<FaFacebook />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/" className="hover:text-gray-400">
|
||||||
|
<BsTwitter />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/" className="hover:text-gray-400">
|
||||||
|
<FaGithub />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/" className="hover:text-gray-400">
|
||||||
|
<FaLinkedin />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="" className="flex items-center justify-center mt-8 ">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="enter email"
|
||||||
|
className="w-full p-2 rounded-lg bg-gray-800 border border-gray-600 outline-none"
|
||||||
|
/>
|
||||||
|
<button className="bg-red-500 px-4 py-2 text-white rounded-md ml-2 hover:bg-red-700">
|
||||||
|
Subscibe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 border-t border-gray-700 pt-4">
|
||||||
|
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<p>© {new Date().getFullYear()} e-shop All rights reserved.</p>
|
||||||
|
<div className="flex space-x-4 mt-4 md:mt-0">
|
||||||
|
<a href="/" className="hover:underline">
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
<a href="/" className="hover:underline">
|
||||||
|
Terms & Conditions
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
87
src/components/InfoSection.jsx
Normal file
87
src/components/InfoSection.jsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FaHeadset, FaLock, FaShoppingCart, FaTag } from "react-icons/fa";
|
||||||
|
import { FaMoneyBill1Wave } from "react-icons/fa6";
|
||||||
|
const InfoSection = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const infoItems = [
|
||||||
|
{
|
||||||
|
icon: <FaShoppingCart className="text-3xl text-red-600" />,
|
||||||
|
title: "Free Shipping",
|
||||||
|
description: "Get your orders delivered with no extra cost",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaHeadset className="text-3xl text-red-600" />,
|
||||||
|
title: "Support 24/7",
|
||||||
|
description: "We are here to assist you anytime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaMoneyBill1Wave className="text-3xl text-red-600" />,
|
||||||
|
title: "100% Money Back",
|
||||||
|
description: "Full refund if you are not satisfied ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FaLock className="text-3xl text-red-600" />,
|
||||||
|
title: "Payment Secure",
|
||||||
|
description: "your payment information is safe with us",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: <FaTag className="text-3xl text-red-600" />,
|
||||||
|
title: "Discount",
|
||||||
|
description: "Enjoy the best prices on your products",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white shadow rounded mb-8 mt-12 ">
|
||||||
|
<div className="container mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||||
|
{infoItems.map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col items-center text-center p-4 border rounded-lg shadow-md animate-pulse h-48
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-300 w-12 h-12 mb-4 rounded-full"></div>
|
||||||
|
<h3 className="w-3/4 bg-gray-300 h-5 rounded mb-4"></h3>
|
||||||
|
<p className="bg-gray-300 rounded h-3 w-full mb-2"></p>
|
||||||
|
<p className="bg-gray-300 rounded h-3 w-full "></p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white mb-8 mt-12">
|
||||||
|
<div className="container mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
|
||||||
|
{infoItems.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col items-center text-center p-4 border rounded-lg shadow-md
|
||||||
|
transform transition-transform duration-300 hover:scale-105 cursor-pointer "
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<h3 className="mt-4 text-lg font-serif font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-gray-600">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoSection;
|
21
src/components/Model.jsx
Normal file
21
src/components/Model.jsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Modal = ({ isModelOpen, setIsModelOpen, children }) => {
|
||||||
|
if (!isModelOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
|
||||||
|
<div className="bg-white shadow-lg rounded-lg p-6 w-full max-w-md">
|
||||||
|
<button
|
||||||
|
className="absolute top-4 right-4 text-gray-300 text-3xl"
|
||||||
|
onClick={() => setIsModelOpen(false)}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
107
src/components/Navbar.jsx
Normal file
107
src/components/Navbar.jsx
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaSearch, FaShoppingCart, FaUser } from "react-icons/fa";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Link, useNavigate } from "react-router";
|
||||||
|
import Login from "../pages/Login";
|
||||||
|
import Register from "../pages/Register";
|
||||||
|
import Modal from "./Model";
|
||||||
|
import { setSearchTerm } from "../redux/productSlice";
|
||||||
|
const Navbar = () => {
|
||||||
|
const products = useSelector((state) => state.cart.products);
|
||||||
|
const [isModelOpen, setIsModelOpen] = useState(false);
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [search, setSearch] = useState();
|
||||||
|
const disptach = useDispatch();
|
||||||
|
const navigate = useNavigate("/");
|
||||||
|
|
||||||
|
const openSignUp = () => {
|
||||||
|
setIsLogin(false);
|
||||||
|
setIsModelOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLogin = () => {
|
||||||
|
setIsLogin(true);
|
||||||
|
setIsModelOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
disptach(setSearchTerm(search));
|
||||||
|
navigate("/filter-data");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="w-full bg-white shadow-md">
|
||||||
|
<div className="container mx-auto px-4 md:px-16 lg:px-24 py-4 flex justify-between items-center">
|
||||||
|
<div className="text-lg font-bold">
|
||||||
|
<Link to="/">
|
||||||
|
<span className="font-serif font-bold">E-Shop</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex-1 mx-4 ">
|
||||||
|
<form onSubmit={handleSearch}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="search product"
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="w-full border py-2 px-4 outline-none rounded-full"
|
||||||
|
/>
|
||||||
|
<FaSearch className="top-3 absolute right-3 text-red-500" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="flex item-center space-x-4">
|
||||||
|
<Link to="/cart" className="relative">
|
||||||
|
<FaShoppingCart className="text-xl" />
|
||||||
|
{products.length > 0 && (
|
||||||
|
<span className="absolute top-0 text-xs w-3 left-3 bg-red-600 rounded-full flex justify-center items-center text-white">
|
||||||
|
{products.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="hidden md:block hover:underline"
|
||||||
|
onClick={openLogin}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<span> | </span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="hidden md:block hover:underline"
|
||||||
|
onClick={openSignUp}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button className="block md:hidden ">
|
||||||
|
<FaUser />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center space-x-10 py-4 text-sm font-bold">
|
||||||
|
<Link to="/" className="hover:underline">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link to="/shop" className="hover:underline">
|
||||||
|
Shop
|
||||||
|
</Link>
|
||||||
|
<Link to="/contact" className="hover:underline">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
<Link to="/about" className="hover:underline">
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Modal setIsModelOpen={setIsModelOpen} isModelOpen={isModelOpen}>
|
||||||
|
{isLogin ? (
|
||||||
|
<Login openSignUp={openSignUp} />
|
||||||
|
) : (
|
||||||
|
<Register openLogin={openLogin} />
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
77
src/components/ProductCard.jsx
Normal file
77
src/components/ProductCard.jsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { FaStar } from "react-icons/fa";
|
||||||
|
import { addToCart } from "../redux/cartSlice";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
const ProductCard = ({ product }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const handleAddToCart = (e, product) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
dispatch(addToCart(product));
|
||||||
|
alert("Product Added Successfully!");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-4 shadow rounded relative border transform transition-transform cursor-pointer duration-300 hover:scale-105 animate-pulse">
|
||||||
|
<div className="w-full h-48 bg-gray-300 mb-4 rounded-lg"></div>
|
||||||
|
<div className="h-4 bg-gray-300 mb-2 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 mb-2 rounded w-1/2"></div>
|
||||||
|
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<FaStar key={index} className="text-gray-300" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 right-2 flex items-center justify-center w-8 h-8 bg-gray-300 text-white text-sm rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/product/${product.id}`}>
|
||||||
|
<div className="bg-white p-4 shadow rounded relative border transform transition-transform cursor-pointer duration-300 hover:scale-105">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-48 object-contain mb-4"
|
||||||
|
/>
|
||||||
|
<h3 className="text-lg font-semibold">{product.name}</h3>
|
||||||
|
<p className="text-gray-500">${product.price}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center mt-2">
|
||||||
|
<FaStar className="text-yellow-500" />
|
||||||
|
<FaStar className="text-yellow-500" />
|
||||||
|
<FaStar className="text-yellow-500" />
|
||||||
|
<FaStar className="text-yellow-500" />
|
||||||
|
<FaStar className="text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute bottom-4 right-2 flex items-center justify-center w-8 h-8 bg-red-600
|
||||||
|
group text-white text-sm rounded-full hover:w-32 hover:bg-red-700 duration-300 transition-all"
|
||||||
|
onClick={(e) => handleAddToCart(e, product)}
|
||||||
|
>
|
||||||
|
<span className="group-hover:hidden"> + </span>
|
||||||
|
<span className="hidden group-hover:block"> Add to cart </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCard;
|
15
src/components/layout/Layout.jsx
Normal file
15
src/components/layout/Layout.jsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React from "react";
|
||||||
|
import Navbar from "../Navbar";
|
||||||
|
import Footer from "../Footer";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
const Layout = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<Outlet />
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
3
src/index.css
Normal file
3
src/index.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
13
src/main.jsx
Normal file
13
src/main.jsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "./redux/store.jsx";
|
||||||
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
createRoot(document.getElementById("root")).render(
|
||||||
|
<HelmetProvider>
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</HelmetProvider>
|
||||||
|
);
|
122
src/pages/About.jsx
Normal file
122
src/pages/About.jsx
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FaCarSide } from "react-icons/fa";
|
||||||
|
import { IoIosPricetags } from "react-icons/io";
|
||||||
|
import { FaCartShopping } from "react-icons/fa6";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
const About = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 py-12 px-4 md:px-16 lg:px-24">
|
||||||
|
<Helmet>
|
||||||
|
<title>About Us - Esop</title>
|
||||||
|
|
||||||
|
<meta name="description" content="" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div className=" bg-black bg-opacity-50 z-10"></div>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<img
|
||||||
|
src="https://pujabhandar-1.web.app/assets/1-S31Ps3IF.jpg"
|
||||||
|
alt="About Us"
|
||||||
|
className="w-90 h-72 md:h-66 text-center object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mission Section */}
|
||||||
|
<div className="mt-16 max-w-5xl mx-auto text-center">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold mb-6 text-gray-800">
|
||||||
|
Our Mission
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 leading-relaxed text-lg">
|
||||||
|
At <span className="text-red-600 font-semibold">E-Shop</span>, our
|
||||||
|
mission is to provide an exceptional online shopping experience by
|
||||||
|
offering a wide range of high-quality products, seamless customer
|
||||||
|
service, and a commitment to your satisfaction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Company Highlights */}
|
||||||
|
<div className="mt-16 max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="bg-red-100 text-red-600 w-16 h-16 mx-auto flex items-center justify-center rounded-full mb-4">
|
||||||
|
<FaCartShopping className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800">
|
||||||
|
Quality Products
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
We offer a curated selection of top-quality products for all your
|
||||||
|
needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="bg-red-100 text-red-600 w-16 h-16 mx-auto flex items-center justify-center rounded-full mb-4">
|
||||||
|
<FaCarSide className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800">Fast Delivery</h3>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
We ensure quick and reliable shipping to your doorstep.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="bg-red-100 text-red-600 w-16 h-16 mx-auto flex items-center justify-center rounded-full mb-4">
|
||||||
|
<IoIosPricetags className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800">
|
||||||
|
Affordable Prices
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
Get the best value for your money with our competitive pricing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meet the Team */}
|
||||||
|
<div className="mt-16 max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold mb-8 text-gray-800 text-center">
|
||||||
|
Meet the Team
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
name: "John Doe",
|
||||||
|
role: "Founder & CEO",
|
||||||
|
img: "https://static.vecteezy.com/system/resources/previews/024/183/538/non_2x/male-avatar-portrait-of-a-business-man-in-a-suit-illustration-of-male-character-in-modern-color-style-vector.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jane Smith",
|
||||||
|
role: "Chief Marketing Officer",
|
||||||
|
img: "https://static.vecteezy.com/system/resources/previews/024/183/538/non_2x/male-avatar-portrait-of-a-business-man-in-a-suit-illustration-of-male-character-in-modern-color-style-vector.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Emily Johnson",
|
||||||
|
role: "Product Manager",
|
||||||
|
img: "https://static.vecteezy.com/system/resources/previews/024/183/538/non_2x/male-avatar-portrait-of-a-business-man-in-a-suit-illustration-of-male-character-in-modern-color-style-vector.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Michael Brown",
|
||||||
|
role: "Lead Developer",
|
||||||
|
img: "https://static.vecteezy.com/system/resources/previews/024/183/538/non_2x/male-avatar-portrait-of-a-business-man-in-a-suit-illustration-of-male-character-in-modern-color-style-vector.jpg",
|
||||||
|
},
|
||||||
|
].map((member, index) => (
|
||||||
|
<div key={index} className="text-center">
|
||||||
|
<img
|
||||||
|
src={member.img}
|
||||||
|
alt={member.name}
|
||||||
|
className="w-32 h-32 mx-auto rounded-full mb-4"
|
||||||
|
/>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
|
{member.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{member.role}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
160
src/pages/Cart.jsx
Normal file
160
src/pages/Cart.jsx
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { FaTrashAlt } from "react-icons/fa";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import Modal from "../components/Model";
|
||||||
|
import ChangeAddress from "../components/ChangeAddress";
|
||||||
|
import {
|
||||||
|
decreaseQuantity,
|
||||||
|
increaseQuantity,
|
||||||
|
removeFromCart,
|
||||||
|
} from "../redux/cartSlice";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
const Cart = () => {
|
||||||
|
const cart = useSelector((state) => state.cart);
|
||||||
|
const [address, setAddress] = useState("main street, 123");
|
||||||
|
const [isModelOpen, setIsModelOpen] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 min-h-96 px-4 md:px-16 lg:px-24">
|
||||||
|
{cart.products.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-semibold mb-4">SHOPPING CART</h3>
|
||||||
|
<div className="flex flex-col md:flex-row justify-between space-x-10 mt-8">
|
||||||
|
<div className="md:w-2/3">
|
||||||
|
<div className="hidden md:flex justify-between border-b items-center mb-4 text-xs font-bold">
|
||||||
|
<p>PRODUCTS</p>
|
||||||
|
<div className="flex space-x-8">
|
||||||
|
<p>PRICE</p>
|
||||||
|
<p>QUANTITY</p>
|
||||||
|
<p>SUBTOTAL</p>
|
||||||
|
<p>REMOVE</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{cart.products.map((product) => (
|
||||||
|
<div
|
||||||
|
key={product.id}
|
||||||
|
className="flex flex-col md:flex-row justify-between items-start md:items-center p-3 border-b space-y-4 md:space-y-0"
|
||||||
|
>
|
||||||
|
{/* Product Details */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-16 h-16 object-contain rounded"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Responsive Details */}
|
||||||
|
<div className="flex flex-col md:flex-row md:space-x-12 items-start md:items-center w-full md:w-auto space-y-4 md:space-y-0">
|
||||||
|
{/* Price */}
|
||||||
|
<div className="md:hidden font-bold">Price:</div>
|
||||||
|
<p>${product.price}</p>
|
||||||
|
|
||||||
|
{/* Quantity */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="md:hidden font-bold mr-2">
|
||||||
|
Quantity:
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center border">
|
||||||
|
<button
|
||||||
|
className="text-xl font-bold px-1.5 border-r"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(decreaseQuantity(product.id))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<p className="text-sm px-2">{product.quantity}</p>
|
||||||
|
<button
|
||||||
|
className="text-xl px-1 border-1"
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(increaseQuantity(product.id))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtotal */}
|
||||||
|
<div>
|
||||||
|
<div className="md:hidden font-bold">Subtotal:</div>
|
||||||
|
<p>${(product.quantity * product.price).toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Remove */}
|
||||||
|
<div>
|
||||||
|
<div className="md:hidden font-bold">Remove:</div>
|
||||||
|
<button
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
onClick={() => dispatch(removeFromCart(product.id))}
|
||||||
|
>
|
||||||
|
<FaTrashAlt />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:w-1/3 bg-white p-6 rounded-lg shadow-md border ">
|
||||||
|
<h3 className="text-sm font-semibold mb-5">CART TOTAL</h3>
|
||||||
|
<div className="flex justify-between mb-5 border-b pb-1">
|
||||||
|
<span className="text-sm">Total Items: </span>
|
||||||
|
<span>{cart.totalQuantity} </span>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 border-b pb-2">
|
||||||
|
{/* <p>Shipping:</p> */}
|
||||||
|
<p>Shipping to:</p>
|
||||||
|
<span className="text-xs font-bold">{address}</span>
|
||||||
|
<button
|
||||||
|
className="text-blue-500 hover:underline mt-1 ml-2"
|
||||||
|
onClick={() => setIsModelOpen(true)}
|
||||||
|
>
|
||||||
|
Change Address
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between mb-4">
|
||||||
|
<span>Total Price: </span>
|
||||||
|
<span>${cart.totalPrice.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="w-full bg-red-600 text-white py-2 hover:bg-red-800"
|
||||||
|
onClick={() => navigate("/checkout")}
|
||||||
|
>
|
||||||
|
Proceed to Checkout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal isModelOpen={isModelOpen} setIsModelOpen={setIsModelOpen}>
|
||||||
|
<ChangeAddress
|
||||||
|
setAddress={setAddress}
|
||||||
|
setIsModelOpen={setIsModelOpen}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<img
|
||||||
|
src="https://static.vecteezy.com/system/resources/previews/016/462/240/non_2x/empty-shopping-cart-illustration-concept-on-white-background-vector.jpg"
|
||||||
|
alt=""
|
||||||
|
className="h-96 my-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cart;
|
304
src/pages/Checkout.jsx
Normal file
304
src/pages/Checkout.jsx
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { FaAngleUp, FaAngleDown } from "react-icons/fa";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
const Checkout = ({ setOrder }) => {
|
||||||
|
const [billingToggle, setBillingToggle] = useState(true);
|
||||||
|
const [shippingToggle, setShippingToggle] = useState(false);
|
||||||
|
const [paymentToggle, setPaymentToggle] = useState(false);
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState("cod");
|
||||||
|
|
||||||
|
const [shippingInfo, setShippingInfo] = useState({
|
||||||
|
address: " ",
|
||||||
|
city: "",
|
||||||
|
zip: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const cart = useSelector((state) => state.cart);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleOrder = () => {
|
||||||
|
const newOrder = {
|
||||||
|
products: cart.products,
|
||||||
|
orderNumber: "12344",
|
||||||
|
shippingInformation: shippingInfo,
|
||||||
|
totalPrice: cart.totalPrice,
|
||||||
|
};
|
||||||
|
setOrder(newOrder);
|
||||||
|
navigate("/order-confirmation");
|
||||||
|
console.log("button clicked");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="container mx-auto py-8 min-h-96 px-4 md:px-16 lg:px-24">
|
||||||
|
<h3 className="text-2xl font-semibold mb-4">CHECKOUT</h3>
|
||||||
|
<div className="flex flex-col md:flex-row justify-between space-x-10 mt-8">
|
||||||
|
<div className="border md:w-2/3 p-5 ">
|
||||||
|
{/* Billing Information */}
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
onClick={() => setBillingToggle(!billingToggle)}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
Billing Information
|
||||||
|
</h3>
|
||||||
|
{billingToggle ? <FaAngleDown /> : <FaAngleUp />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`${billingToggle ? "" : "hidden"}`}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter name"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700" htmlFor="">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="enter email"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="block text-gray-700">
|
||||||
|
Phone
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter phone"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* shipping Information */}
|
||||||
|
<div className="mt-5">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
onClick={() => setShippingToggle(!shippingToggle)}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
Shipping Information
|
||||||
|
</h3>
|
||||||
|
{shippingToggle ? <FaAngleDown /> : <FaAngleUp />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`${shippingToggle ? "" : "hidden"}`}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700">Address</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter address"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
onChange={(e) =>
|
||||||
|
setShippingInfo({
|
||||||
|
...shippingInfo,
|
||||||
|
address: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-gray-700" htmlFor="">
|
||||||
|
City
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter city name"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
onChange={(e) =>
|
||||||
|
setShippingInfo({
|
||||||
|
...shippingInfo,
|
||||||
|
city: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="block text-gray-700">
|
||||||
|
Zipcode
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter zip code"
|
||||||
|
className="w-full px-3 py-2 border mb-4 rounded outline-none"
|
||||||
|
onChange={(e) =>
|
||||||
|
setShippingInfo({
|
||||||
|
...shippingInfo,
|
||||||
|
zip: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* payment Information */}
|
||||||
|
<div className="mt-5">
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between"
|
||||||
|
onClick={() => setPaymentToggle(!paymentToggle)}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Payment Method</h3>
|
||||||
|
{paymentToggle ? <FaAngleDown /> : <FaAngleUp />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`${paymentToggle ? "" : "hidden"}`}>
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="payment"
|
||||||
|
checked={paymentMethod === "cod"}
|
||||||
|
onChange={() => setPaymentMethod("cod")}
|
||||||
|
/>
|
||||||
|
<label className="block text-gray-700 ml-2">
|
||||||
|
Cash on Delivery
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="payment"
|
||||||
|
checked={paymentMethod === "dc"}
|
||||||
|
onChange={() => setPaymentMethod("dc")}
|
||||||
|
/>
|
||||||
|
<label className="block text-gray-700 ml-2">Debit Card</label>
|
||||||
|
</div>
|
||||||
|
{paymentMethod === "dc" && (
|
||||||
|
<div className="bg-gray-100 p-4 rounded-lg mb-4">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">
|
||||||
|
Debit Card Information
|
||||||
|
</h3>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label
|
||||||
|
htmlFor=""
|
||||||
|
className="block text-gray-700 font-semibold mb-2"
|
||||||
|
>
|
||||||
|
Card Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter card number"
|
||||||
|
className="border p-2 w-full rounded outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-gray-700 font-semibold mb-2">
|
||||||
|
Card Holder Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="enter card holder name"
|
||||||
|
className="border p-2 w-full rounded outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mb-4">
|
||||||
|
<div className="w-1/2 mr-2">
|
||||||
|
<label
|
||||||
|
htmlFor=""
|
||||||
|
className="block text-gray-700 font-semibold mb-2"
|
||||||
|
>
|
||||||
|
Expire Date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="MM/YY"
|
||||||
|
className="p-2 border w-full rounded outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-1/2 ml-2">
|
||||||
|
<label
|
||||||
|
htmlFor=""
|
||||||
|
className="block text-gray-700 font-semibold mb-2"
|
||||||
|
>
|
||||||
|
CVV
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="CVV"
|
||||||
|
className="border p-2 w-full rounded outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:w-1/3 bg-white p-6 rounded-lg shadow-md border mt-8 md:mt-0">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Order Summary</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{cart.products.map((product) => (
|
||||||
|
<div key={product.id} className="flex justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-16 h-16 object-contain rounded"
|
||||||
|
/>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h4 className="text-md font-semibold">{product.name}</h4>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
${product.price} X {product.quantity}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-800">
|
||||||
|
${product.price}
|
||||||
|
{/* * {product.quantity} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 border-t pt-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Total Price: </span>
|
||||||
|
<span className="font-semibold">
|
||||||
|
${cart.totalPrice.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="w-full bg-red-600 text-white py-2 mt-6 hover:bg-red-800"
|
||||||
|
onClick={handleOrder}
|
||||||
|
>
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Checkout;
|
119
src/pages/Contact.jsx
Normal file
119
src/pages/Contact.jsx
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import React from "react";
|
||||||
|
import { IoLocationSharp } from "react-icons/io5";
|
||||||
|
import { IoMdMail } from "react-icons/io";
|
||||||
|
import { FaPhone } from "react-icons/fa6";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
|
||||||
|
const Contact = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100 py-12 px-4 md:px-16 lg:px-24">
|
||||||
|
<Helmet>
|
||||||
|
<title>Contact Us - E-Shop</title>
|
||||||
|
<meta name="description" content="" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="max-w-5xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
|
||||||
|
<div className="flex flex-col md:flex-row">
|
||||||
|
<div className="bg-red-600 text-white p-8 md:w-1/2 flex flex-col justify-center">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">Contact Us</h2>
|
||||||
|
|
||||||
|
<p className="mb-6">
|
||||||
|
Have any questions? We'd love to hear from you. Feel free to reach
|
||||||
|
out to us!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<IoLocationSharp className="text-xl mr-2" />
|
||||||
|
<p>123 Street Name, City, State, ZIP</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<IoMdMail className="text-xl mr-2" />
|
||||||
|
<p>contact@example.com</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaPhone className="text-xl mr-2" />
|
||||||
|
|
||||||
|
<p>+1 (555) 123-4567</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8 md:w-1/2">
|
||||||
|
<h2 className="text-3xl uppercase font-bold mb-6 text-gray-800">
|
||||||
|
Get in Touch
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form action="#" method="POST" className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
className="mt-1 block w-full px-4 py-2 outline-none border border-gray-300 rounded-md shadow-sm"
|
||||||
|
placeholder="your name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
className="mt-1 block w-full px-4 py-2 outline-none border border-gray-300 rounded-md shadow-sm"
|
||||||
|
placeholder="enter your email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="message"
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
rows="4"
|
||||||
|
className="mt-1 block w-full px-4 py-2 outline-none border border-gray-300 rounded-md shadow-sm"
|
||||||
|
placeholder="enter your message"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 px-4 bg-red-600 text-white font-bold
|
||||||
|
rounded-md hover:bg-red-700 mt-3"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contact;
|
105
src/pages/Home.jsx
Normal file
105
src/pages/Home.jsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Hero from "../assets/images/hero-banner.jpg";
|
||||||
|
import InfoSection from "../components/InfoSection";
|
||||||
|
import CategorySection from "../components/CategorySection";
|
||||||
|
import { setProducts } from "../redux/productSlice";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { categories, mockData } from "../assets/mockData";
|
||||||
|
import ProductCard from "../components/ProductCard";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
import Shop from "./Shop";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
const Home = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const products = useSelector((state) => state.products);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setProducts(mockData));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Home - E-Shop</title>
|
||||||
|
<meta name="description" content="" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="bg-white mt-2 px-4 md:px-16 lg:px-24">
|
||||||
|
<div className="container mx-auto py-4 flex flex-col md:flex-row md:space-x-4">
|
||||||
|
{/* Categories Section */}
|
||||||
|
<div className="w-full md:w-3/12">
|
||||||
|
<div className="bg-red-600 text-white text-sm font-bold px-2 py-2.5">
|
||||||
|
Shop By Categories
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-4 bg-gray-100 p-3 border">
|
||||||
|
{categories.map((category, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex items-center text-sm font-medium"
|
||||||
|
>
|
||||||
|
<div className="w-2 h-2 border border-red-500 rounded-full mr-2"></div>
|
||||||
|
<div>{category}</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="w-full md:w-9/12 relative">
|
||||||
|
<div className="w-full">
|
||||||
|
<img
|
||||||
|
src={Hero}
|
||||||
|
alt="Hero Banner"
|
||||||
|
className="w-full h-96 object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 flex flex-col justify-center items-center text-center bg-black bg-opacity-50 text-white">
|
||||||
|
<h2 className="text-2xl md:text-4xl font-bold">
|
||||||
|
WELCOME TO E-SHOP
|
||||||
|
</h2>
|
||||||
|
<p className="text-base md:text-lg">MILLIONS + PRODUCTS</p>
|
||||||
|
<Link to="/shop">
|
||||||
|
<button
|
||||||
|
className="bg-red-600 px-8 py-1.5 text-white mt-4 hover:bg-red-700 transform
|
||||||
|
transition-transform duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
SHOP NOW
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<InfoSection />
|
||||||
|
<CategorySection />
|
||||||
|
<div className="container mx-auto py-12">
|
||||||
|
{loading ? (
|
||||||
|
<div className="h-8 w-32 mx-auto bg-gray-300 rounded-xl mb-6 animate-pulse"></div>
|
||||||
|
) : (
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-center">
|
||||||
|
Top Products
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6 cursor-pointer">
|
||||||
|
{products?.products?.slice(0, 5).map((product) => (
|
||||||
|
<ProductCard product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Shop />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
57
src/pages/Login.jsx
Normal file
57
src/pages/Login.jsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from "react";
|
||||||
|
const Login = ({ openSignUp }) => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto p-6 bg-white rounded-lg">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Login</h2>
|
||||||
|
<form action="">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="block text-gray-700 mb-1">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 outline-none rounded-lg"
|
||||||
|
placeholder="enter an email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-gray-700 mb-1">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg outline-none"
|
||||||
|
placeholder="enter password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<label htmlFor="inline-flex items-center">
|
||||||
|
<input type="checkbox" className="form-checkbox" />
|
||||||
|
<span className="ml-2 text-gray-700 ">Remember Me</span>
|
||||||
|
</label>
|
||||||
|
<a href="/" className="text-gray-700">
|
||||||
|
Forgot Password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 ">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-red-600 text-white py-2 rounded-full hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-700">Don't have an account ? </span>
|
||||||
|
<button
|
||||||
|
className="text-red-600 hover:text-red-700 hover:underline"
|
||||||
|
onClick={openSignUp}
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
57
src/pages/Order.jsx
Normal file
57
src/pages/Order.jsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
const Order = ({ order }) => {
|
||||||
|
const navigate = useNavigate("/");
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 md:px-16 lg:px-24">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Thank you for your order</h2>
|
||||||
|
<p>
|
||||||
|
Your order has been placed successfully you will recieve email shortly.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 p-4 border rounded-lg bg-gray-100">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Order Summary</h3>
|
||||||
|
<p>Order Number : {order.orderNumber}</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="text-md font-semibold mb-2">Shipping Information</h4>
|
||||||
|
<p>{order.shippingInformation.address}</p>
|
||||||
|
<p>{order.shippingInformation.city}</p>
|
||||||
|
<p>{order.shippingInformation.zip}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="text-md font-semibold mb-2">Items Ordered</h3>
|
||||||
|
{order.products.map((product) => (
|
||||||
|
<div key={product.id} className="flex justify-between mt-2">
|
||||||
|
<p>
|
||||||
|
{product.name} (X{product.quantity})
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>{product.price * product.quantity}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between">
|
||||||
|
<span>Total Price:</span>
|
||||||
|
<span className="font-semibold">{order.totalPrice}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<button className="bg-green-500 text-white py-2 px-4 hover:bg-green-600">
|
||||||
|
Order Tracking
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="ml-4 bg-red-600 text-white py-2 px-4 hover:bg-red-800
|
||||||
|
"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
>
|
||||||
|
Continue Shopping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Order;
|
94
src/pages/ProductDetail.jsx
Normal file
94
src/pages/ProductDetail.jsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FaCarSide } from "react-icons/fa";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { FaQuestion } from "react-icons/fa";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
const ProductDetail = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const products = useSelector((state) => state.products.products);
|
||||||
|
const [product, setProduct] = useState();
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
useEffect(() => {
|
||||||
|
const newProduct = products.find((product) => product.id === parseInt(id));
|
||||||
|
setProduct(newProduct);
|
||||||
|
}, [id, products]);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 px-4 md:px-16 lg:px-24">
|
||||||
|
<Helmet>
|
||||||
|
<title>{product.name} - Product Description</title>
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content={`Details about ${product.name}`}
|
||||||
|
></meta>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-x-16">
|
||||||
|
{/* product image */}
|
||||||
|
<div className="md:w-1/2 py-4 shadow-md md:px-8 h-96 flex justify-center">
|
||||||
|
<img src={product.image} alt={product.name} className="h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* product information */}
|
||||||
|
<div className="md:w-1/2 p-4 shadow-md md:p-16 flex flex-col items-center gap-y-2">
|
||||||
|
<h2 className="text-3xl font-semibold mb-2"> {product.name} </h2>
|
||||||
|
<p className="text-xl font-semibold text-gray-800 mb-4">
|
||||||
|
${product.price}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-center mb-4 gap-4">
|
||||||
|
<button
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-xl font-bold text-white w-10 h-10
|
||||||
|
rounded-full"
|
||||||
|
onClick={() => setQuantity(quantity > 1 ? quantity - 1 : 1)}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="quantity"
|
||||||
|
value={quantity}
|
||||||
|
className="border border-gray-300 text-center text-lg font-semibold rounded-md p-2 w-14"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-xl font-bold text-white w-10 h-10
|
||||||
|
rounded-full"
|
||||||
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="bg-red-600 text-white py-1.5 px-4 hover:bg-red-800">
|
||||||
|
Add to Cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-4 mt-4">
|
||||||
|
<p className="flex items-center">
|
||||||
|
<FaCarSide className="mr-1" />
|
||||||
|
Delivery & Return
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="flex items-center">
|
||||||
|
<FaQuestion className="mr-1" />
|
||||||
|
Ask a Question
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-xl font-bold mb-2">Product Description</h3>
|
||||||
|
<p>Product Description will goes here.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetail;
|
62
src/pages/Register.jsx
Normal file
62
src/pages/Register.jsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import React from "react";
|
||||||
|
const Register = ({ openLogin }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="max-w-md mx-auto p-6 bg-white rounded-lg">
|
||||||
|
<h2 className="text-2xl font-bold mb-4 text-gray-800">Register</h2>
|
||||||
|
<form action="">
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="block text-gray-700 mb-1">Name</label>
|
||||||
|
<input
|
||||||
|
type="name"
|
||||||
|
className="w-full px-4 py-2 border border=gray-300 rounded-lg outline-none"
|
||||||
|
placeholder="enter your name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-gray-700 mb-1 ">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg
|
||||||
|
outline-none"
|
||||||
|
placeholder="enter an email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-gray-700 mb-1">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg
|
||||||
|
outline-none"
|
||||||
|
placeholder="enter a password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 ">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-red-600 text-white py-2 rounded-full hover:bg-red-700
|
||||||
|
transition duration-300"
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-gray-700">Already have an account ? </span>
|
||||||
|
<button
|
||||||
|
className="text-red-600 hover:underline focus:outline-none"
|
||||||
|
onClick={openLogin}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
37
src/pages/Shop.jsx
Normal file
37
src/pages/Shop.jsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import ProductCard from "../components/ProductCard";
|
||||||
|
|
||||||
|
const Shop = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const products = useSelector((state) => state.products);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto py-12 px-4 md:px-16 lg:px-24">
|
||||||
|
{loading ? (
|
||||||
|
<div className="h-8 w-32 mx-auto mb-6 bg-gray-300 rounded-xl animate-pulse"></div>
|
||||||
|
) : (
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-center">Shop</h2>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6 cursor-pointer">
|
||||||
|
{products?.products?.map((product) => (
|
||||||
|
<ProductCard product={product} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shop;
|
75
src/redux/cartSlice.jsx
Normal file
75
src/redux/cartSlice.jsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
products: [],
|
||||||
|
totalQuantity: 0,
|
||||||
|
totalPrice: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cartSlice = createSlice({
|
||||||
|
name: "cart",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addToCart(state, action) {
|
||||||
|
const newItem = action.payload;
|
||||||
|
const itemIndex = state.products.find((item) => item.id === newItem.id);
|
||||||
|
|
||||||
|
if (itemIndex) {
|
||||||
|
itemIndex.quantity++;
|
||||||
|
itemIndex.totalPrice += newItem.price;
|
||||||
|
} else {
|
||||||
|
state.products.push({
|
||||||
|
id: newItem.id,
|
||||||
|
name: newItem.name,
|
||||||
|
price: newItem.price,
|
||||||
|
quantity: 1,
|
||||||
|
totalPrice: newItem.price,
|
||||||
|
image: newItem.image,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
state.totalPrice += newItem.price;
|
||||||
|
state.totalQuantity++;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromCart(state, action) {
|
||||||
|
const id = action.payload;
|
||||||
|
const findItem = state.products.find((item) => item.id === id);
|
||||||
|
|
||||||
|
if (findItem) {
|
||||||
|
state.totalPrice -= findItem.totalPrice;
|
||||||
|
state.totalQuantity -= findItem.quantity;
|
||||||
|
state.products = state.products.filter((item) => item.id !== id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
increaseQuantity(state, action) {
|
||||||
|
const id = action.payload;
|
||||||
|
const findItem = state.products.find((item) => item.id === id);
|
||||||
|
if (findItem) {
|
||||||
|
findItem.quantity++;
|
||||||
|
findItem.totalPrice += findItem.price;
|
||||||
|
state.totalQuantity++;
|
||||||
|
state.totalPrice += findItem.price;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
decreaseQuantity(state, action) {
|
||||||
|
const id = action.payload;
|
||||||
|
const findItem = state.products.find((item) => item.id === id);
|
||||||
|
|
||||||
|
if (findItem.quantity > 1) {
|
||||||
|
if (findItem) {
|
||||||
|
findItem.quantity--;
|
||||||
|
findItem.totalPrice -= findItem.price;
|
||||||
|
state.totalQuantity--;
|
||||||
|
state.totalPrice -= findItem.price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addToCart, removeFromCart, increaseQuantity, decreaseQuantity } =
|
||||||
|
cartSlice.actions;
|
||||||
|
export default cartSlice.reducer;
|
28
src/redux/productSlice.jsx
Normal file
28
src/redux/productSlice.jsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
products: [],
|
||||||
|
searchTerm: "",
|
||||||
|
filterData: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const productSlice = createSlice({
|
||||||
|
name: "products",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setProducts(state, action) {
|
||||||
|
state.products = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchTerm(state, action) {
|
||||||
|
state.searchTerm = action.payload;
|
||||||
|
state.filterData = state.products.filter((product) =>
|
||||||
|
product.name.toLowerCase().includes(state.searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSearchTerm, setProducts } = productSlice.actions;
|
||||||
|
|
||||||
|
export default productSlice.reducer;
|
9
src/redux/store.jsx
Normal file
9
src/redux/store.jsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import cartReducer from "./cartSlice";
|
||||||
|
import productReducer from "./productSlice";
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
cart: cartReducer,
|
||||||
|
products: productReducer,
|
||||||
|
},
|
||||||
|
});
|
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
7
vite.config.js
Normal file
7
vite.config.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
Loading…
Reference in a new issue