Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
858bd39322 | ||
|
|
4defafe3e7 | ||
|
|
da97a99436 | ||
|
|
d44d30cb05 | ||
|
|
ebbfcd0e93 | ||
|
|
e005c61030 | ||
| cbda4dee82 | |||
| 038f60d61e |
68
db.json
@@ -3,12 +3,72 @@
|
||||
{
|
||||
"id": "1a54",
|
||||
"name": "новый товар",
|
||||
"price": 102
|
||||
"price": 102,
|
||||
"image": "images/fork.jpg"
|
||||
},
|
||||
{
|
||||
"id": "f4f8",
|
||||
"name": " товарчик",
|
||||
"price": 111
|
||||
"name": "товарчик",
|
||||
"price": 111,
|
||||
"image": "images/chery.jpg"
|
||||
},
|
||||
{
|
||||
"id": "0fda",
|
||||
"name": "ложка",
|
||||
"price": 48,
|
||||
"image": "images/bananas.jpg"
|
||||
}
|
||||
]
|
||||
],
|
||||
"basket": [
|
||||
{
|
||||
"id": "0fda",
|
||||
"name": "ложка",
|
||||
"price": 48,
|
||||
"image": "images/bananas.jpg"
|
||||
}
|
||||
],
|
||||
"favorites": [
|
||||
{
|
||||
"id": "1a54",
|
||||
"name": "новый товар",
|
||||
"price": 102,
|
||||
"image": "images/fork.jpg"
|
||||
}
|
||||
],
|
||||
"orders": [
|
||||
{
|
||||
"id": "1",
|
||||
"items": [
|
||||
{
|
||||
"name": "Ложка",
|
||||
"image": "images/spoon.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Вилка",
|
||||
"image": "images/fork.jpg"
|
||||
}
|
||||
],
|
||||
"status": "in-process"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"items": [
|
||||
{
|
||||
"name": "Утюг",
|
||||
"image": "images/iron.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Бананы",
|
||||
"image": "images/bananas.jpg"
|
||||
}
|
||||
],
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"profile": {
|
||||
"id": 1,
|
||||
"firstName": "Иван",
|
||||
"lastName": "Иванов",
|
||||
"image": "images/бананы.jpg"
|
||||
}
|
||||
}
|
||||
BIN
images/bananas.jpg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
images/chery.jpg
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
images/child.jpg
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
images/fork.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
images/glasses.jpg
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
images/gmail.png
Normal file
|
After Width: | Height: | Size: 781 B |
BIN
images/iron.jpg
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
images/knife.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
images/logo.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/masha.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
images/phone.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
images/screwdriver.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
images/skateboard.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
images/spoon.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
images/telegram.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
images/vanadiy.jpg
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
images/vk.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
images/бананы.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<title>Интернет-магазин</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
15
newSite.html
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<title>Интернет-магазин</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<h2 class="text-center my-3">Рекомендуемые товары:</h2>
|
||||
<div id="root"></div>
|
||||
</main>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
56
package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"bootstrap": "5.3.3",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
@@ -2152,6 +2153,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
@@ -5107,6 +5117,44 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
||||
@@ -5411,6 +5459,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"bootstrap": "5.3.3",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
|
||||
51
src/App.jsx
@@ -1,39 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import useProducts from './hooks/useProducts';
|
||||
import ProductList from './components/ProductList';
|
||||
import ProductForm from './components/ProductForm';
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import MainPage from "./pages/MainPage";
|
||||
import BasketPage from "./pages/BasketPage";
|
||||
import FavoritesPage from "./pages/FavoritesPage";
|
||||
import OrderPage from "./pages/OrderPage";
|
||||
import AccountPage from "./pages/AccountPage";
|
||||
|
||||
export default function App() {
|
||||
const { products, add, update, remove } = useProducts();
|
||||
const [editing, setEditing] = useState(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const [sortOrder, setSortOrder] = useState('asc');
|
||||
|
||||
const handleAdd = () => { setEditing(null); setShowForm(true); };
|
||||
const handleEdit = prod => { setEditing(prod); setShowForm(true); };
|
||||
const handleDelete = id => remove(id);
|
||||
const handleSave = prod => {
|
||||
editing ? update({ ...prod, id: editing.id }) : add(prod);
|
||||
setShowForm(false);
|
||||
};
|
||||
const handleCancel = () => setShowForm(false);
|
||||
|
||||
const sortedProducts = [...products].sort((a, b) =>
|
||||
sortOrder === 'asc'
|
||||
? a.price - b.price
|
||||
: b.price - a.price
|
||||
);
|
||||
|
||||
const toggleSortOrder = () => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
|
||||
|
||||
return (
|
||||
<div className="container my-4">
|
||||
{/* <h1 className="mb-4">Каталог товаров</h1> */}
|
||||
<button className="btn btn-success mb-3" onClick={handleAdd}>Добавить товар</button>
|
||||
<button className="btn btn-outline-primary mb-3 ms-3" onClick={toggleSortOrder}> Сортировать по цене {sortOrder === 'asc' ? '▲' : '▼'}</button>
|
||||
{showForm && <ProductForm initial={editing} onSave={handleSave} onCancel={handleCancel} mode={editing ? 'edit' : 'add'} />}
|
||||
<ProductList products={sortedProducts} onEdit={handleEdit} onDelete={handleDelete} />
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/basket" element={<BasketPage />} />
|
||||
<Route path="/favorites" element={<FavoritesPage />} />
|
||||
<Route path="/orders" element={<OrderPage />} />
|
||||
<Route path="/account" element={<AccountPage />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
29
src/components/Footer.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="container mt-5">
|
||||
<div className="bg-light p-4">
|
||||
<h5>Помощь:</h5>
|
||||
<div className="d-flex flex-wrap">
|
||||
<div className="d-flex align-items-center me-4 mb-3">
|
||||
<i className="bi bi-telephone-fill me-2"></i>
|
||||
<a href="#" className="text-decoration-none text-dark">8 (800)-555-35-35</a>
|
||||
</div>
|
||||
<div className="d-flex align-items-center me-4 mb-3">
|
||||
<img src="images/vk.png" alt="VK" className="me-2" style={{ width: 24, height: 24 }} />
|
||||
<a href="https://vk.com/howmakesite_nn?from=search" target="_blank" className="text-decoration-none text-dark">vk.com</a>
|
||||
</div>
|
||||
<div className="d-flex align-items-center me-4 mb-3">
|
||||
<img src="images/telegram.png" alt="Telegram" className="me-2" style={{ width: 24, height: 24 }} />
|
||||
<a href="#" className="text-decoration-none text-dark">tg.me</a>
|
||||
</div>
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<img src="images/gmail.png" alt="Gmail" className="me-2" style={{ width: 24, height: 24 }} />
|
||||
<a href="mailto:ozon-zon-zon@mail.joke" className="text-decoration-none text-dark">ozon-zon-zon@mail.joke</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
28
src/components/Header.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header>
|
||||
<div className="d-block mt-3 ms-3">
|
||||
<img src="images/logo.jpg" alt="Название магазина" className="me-3" style={{ width: 200, height: "auto" }} />
|
||||
<Link to="/" className="text-decoration-none text-dark m-1">
|
||||
<h1 className="display-4 h3 mt-3"><b>Название магазина</b></h1>
|
||||
</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<div className="dropdown">
|
||||
<button className="btn btn-primary dropdown-toggle ms-3" type="button" id="navigationDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Навигация
|
||||
</button>
|
||||
<ul className="dropdown-menu" aria-labelledby="navigationDropdown">
|
||||
<li><Link className="dropdown-item d-flex justify-content-between align-items-center" to="/account">Личный кабинет<i className="bi bi-person-circle ms-2"></i></Link></li>
|
||||
<li><Link className="dropdown-item d-flex justify-content-between align-items-center" to="/basket">Корзина<i className="bi bi-cart4 ms-2"></i></Link></li>
|
||||
<li><Link className="dropdown-item d-flex justify-content-between align-items-center" to="/orders">Заказы<i className="bi bi-receipt ms-2"></i></Link></li>
|
||||
<li><Link className="dropdown-item d-flex justify-content-between align-items-center" to="/favorites">Избранное<i className="bi bi-heart-fill ms-2"></i></Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ProductCard({ product, onEdit, onDelete }) {
|
||||
export default function ProductCard({ product, onEdit, onDelete, onAddToFavorites }) {
|
||||
return (
|
||||
<div className="col">
|
||||
<div className="card h-100">
|
||||
{product.image && (
|
||||
<img src={product.image} className="card-img-top" alt={product.name} style={{ height: 300, objectFit: 'cover' }} />
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{product.name}</h5>
|
||||
<p className="card-text">Цена: {product.price} ₽</p>
|
||||
<button className="btn btn-sm btn-outline-primary me-2" onClick={() => onEdit(product)}>Изменить</button>
|
||||
<button className="btn btn-sm btn-outline-danger" onClick={() => onDelete(product.id)}>Удалить</button>
|
||||
<button className="btn btn-sm btn-outline-danger me-2" onClick={() => onDelete(product.id)}>Удалить</button>
|
||||
<button className="btn btn-sm btn-outline-success" onClick={() => onAddToFavorites(product)}>
|
||||
<i className="bi bi-heart"></i> В избранное
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default function ProductForm({ initial, onSave, onCancel, mode }) {
|
||||
const [form, setForm] = useState({ name: '', price: '' });
|
||||
export default function ProductForm({ initial, onSave, onCancel }) {
|
||||
const [form, setForm] = useState({ name: '', price: '', image: '' });
|
||||
|
||||
useEffect(() => {
|
||||
if (initial) setForm({ name: initial.name, price: initial.price });
|
||||
if (initial) {
|
||||
setForm({
|
||||
name: initial.name || '',
|
||||
price: initial.price || '',
|
||||
image: initial.image || ''
|
||||
});
|
||||
} else {
|
||||
setForm({ name: '', price: '', image: '' });
|
||||
}
|
||||
}, [initial]);
|
||||
|
||||
const handleChange = e => setForm({ ...form, [e.target.name]: e.target.value });
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
onSave({ ...initial, name: form.name, price: Number(form.price) });
|
||||
setForm({ name: '', price: '' });
|
||||
onSave({ ...initial, name: form.name, price: Number(form.price), image: form.image });
|
||||
setForm({ name: '', price: '', image: '' });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="card p-3 mb-4">
|
||||
<h4 className="mb-3">
|
||||
{mode === 'edit' ? 'Редактирование товара' : 'Добавление товара'}
|
||||
</h4>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Название</label>
|
||||
<input name="name" value={form.name} onChange={handleChange} required className="form-control" />
|
||||
<input
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Цена</label>
|
||||
<input name="price" value={form.price} onChange={handleChange} type="number" required className="form-control" />
|
||||
<input
|
||||
name="price"
|
||||
value={form.price}
|
||||
onChange={handleChange}
|
||||
type="number"
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Ссылка на картинку</label>
|
||||
<input
|
||||
name="image"
|
||||
value={form.image}
|
||||
onChange={handleChange}
|
||||
className="form-control"
|
||||
placeholder="Например: images/glasses.jpg или https://example.com/photo.jpg"
|
||||
/>
|
||||
</div>
|
||||
{form.image && (
|
||||
<div className="mb-2 text-center">
|
||||
<img
|
||||
src={form.image}
|
||||
alt="Превью"
|
||||
style={{ maxHeight: 120, objectFit: 'contain', maxWidth: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button type="submit" className="btn btn-primary mb-2" style={{ width: 'auto' }}>Сохранить</button>
|
||||
<button type="button" className="btn btn-secondary" style={{ width: 'auto' }} onClick={onCancel}>Отмена</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React from 'react';
|
||||
import ProductCard from './ProductCard';
|
||||
|
||||
export default function ProductList({ products, onEdit, onDelete }) {
|
||||
export default function ProductList({ products, onEdit, onDelete, onAddToFavorites }) {
|
||||
return (
|
||||
<div className="row row-cols-1 row-cols-md-3 g-4">
|
||||
{products.map(prod => (
|
||||
<ProductCard key={prod.id} product={prod} onEdit={onEdit} onDelete={onDelete} />
|
||||
<ProductCard
|
||||
key={prod.id}
|
||||
product={prod}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onAddToFavorites={onAddToFavorites}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
35
src/hooks/useBasket.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useBasket() {
|
||||
const [basket, setBasket] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:5000/basket')
|
||||
.then(res => res.json())
|
||||
.then(setBasket);
|
||||
}, []);
|
||||
|
||||
const addToBasket = async (item) => {
|
||||
const res = await fetch('http://localhost:5000/basket', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item)
|
||||
});
|
||||
const newItem = await res.json();
|
||||
setBasket([...basket, newItem]);
|
||||
};
|
||||
|
||||
const removeFromBasket = async (id) => {
|
||||
await fetch(`http://localhost:5000/basket/${id}`, { method: 'DELETE' });
|
||||
setBasket(basket.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
const clearBasket = async () => {
|
||||
for (let item of basket) {
|
||||
await fetch(`http://localhost:5000/basket/${item.id}`, { method: 'DELETE' });
|
||||
}
|
||||
setBasket([]);
|
||||
};
|
||||
|
||||
return { basket, addToBasket, removeFromBasket, clearBasket };
|
||||
}
|
||||
28
src/hooks/useFavorites.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useFavorites() {
|
||||
const [favorites, setFavorites] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:5000/favorites')
|
||||
.then(res => res.json())
|
||||
.then(setFavorites);
|
||||
}, []);
|
||||
|
||||
const addToFavorites = async (item) => {
|
||||
const res = await fetch('http://localhost:5000/favorites', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item)
|
||||
});
|
||||
const newItem = await res.json();
|
||||
setFavorites([...favorites, newItem]);
|
||||
};
|
||||
|
||||
const removeFromFavorites = async (id) => {
|
||||
await fetch(`http://localhost:5000/favorites/${id}`, { method: 'DELETE' });
|
||||
setFavorites(favorites.filter(item => item.id !== id));
|
||||
};
|
||||
|
||||
return { favorites, addToFavorites, removeFromFavorites };
|
||||
}
|
||||
26
src/hooks/useOrders.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useOrders() {
|
||||
const [orders, setOrders] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:5000/orders')
|
||||
.then(res => res.json())
|
||||
.then(setOrders);
|
||||
}, []);
|
||||
|
||||
const addOrder = async (order) => {
|
||||
const res = await fetch('http://localhost:5000/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(order)
|
||||
});
|
||||
const newOrder = await res.json();
|
||||
setOrders([...orders, newOrder]);
|
||||
};
|
||||
|
||||
const inProcess = orders.filter(o => o.status === "in-process");
|
||||
const completed = orders.filter(o => o.status === "completed");
|
||||
|
||||
return { orders, addOrder, inProcess, completed };
|
||||
}
|
||||
24
src/hooks/useProfile.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useProfile() {
|
||||
const [profile, setProfile] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:5000/profile')
|
||||
.then(res => res.json())
|
||||
.then(setProfile);
|
||||
}, []);
|
||||
|
||||
const updateProfile = async (newProfile) => {
|
||||
// PATCH или PUT — по ситуации
|
||||
const res = await fetch('http://localhost:5000/profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newProfile)
|
||||
});
|
||||
const updated = await res.json();
|
||||
setProfile(updated);
|
||||
};
|
||||
|
||||
return { profile, updateProfile };
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css' // опционально
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
|
||||
const container = document.getElementById('root')
|
||||
createRoot(container).render(<App />)
|
||||
const container = document.getElementById("root");
|
||||
createRoot(container).render(<App />);
|
||||
76
src/pages/AccountPage.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from "react";
|
||||
import useProfile from "../hooks/useProfile";
|
||||
|
||||
export default function AccountPage() {
|
||||
const { profile, updateProfile } = useProfile();
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [form, setForm] = useState(null);
|
||||
|
||||
if (!profile) {
|
||||
return <div className="text-center">Загрузка...</div>;
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
setForm(profile);
|
||||
setShowEdit(true);
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleSave = async e => {
|
||||
e.preventDefault();
|
||||
await updateProfile(form);
|
||||
setShowEdit(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => setShowEdit(false);
|
||||
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="card text-center mx-auto" style={{ maxWidth: 400, maxHeight: 400 }}>
|
||||
<img src={profile.image} className="card-img-top" alt="Профиль" style={{ width: "100%", height: 300, objectFit: "cover" }} />
|
||||
<div className="card-body">
|
||||
<h3 className="card-title">{profile.firstName} {profile.lastName}</h3>
|
||||
<button className="btn btn-primary" onClick={handleEdit}>
|
||||
<i className="bi bi-pencil"></i> Редактировать профиль
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEdit && (
|
||||
<div className="modal d-block" tabIndex="-1" style={{ background: 'rgba(0,0,0,0.3)' }}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Редактировать профиль</h5>
|
||||
<button type="button" className="btn-close" onClick={handleCancel}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Имя</label>
|
||||
<input className="form-control" name="firstName" value={form.firstName} onChange={handleChange} required />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Фамилия</label>
|
||||
<input className="form-control" name="lastName" value={form.lastName} onChange={handleChange} required />
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">URL аватара</label>
|
||||
<input className="form-control" name="image" value={form.image} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="submit" className="btn btn-success">Сохранить</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCancel}>Отмена</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/pages/BasketPage.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import useBasket from "../hooks/useBasket";
|
||||
import useOrders from "../hooks/useOrders";
|
||||
|
||||
export default function BasketPage() {
|
||||
|
||||
const { basket, removeFromBasket, clearBasket } = useBasket();
|
||||
const { addOrder } = useOrders();
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (basket.length === 0) {
|
||||
alert("Корзина пуста");
|
||||
return;
|
||||
}
|
||||
// Сформируем заказ
|
||||
addOrder({
|
||||
items: basket,
|
||||
status: "in-process"
|
||||
});
|
||||
clearBasket(); // Очищаем корзину
|
||||
alert("Заказ оформлен!");
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container d-flex justify-content-center align-items-center" style={{ minHeight: "60vh" }}>
|
||||
<div className="card p-4 shadow" style={{ minWidth: "60vw" }}>
|
||||
<h2 className="text-center">Корзина</h2>
|
||||
<ul className="list-group list-group-flush">
|
||||
{basket.map(item => (
|
||||
<li className="list-group-item d-flex align-items-center" key={item.id}>
|
||||
<img src={item.image} alt={item.name} style={{ width: 100, height: 100 }} className="me-3" />
|
||||
{item.name} <span className="ms-auto">{item.price} руб.</span>
|
||||
<button onClick={() => removeFromBasket(item.id)} className="btn btn-danger ms-2">Удалить</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="text-center mt-3">
|
||||
<button className={`btn w-100 ${basket.length === 0 ? "btn-secondary" : "btn-success"}`} onClick={handleCheckout}>
|
||||
Оплатить {}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
34
src/pages/FavoritesPage.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import useBasket from "../hooks/useBasket";
|
||||
import useFavorites from "../hooks/useFavorites";
|
||||
|
||||
export default function FavoritesPage() {
|
||||
const { favorites, removeFromFavorites } = useFavorites();
|
||||
const { basket, addToBasket } = useBasket();
|
||||
|
||||
const handleAddToBasket = (item) => {
|
||||
addToBasket(item);
|
||||
removeFromFavorites(item.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h2 className="mb-4">Избранное</h2>
|
||||
<div className="row row-cols-1 row-cols-md-2 g-4">
|
||||
{favorites.map(item => (
|
||||
<div className="col" key={item.id}>
|
||||
<div className="card mx-auto" style={{ width: "70%" }}>
|
||||
<img src={item.image} className="card-img-top" alt={item.name} style={{ width: "100%", height: 300, objectFit: "cover" }} />
|
||||
<div className="card-body text-center">
|
||||
<h5 className="card-title">{item.name}</h5>
|
||||
<p className="card-text">{item.price}</p>
|
||||
<button className="btn btn-success me-2" onClick={() => handleAddToBasket(item)}>В корзину</button>
|
||||
<button onClick={() => removeFromFavorites(item.id)} className="btn btn-danger">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
src/pages/MainPage.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from "react";
|
||||
import useProducts from "../hooks/useProducts";
|
||||
import useFavorites from "../hooks/useFavorites";
|
||||
import ProductList from "../components/ProductList";
|
||||
import ProductForm from "../components/ProductForm";
|
||||
|
||||
export default function MainPage() {
|
||||
const { products, add, update, remove } = useProducts();
|
||||
const { favorites, addToFavorites } = useFavorites();
|
||||
const [editing, setEditing] = useState(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
const handleAdd = () => { setEditing(null); setShowForm(true); };
|
||||
const handleEdit = prod => { setEditing(prod); setShowForm(true); };
|
||||
const handleDelete = id => remove(id);
|
||||
const handleSave = prod => {
|
||||
editing ? update({ ...prod, id: editing.id }) : add(prod);
|
||||
setShowForm(false);
|
||||
};
|
||||
const handleCancel = () => setShowForm(false);
|
||||
|
||||
const handleAddToFavorites = product => {
|
||||
if (!favorites.some(fav => fav.id === product.id)) {
|
||||
addToFavorites(product);
|
||||
}
|
||||
else alert('Товар уже в избранном!');
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container my-4">
|
||||
<button className="btn btn-success mb-3" onClick={handleAdd}>Добавить товар</button>
|
||||
{showForm && <ProductForm initial={editing} onSave={handleSave} onCancel={handleCancel} />}
|
||||
<h2 className="text-center my-3">Рекомендуемые товары:</h2>
|
||||
<ProductList
|
||||
products={products}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onAddToFavorites={handleAddToFavorites}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
58
src/pages/OrderPage.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import useOrders from "../hooks/useOrders";
|
||||
|
||||
export default function OrderPage() {
|
||||
const { inProcess, completed } = useOrders();
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1 className="text-center">Заказы</h1>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header bg-warning text-dark">
|
||||
<h2 className="h5 m-0">В процессе</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{inProcess.length === 0 ? (
|
||||
<div className="text-center text-muted">Нет заказов</div>
|
||||
) : (
|
||||
inProcess.map(order => (
|
||||
<ul className="list-group list-group-flush mb-3" key={order.id}>
|
||||
{order.items.map((item, idx) => (
|
||||
<li className="list-group-item d-flex align-items-center mb-2" key={idx}>
|
||||
<img src={item.image} className="me-2" style={{ width: 120, height: 120, objectFit: "cover" }} alt={item.name} /> {item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header bg-success text-white">
|
||||
<h2 className="h5 m-0">Завершённые</h2>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{completed.length === 0 ? (
|
||||
<div className="text-center text-muted">Нет завершённых заказов</div>
|
||||
) : (
|
||||
completed.map(order => (
|
||||
<ul className="list-group list-group-flush mb-3" key={order.id}>
|
||||
{order.items.map((item, idx) => (
|
||||
<li className="list-group-item d-flex align-items-center mb-2" key={idx}>
|
||||
<img src={item.image} className="me-2" style={{ width: 120, height: 120, objectFit: "cover" }} alt={item.name} /> {item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
open: '/newSite.html',
|
||||
open: '/',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||