add toml
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
# Modern E-Commerce Store
|
||||
|
||||
A production-ready e-commerce application with responsive design, authentication, and full shopping functionality.
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive UI with mobile-first design
|
||||
- Homepage with hero section and featured products
|
||||
- Category browsing
|
||||
- Product cards with images and details
|
||||
- Product detail page
|
||||
- Shopping cart functionality
|
||||
- Checkout form with validation
|
||||
- Authentication pages (login/signup)
|
||||
- Reusable React components
|
||||
- Mock backend/data layer
|
||||
- Clean, modern styling
|
||||
- Context API for state management
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React.js (with hooks and context API)
|
||||
- React Router for navigation
|
||||
- CSS Modules for styling
|
||||
- LocalStorage for data persistence
|
||||
- Mock API for backend simulation
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v14 or later)
|
||||
- npm or yarn
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
|
||||
The app will be available at http://localhost:3000
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Header/
|
||||
│ ├── Footer/
|
||||
│ ├── Hero/
|
||||
│ ├── ProductCard/
|
||||
│ ├── Category/
|
||||
│ ├── CartItem/
|
||||
│ └── ProductList/
|
||||
├── pages/
|
||||
│ ├── Home/
|
||||
│ ├── Products/
|
||||
│ ├── ProductDetail/
|
||||
│ ├── Cart/
|
||||
│ ├── Checkout/
|
||||
│ ├── Login/
|
||||
│ └── Register/
|
||||
├── context/
|
||||
│ ├── AuthContext.js
|
||||
│ └── CartContext.js
|
||||
├── styles/
|
||||
│ └── globals.css
|
||||
└── App.js
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This project was built with modern React practices including:
|
||||
- Component-based architecture
|
||||
- Context API for state management
|
||||
- Responsive design principles
|
||||
- Form validation
|
||||
- Mock data layer for demonstration
|
||||
|
||||
## Deployment
|
||||
|
||||
To build for production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The build artifacts will be stored in the `build` folder.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "modern-ecommerce-store",
|
||||
"version": "1.0.0",
|
||||
"description": "A production-ready e-commerce application",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import './styles/globals.css';
|
||||
import Header from './components/Header/Header';
|
||||
import Footer from './components/Footer/Footer';
|
||||
import Home from './pages/Home/Home';
|
||||
import Products from './pages/Products/Products';
|
||||
import ProductDetail from './pages/ProductDetail/ProductDetail';
|
||||
import Cart from './pages/Cart/Cart';
|
||||
import Checkout from './pages/Checkout/Checkout';
|
||||
import Login from './pages/Login/Login';
|
||||
import Register from './pages/Register/Register';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import { CartProvider } from './context/CartContext';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<CartProvider>
|
||||
<Router>
|
||||
<div className="app">
|
||||
<Header />
|
||||
<main className="main-content">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/products" element={<Products />} />
|
||||
<Route path="/product/:id" element={<ProductDetail />} />
|
||||
<Route path="/cart" element={<Cart />} />
|
||||
<Route path="/checkout" element={<Checkout />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</CartProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,84 +0,0 @@
|
||||
.cart-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.cart-item-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.cart-item-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cart-item-name {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cart-item-price {
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quantity-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cart-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cart-item-image {
|
||||
margin-right: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import './CartItem.css';
|
||||
|
||||
const CartItem = ({ item }) => {
|
||||
const { updateQuantity, removeFromCart } = useCart();
|
||||
|
||||
const handleQuantityChange = (newQuantity) => {
|
||||
if (newQuantity >= 1) {
|
||||
updateQuantity(item.id, newQuantity);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cart-item">
|
||||
<img src={item.image} alt={item.name} className="cart-item-image" />
|
||||
<div className="cart-item-details">
|
||||
<h3 className="cart-item-name">{item.name}</h3>
|
||||
<p className="cart-item-price">${item.price.toFixed(2)}</p>
|
||||
<div className="quantity-controls">
|
||||
<button
|
||||
onClick={() => handleQuantityChange(item.quantity - 1)}
|
||||
className="quantity-btn"
|
||||
disabled={item.quantity <= 1}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="quantity">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => handleQuantityChange(item.quantity + 1)}
|
||||
className="quantity-btn"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="remove-btn"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CartItem;
|
||||
@@ -1,39 +0,0 @@
|
||||
.category {
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.category-image img {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ddd;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.category-image img:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.category-name {
|
||||
margin-top: 0.5rem;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.category a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.category-image img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './Category.css';
|
||||
|
||||
const Category = ({ category }) => {
|
||||
return (
|
||||
<div className="category">
|
||||
<Link to={`/products?category=${category.slug}`}>
|
||||
<div className="category-image">
|
||||
<img src={category.image} alt={category.name} />
|
||||
</div>
|
||||
<h3 className="category-name">{category.name}</h3>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
||||
@@ -1,56 +0,0 @@
|
||||
.footer {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer-section ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-section ul li a {
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.footer-section ul li a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-container {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import './Footer.css';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="footer-container">
|
||||
<div className="footer-section">
|
||||
<h3>ShopEasy</h3>
|
||||
<p>Your one-stop shop for all your needs. Quality products at affordable prices.</p>
|
||||
</div>
|
||||
|
||||
<div className="footer-section">
|
||||
<h4>Quick Links</h4>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href="/cart">Cart</a></li>
|
||||
<li><a href="/login">Login</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="footer-section">
|
||||
<h4>Contact Us</h4>
|
||||
<p>Email: info@shopeasy.com</p>
|
||||
<p>Phone: (123) 456-7890</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-bottom">
|
||||
<p>© 2026 ShopEasy. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,85 +0,0 @@
|
||||
.header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.user-greeting {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: none;
|
||||
border: 1px solid #007bff;
|
||||
color: #007bff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/AuthContext';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import './Header.css';
|
||||
|
||||
const Header = () => {
|
||||
const { user, logout } = useContext(AuthContext);
|
||||
const { getTotalItems } = useCart();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-container">
|
||||
<Link to="/" className="logo">
|
||||
<h2>ShopEasy</h2>
|
||||
</Link>
|
||||
|
||||
<nav className="nav">
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/products">Products</Link>
|
||||
<Link to="/cart">Cart ({getTotalItems()})</Link>
|
||||
|
||||
{user ? (
|
||||
<>
|
||||
<span className="user-greeting">Hello, {user.name}</span>
|
||||
<button onClick={handleLogout} className="logout-btn">Logout</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login" className="auth-link">Login</Link>
|
||||
<Link to="/register" className="auth-link">Register</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,57 +0,0 @@
|
||||
.hero {
|
||||
background: linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)), url('https://images.unsplash.com/photo-1607082350899-7e105aa886ae?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './Hero.css';
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section className="hero">
|
||||
<div className="hero-content">
|
||||
<h1>Welcome to ShopEasy</h1>
|
||||
<p>Your one-stop destination for all your shopping needs</p>
|
||||
<Link to="/products" className="cta-button">
|
||||
Shop Now
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
@@ -1,67 +0,0 @@
|
||||
.product-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.view-details-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.view-details-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.product-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './ProductCard.css';
|
||||
|
||||
const ProductCard = ({ product }) => {
|
||||
return (
|
||||
<div className="product-card">
|
||||
<img src={product.image} alt={product.name} className="product-image" />
|
||||
<div className="product-info">
|
||||
<h3 className="product-name">{product.name}</h3>
|
||||
<p className="product-description">{product.description}</p>
|
||||
<div className="product-price">${product.price.toFixed(2)}</div>
|
||||
<Link to={`/product/${product.id}`} className="view-details-btn">
|
||||
View Details
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
@@ -1,13 +0,0 @@
|
||||
.product-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.product-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import ProductCard from '../ProductCard/ProductCard';
|
||||
import './ProductList.css';
|
||||
|
||||
const ProductList = ({ products }) => {
|
||||
if (!products || products.length === 0) {
|
||||
return (
|
||||
<div className="product-list">
|
||||
<p>No products found.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="product-list">
|
||||
{products.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductList;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React, { createContext, useState, useContext } from 'react';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
const login = (userData) => {
|
||||
setUser(userData);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('user');
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||
|
||||
const CartContext = createContext();
|
||||
|
||||
export const useCart = () => {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error('useCart must be used within a CartProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const CartProvider = ({ children }) => {
|
||||
const [cartItems, setCartItems] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedCart = localStorage.getItem('cartItems');
|
||||
if (savedCart) {
|
||||
setCartItems(JSON.parse(savedCart));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cartItems', JSON.stringify(cartItems));
|
||||
}, [cartItems]);
|
||||
|
||||
const addToCart = (product, quantity = 1) => {
|
||||
setCartItems(prevItems => {
|
||||
const existingItem = prevItems.find(item => item.id === product.id);
|
||||
|
||||
if (existingItem) {
|
||||
return prevItems.map(item =>
|
||||
item.id === product.id
|
||||
? { ...item, quantity: item.quantity + quantity }
|
||||
: item
|
||||
);
|
||||
} else {
|
||||
return [...prevItems, { ...product, quantity }];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateQuantity = (id, newQuantity) => {
|
||||
if (newQuantity < 1) return;
|
||||
|
||||
setCartItems(prevItems =>
|
||||
prevItems.map(item =>
|
||||
item.id === id ? { ...item, quantity: newQuantity } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const removeFromCart = (id) => {
|
||||
setCartItems(prevItems => prevItems.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setCartItems([]);
|
||||
};
|
||||
|
||||
const getTotalItems = () => {
|
||||
return cartItems.reduce((total, item) => total + item.quantity, 0);
|
||||
};
|
||||
|
||||
const getTotalPrice = () => {
|
||||
return cartItems.reduce((total, item) => total + (item.price * item.quantity), 0);
|
||||
};
|
||||
|
||||
const value = {
|
||||
cartItems,
|
||||
addToCart,
|
||||
updateQuantity,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
getTotalItems,
|
||||
getTotalPrice
|
||||
};
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={value}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,104 +0,0 @@
|
||||
.cart {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cart-title {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cart-content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
background-color: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cart-summary {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.summary-row.total {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
border-bottom: none;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
width: 100%;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.checkout-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.cart-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.cart-empty h2 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cart-empty p {
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.continue-shopping-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.continue-shopping-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cart-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cart-summary {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import CartItem from '../../components/CartItem/CartItem';
|
||||
import './Cart.css';
|
||||
|
||||
const Cart = () => {
|
||||
const { cartItems, getTotalPrice } = useCart();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const total = getTotalPrice();
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (cartItems.length > 0) {
|
||||
navigate('/checkout');
|
||||
}
|
||||
};
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
return (
|
||||
<div className="cart-empty">
|
||||
<div className="container">
|
||||
<h2>Your Cart is Empty</h2>
|
||||
<p>You haven't added any items to your cart yet.</p>
|
||||
<button
|
||||
onClick={() => navigate('/products')}
|
||||
className="continue-shopping-btn"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cart">
|
||||
<div className="container">
|
||||
<h1 className="cart-title">Your Shopping Cart</h1>
|
||||
|
||||
<div className="cart-content">
|
||||
<div className="cart-items">
|
||||
{cartItems.map(item => (
|
||||
<CartItem key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cart-summary">
|
||||
<h3>Order Summary</h3>
|
||||
<div className="summary-row">
|
||||
<span>Subtotal:</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="summary-row">
|
||||
<span>Shipping:</span>
|
||||
<span>Free</span>
|
||||
</div>
|
||||
<div className="summary-row total">
|
||||
<span>Total:</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
className="checkout-btn"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cart;
|
||||
@@ -1,180 +0,0 @@
|
||||
.checkout {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.checkout-title {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.checkout-content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.checkout-form {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input.error {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.order-summary h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-items {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.summary-total {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.grand-total {
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.checkout-empty {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.checkout-empty h2 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.checkout-empty p {
|
||||
color: #666;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.continue-shopping-btn {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.continue-shopping-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.checkout-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './Checkout.css';
|
||||
|
||||
const Checkout = () => {
|
||||
const [cartItems, setCartItems] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
address: '',
|
||||
city: '',
|
||||
zipCode: '',
|
||||
cardNumber: '',
|
||||
expiryDate: '',
|
||||
cvv: ''
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const items = JSON.parse(localStorage.getItem('cartItems') || '[]');
|
||||
setCartItems(items);
|
||||
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
setTotal(total);
|
||||
}, []);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = 'First name is required';
|
||||
}
|
||||
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = 'Last name is required';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Email is invalid';
|
||||
}
|
||||
|
||||
if (!formData.address.trim()) {
|
||||
newErrors.address = 'Address is required';
|
||||
}
|
||||
|
||||
if (!formData.city.trim()) {
|
||||
newErrors.city = 'City is required';
|
||||
}
|
||||
|
||||
if (!formData.zipCode.trim()) {
|
||||
newErrors.zipCode = 'ZIP code is required';
|
||||
}
|
||||
|
||||
if (!formData.cardNumber.trim()) {
|
||||
newErrors.cardNumber = 'Card number is required';
|
||||
}
|
||||
|
||||
if (!formData.expiryDate.trim()) {
|
||||
newErrors.expiryDate = 'Expiry date is required';
|
||||
}
|
||||
|
||||
if (!formData.cvv.trim()) {
|
||||
newErrors.cvv = 'CVV is required';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validateForm()) {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
// Clear cart
|
||||
localStorage.removeItem('cartItems');
|
||||
setIsSubmitting(false);
|
||||
alert('Order placed successfully!');
|
||||
navigate('/');
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
return (
|
||||
<div className="checkout-empty">
|
||||
<div className="container">
|
||||
<h2>Empty Cart</h2>
|
||||
<p>Your cart is empty. Please add items before checking out.</p>
|
||||
<button
|
||||
onClick={() => navigate('/products')}
|
||||
className="continue-shopping-btn"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="checkout">
|
||||
<div className="container">
|
||||
<h1 className="checkout-title">Checkout</h1>
|
||||
|
||||
<div className="checkout-content">
|
||||
<form onSubmit={handleSubmit} className="checkout-form">
|
||||
<div className="form-section">
|
||||
<h2>Shipping Information</h2>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName">First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
className={errors.firstName ? 'error' : ''}
|
||||
/>
|
||||
{errors.firstName && <span className="error-message">{errors.firstName}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName">Last Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
className={errors.lastName ? 'error' : ''}
|
||||
/>
|
||||
{errors.lastName && <span className="error-message">{errors.lastName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address *</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={errors.email ? 'error' : ''}
|
||||
/>
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="address">Address *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
className={errors.address ? 'error' : ''}
|
||||
/>
|
||||
{errors.address && <span className="error-message">{errors.address}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="city">City *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="city"
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleChange}
|
||||
className={errors.city ? 'error' : ''}
|
||||
/>
|
||||
{errors.city && <span className="error-message">{errors.city}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="zipCode">ZIP Code *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="zipCode"
|
||||
name="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={handleChange}
|
||||
className={errors.zipCode ? 'error' : ''}
|
||||
/>
|
||||
{errors.zipCode && <span className="error-message">{errors.zipCode}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h2>Payment Information</h2>
|
||||
<div className="form-group">
|
||||
<label htmlFor="cardNumber">Card Number *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="cardNumber"
|
||||
name="cardNumber"
|
||||
value={formData.cardNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="1234 5678 9012 3456"
|
||||
className={errors.cardNumber ? 'error' : ''}
|
||||
/>
|
||||
{errors.cardNumber && <span className="error-message">{errors.cardNumber}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="expiryDate">Expiry Date *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="expiryDate"
|
||||
name="expiryDate"
|
||||
value={formData.expiryDate}
|
||||
onChange={handleChange}
|
||||
placeholder="MM/YY"
|
||||
className={errors.expiryDate ? 'error' : ''}
|
||||
/>
|
||||
{errors.expiryDate && <span className="error-message">{errors.expiryDate}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="cvv">CVV *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="cvv"
|
||||
name="cvv"
|
||||
value={formData.cvv}
|
||||
onChange={handleChange}
|
||||
placeholder="123"
|
||||
className={errors.cvv ? 'error' : ''}
|
||||
/>
|
||||
{errors.cvv && <span className="error-message">{errors.cvv}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-btn"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Processing...' : `Place Order - $${total.toFixed(2)}`}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="order-summary">
|
||||
<h2>Order Summary</h2>
|
||||
<div className="summary-items">
|
||||
{cartItems.map(item => (
|
||||
<div key={item.id} className="summary-item">
|
||||
<span>{item.name} x {item.quantity}</span>
|
||||
<span>${(item.price * item.quantity).toFixed(2)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="summary-total">
|
||||
<div className="total-row">
|
||||
<span>Subtotal:</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="total-row">
|
||||
<span>Shipping:</span>
|
||||
<span>Free</span>
|
||||
</div>
|
||||
<div className="total-row grand-total">
|
||||
<span>Grand Total:</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
@@ -1,18 +0,0 @@
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Hero from '../../components/Hero/Hero';
|
||||
import Category from '../../components/Category/Category';
|
||||
import ProductList from '../../components/ProductList/ProductList';
|
||||
import './Home.css';
|
||||
|
||||
const Home = () => {
|
||||
const [categories] = useState([
|
||||
{ id: 1, name: 'Electronics', slug: 'electronics', image: 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 2, name: 'Clothing', slug: 'clothing', image: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 3, name: 'Home & Kitchen', slug: 'home-kitchen', image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 4, name: 'Books', slug: 'books', image: 'https://images.unsplash.com/photo-1544947950-fa07a98d237f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
]);
|
||||
|
||||
const [featuredProducts] = useState([
|
||||
{ id: 1, name: 'Wireless Headphones', description: 'High-quality wireless headphones with noise cancellation', price: 129.99, image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 2, name: 'Smart Watch', description: 'Feature-rich smartwatch with health monitoring', price: 199.99, image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 3, name: 'Cotton T-Shirt', description: 'Comfortable cotton t-shirt for everyday wear', price: 24.99, image: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
{ id: 4, name: 'Coffee Maker', description: 'Automatic coffee maker with programmable timer', price: 89.99, image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80' },
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<Hero />
|
||||
|
||||
<div className="container">
|
||||
<h2 className="section-title">Shop by Category</h2>
|
||||
<div className="categories">
|
||||
{categories.map(category => (
|
||||
<Category key={category.id} category={category} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="section-title">Featured Products</h2>
|
||||
<ProductList products={featuredProducts} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,101 +0,0 @@
|
||||
.auth-page {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input.error {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-form {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { AuthContext } from '../../context/AuthContext';
|
||||
import './Login.css';
|
||||
|
||||
const Login = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { login } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Email is invalid';
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (formData.password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validateForm()) {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate API call
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Mock successful login
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: formData.email
|
||||
};
|
||||
|
||||
login(mockUser);
|
||||
setIsLoading(false);
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setErrors({ submit: 'Invalid email or password' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="container">
|
||||
<div className="auth-form">
|
||||
<h1 className="auth-title">Login to Your Account</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={errors.email ? 'error' : ''}
|
||||
/>
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className={errors.password ? 'error' : ''}
|
||||
/>
|
||||
{errors.password && <span className="error-message">{errors.password}</span>}
|
||||
</div>
|
||||
|
||||
{errors.submit && <div className="submit-error">{errors.submit}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-btn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Signing In...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="auth-links">
|
||||
<p>Don't have an account? <Link to="/register">Register here</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,141 +0,0 @@
|
||||
.product-detail {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: 1px solid #007bff;
|
||||
color: #007bff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.product-detail-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-category {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.quantity-selector {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.quantity-selector label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.quantity-display {
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.add-to-cart-btn {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-to-cart-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.product-detail-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '../../context/CartContext';
|
||||
import './ProductDetail.css';
|
||||
|
||||
const ProductDetail = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
const [product, setProduct] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
|
||||
// Mock product data
|
||||
const mockProducts = [
|
||||
{ id: 1, name: 'Wireless Headphones', description: 'High-quality wireless headphones with noise cancellation. Perfect for music lovers and professionals alike.', price: 129.99, image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'electronics' },
|
||||
{ id: 2, name: 'Smart Watch', description: 'Feature-rich smartwatch with health monitoring, GPS tracking, and smartphone integration.', price: 199.99, image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'electronics' },
|
||||
{ id: 3, name: 'Cotton T-Shirt', description: 'Comfortable cotton t-shirt for everyday wear. Available in multiple colors and sizes.', price: 24.99, image: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'clothing' },
|
||||
{ id: 4, name: 'Coffee Maker', description: 'Automatic coffee maker with programmable timer and thermal carafe. Brew perfect coffee every time.', price: 89.99, image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'home-kitchen' },
|
||||
{ id: 5, name: 'Bestseller Novel', description: 'Award-winning novel by popular author. A captivating story that keeps readers engaged from start to finish.', price: 14.99, image: 'https://images.unsplash.com/photo-1544947950-fa07a98d237f?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'books' },
|
||||
{ id: 6, name: 'Bluetooth Speaker', description: 'Portable speaker with excellent sound quality and long battery life. Perfect for outdoor adventures.', price: 79.99, image: 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'electronics' },
|
||||
{ id: 7, name: 'Jeans', description: 'Classic fit jeans for casual wear. Made with premium denim for comfort and durability.', price: 59.99, image: 'https://images.unsplash.com/photo-1541099649105-f69ad21f3246?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'clothing' },
|
||||
{ id: 8, name: 'Cookware Set', description: 'Complete cookware set for home cooking. Non-stick coating for easy cleaning and healthy cooking.', price: 149.99, image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80', category: 'home-kitchen' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate API call
|
||||
const fetchProduct = () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
const foundProduct = mockProducts.find(p => p.id === parseInt(id));
|
||||
if (foundProduct) {
|
||||
setProduct(foundProduct);
|
||||
} else {
|
||||
setError('Product not found');
|
||||
}
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
} catch (err) {
|
||||
setError('Failed to load product');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProduct();
|
||||
}, [id]);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToCart(product, quantity);
|
||||
alert(`${quantity} ${product.name}(s) added to cart!`);
|
||||
navigate('/cart');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading product...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return <div className="error">Product not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="product-detail">
|
||||
<div className="container">
|
||||
<button onClick={() => navigate(-1)} className="back-btn">
|
||||
← Back to Products
|
||||
</button>
|
||||
|
||||
<div className="product-detail-content">
|
||||
<div className="product-image">
|
||||
<img src={product.image} alt={product.name} />
|
||||
</div>
|
||||
|
||||
<div className="product-info">
|
||||
<h1 className="product-name">{product.name}</h1>
|
||||
<p className="product-category">{product.category}</p>
|
||||
<div className="product-price">${product.price.toFixed(2)}</div>
|
||||
<p className="product-description">{product.description}</p>
|
||||
|
||||
<div className="quantity-selector">
|
||||
<label htmlFor="quantity">Quantity:</label>
|
||||
<div className="quantity-controls">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="quantity-btn"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="quantity-display">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="quantity-btn"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="add-to-cart-btn"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetail;
|
||||
@@ -1,66 +0,0 @@
|
||||
.products-page {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filters {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filters h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-filters {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ProductList from '../../components/ProductList/ProductList';
|
||||
import './Products.css';
|
||||
|
||||
const Products = () => {
|
||||
const [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
|
||||
// Mock data for products
|
||||
const mockProducts = [
|
||||
{ id: 1, name: 'Wireless Headphones', description: 'High-quality wireless headphones with noise cancellation', price: 129.99, image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'electronics' },
|
||||
{ id: 2, name: 'Smart Watch', description: 'Feature-rich smartwatch with health monitoring', price: 199.99, image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'electronics' },
|
||||
{ id: 3, name: 'Cotton T-Shirt', description: 'Comfortable cotton t-shirt for everyday wear', price: 24.99, image: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'clothing' },
|
||||
{ id: 4, name: 'Coffee Maker', description: 'Automatic coffee maker with programmable timer', price: 89.99, image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'home-kitchen' },
|
||||
{ id: 5, name: 'Bestseller Novel', description: 'Award-winning novel by popular author', price: 14.99, image: 'https://images.unsplash.com/photo-1544947950-fa07a98d237f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'books' },
|
||||
{ id: 6, name: 'Bluetooth Speaker', description: 'Portable speaker with excellent sound quality', price: 79.99, image: 'https://images.unsplash.com/photo-1546868871-7041f2a55e12?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'electronics' },
|
||||
{ id: 7, name: 'Jeans', description: 'Classic fit jeans for casual wear', price: 59.99, image: 'https://images.unsplash.com/photo-1541099649105-f69ad21f3246?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'clothing' },
|
||||
{ id: 8, name: 'Cookware Set', description: 'Complete cookware set for home cooking', price: 149.99, image: 'https://images.unsplash.com/photo-1556911220-e15b29be8c8f?ixlib=rb-4.0.3&auto=format&fit=crop&w=200&q=80', category: 'home-kitchen' },
|
||||
];
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'All Products' },
|
||||
{ id: 'electronics', name: 'Electronics' },
|
||||
{ id: 'clothing', name: 'Clothing' },
|
||||
{ id: 'home-kitchen', name: 'Home & Kitchen' },
|
||||
{ id: 'books', name: 'Books' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate API call
|
||||
const fetchProducts = () => {
|
||||
try {
|
||||
setTimeout(() => {
|
||||
setProducts(mockProducts);
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
} catch (err) {
|
||||
setError('Failed to load products');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
const filteredProducts = selectedCategory === 'all'
|
||||
? products
|
||||
: products.filter(product => product.category === selectedCategory);
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading products...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="products-page">
|
||||
<div className="container">
|
||||
<h1 className="page-title">Our Products</h1>
|
||||
|
||||
<div className="filters">
|
||||
<h3>Filter by Category:</h3>
|
||||
<div className="category-filters">
|
||||
{categories.map(category => (
|
||||
<button
|
||||
key={category.id}
|
||||
className={`filter-btn ${selectedCategory === category.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProductList products={filteredProducts} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Products;
|
||||
@@ -1,112 +0,0 @@
|
||||
.auth-page {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input.error {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-form {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import './Register.css';
|
||||
|
||||
const Register = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = 'First name is required';
|
||||
}
|
||||
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = 'Last name is required';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Email is invalid';
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (formData.password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validateForm()) {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate API call
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Mock successful registration
|
||||
setIsLoading(false);
|
||||
alert('Registration successful! Please log in.');
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setErrors({ submit: 'Registration failed. Please try again.' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="container">
|
||||
<div className="auth-form">
|
||||
<h1 className="auth-title">Create an Account</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="firstName">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
className={errors.firstName ? 'error' : ''}
|
||||
/>
|
||||
{errors.firstName && <span className="error-message">{errors.firstName}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="lastName">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
className={errors.lastName ? 'error' : ''}
|
||||
/>
|
||||
{errors.lastName && <span className="error-message">{errors.lastName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={errors.email ? 'error' : ''}
|
||||
/>
|
||||
{errors.email && <span className="error-message">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className={errors.password ? 'error' : ''}
|
||||
/>
|
||||
{errors.password && <span className="error-message">{errors.password}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
className={errors.confirmPassword ? 'error' : ''}
|
||||
/>
|
||||
{errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
|
||||
</div>
|
||||
|
||||
{errors.submit && <div className="submit-error">{errors.submit}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-btn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Creating Account...' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="auth-links">
|
||||
<p>Already have an account? <Link to="/login">Sign in here</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
@@ -1,53 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: calc(100vh - 120px);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user