8 Commits
lab_5 ... lab_6

Author SHA1 Message Date
qkrlnt
858bd39322 отчет маршруты 2025-05-29 03:34:27 +04:00
qkrlnt
4defafe3e7 отчет6 2025-05-28 19:49:57 +04:00
qkrlnt
da97a99436 доп 2025-05-27 15:39:50 +04:00
qkrlnt
d44d30cb05 nav 2025-05-27 11:38:59 +04:00
qkrlnt
ebbfcd0e93 changes 2025-05-24 10:45:37 +04:00
qkrlnt
e005c61030 done without .docx 2025-05-24 10:42:15 +04:00
cbda4dee82 working on.... 2025-05-23 22:33:12 +02:00
038f60d61e lab_6_start 2025-05-23 21:47:29 +02:00
41 changed files with 653 additions and 76 deletions

68
db.json
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
images/chery.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
images/child.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

BIN
images/fork.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
images/glasses.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
images/gmail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

BIN
images/iron.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
images/knife.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
images/masha.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
images/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

BIN
images/screwdriver.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
images/skateboard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
images/spoon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
images/telegram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/vanadiy.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/vk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
images/бананы.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

15
index.html Normal file
View 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>

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View 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
View 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>
);
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -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
View 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 };
}

View 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
View 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
View 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 };
}

View File

@@ -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
View 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
View 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>
);
}

View 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
View 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
View 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>
);
}

View File

@@ -18,7 +18,7 @@ export default defineConfig({
},
},
server: {
open: '/newSite.html',
open: '/',
},
resolve: {
alias: {

BIN
Отчет6.docx Normal file

Binary file not shown.