PIbd-22 UlybinAA Lab6 #8

Closed
qkrlnt wants to merge 8 commits from lab_6 into lab_5
43 changed files with 652 additions and 61 deletions

68
db.json
View File

@@ -3,12 +3,72 @@
{ {
"id": "1a54", "id": "1a54",
"name": "новый товар", "name": "новый товар",
"price": 102 "price": 102,
"image": "images/fork.jpg"
}, },
{ {
"id": "f4f8", "id": "f4f8",
"name": " товарчик", "name": "товарчик",
"price": 111 "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": "5.3.3",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
@@ -2152,6 +2153,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/corser": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
@@ -5107,6 +5117,44 @@
"node": ">=0.10.0" "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": { "node_modules/read-pkg": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -5411,6 +5459,12 @@
"semver": "bin/semver.js" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "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": "5.3.3",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",

View File

@@ -1,28 +1,26 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import useProducts from './hooks/useProducts'; import { BrowserRouter, Routes, Route } from "react-router-dom";
import ProductList from './components/ProductList'; import Header from "./components/Header";
import ProductForm from './components/ProductForm'; 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() { export default function App() {
const { products, add, update, remove } = useProducts();
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);
return ( return (
<div className="container my-4"> <BrowserRouter>
{/* <h1 className="mb-4">Каталог товаров</h1> */} <Header />
<button className="btn btn-success mb-3" onClick={handleAdd}>Добавить товар</button> <Routes>
{showForm && <ProductForm initial={editing} onSave={handleSave} onCancel={handleCancel} />} <Route path="/" element={<MainPage />} />
<ProductList products={products} onEdit={handleEdit} onDelete={handleDelete} /> <Route path="/basket" element={<BasketPage />} />
</div> <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,14 +1,20 @@
import React from 'react'; import React from 'react';
export default function ProductCard({ product, onEdit, onDelete }) { export default function ProductCard({ product, onEdit, onDelete, onAddToFavorites }) {
return ( return (
<div className="col"> <div className="col">
<div className="card h-100"> <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"> <div className="card-body">
<h5 className="card-title">{product.name}</h5> <h5 className="card-title">{product.name}</h5>
<p className="card-text">Цена: {product.price} </p> <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-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> </div>
</div> </div>

View File

@@ -1,30 +1,70 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
export default function ProductForm({ initial, onSave, onCancel }) { export default function ProductForm({ initial, onSave, onCancel }) {
const [form, setForm] = useState({ name: '', price: '' }); const [form, setForm] = useState({ name: '', price: '', image: '' });
useEffect(() => { 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]); }, [initial]);
const handleChange = e => setForm({ ...form, [e.target.name]: e.target.value }); const handleChange = e => setForm({ ...form, [e.target.name]: e.target.value });
const handleSubmit = e => { const handleSubmit = e => {
e.preventDefault(); e.preventDefault();
onSave({ ...initial, name: form.name, price: Number(form.price) }); onSave({ ...initial, name: form.name, price: Number(form.price), image: form.image });
setForm({ name: '', price: '' }); setForm({ name: '', price: '', image: '' });
}; };
return ( return (
<form onSubmit={handleSubmit} className="card p-3 mb-4"> <form onSubmit={handleSubmit} className="card p-3 mb-4">
<div className="mb-2"> <div className="mb-2">
<label className="form-label">Название</label> <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>
<div className="mb-2"> <div className="mb-2">
<label className="form-label">Цена</label> <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>
<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="submit" className="btn btn-primary mb-2" style={{ width: 'auto' }}>Сохранить</button>
<button type="button" className="btn btn-secondary" style={{ width: 'auto' }} onClick={onCancel}>Отмена</button> <button type="button" className="btn btn-secondary" style={{ width: 'auto' }} onClick={onCancel}>Отмена</button>
</form> </form>

View File

@@ -1,11 +1,17 @@
import React from 'react'; import React from 'react';
import ProductCard from './ProductCard'; import ProductCard from './ProductCard';
export default function ProductList({ products, onEdit, onDelete }) { export default function ProductList({ products, onEdit, onDelete, onAddToFavorites }) {
return ( return (
<div className="row row-cols-1 row-cols-md-3 g-4"> <div className="row row-cols-1 row-cols-md-3 g-4">
{products.map(prod => ( {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> </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 React from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import App from './App' import App from "./App";
import 'bootstrap/dist/css/bootstrap.min.css' // опционально import "bootstrap/dist/css/bootstrap.min.css";
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
const container = document.getElementById('root') const container = document.getElementById("root");
createRoot(container).render(<App />) 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: { server: {
open: '/newSite.html', open: '/',
}, },
resolve: { resolve: {
alias: { alias: {

BIN
Отчет4.docx Normal file

Binary file not shown.

BIN
Отчет5.docx Normal file

Binary file not shown.

BIN
Отчет6.docx Normal file

Binary file not shown.