15 Commits
lab_1 ... 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
d1b2cea66c minimal 2025-05-23 17:43:10 +02:00
311491d554 some changes 2025-05-23 17:33:42 +02:00
5fa65ad591 lab_5 2025-05-22 15:18:29 +02:00
qkrlnt
9c80373c59 lab_4 2025-05-20 16:04:25 +04:00
qkrlnt
49fe7787f3 lab_3_done 2025-05-20 15:13:16 +04:00
qkrlnt
65407801d3 lab_3 2025-05-20 14:58:51 +04:00
qkrlnt
0f37299eab lab_2_done 2025-05-20 14:03:18 +04:00
38 changed files with 7147 additions and 415 deletions

54
.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Зависимости Node.js
/node_modules/
# Результаты сборки Vite
/dist/
# Логи
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
*.log
# Файлы окружения
.env
.env.*.local
# Кеши
/.cache/
.vite/
# IDE и редакторы
.vscode/
.idea/
/*.sublime-workspace
/*.sublime-project
# Системные файлы
.DS_Store
Thumbs.db
# Отчеты об покрытии тестами
/coverage/
# Порты и артефакты
*.pid
*.seed
*.pid.lock
# Прочее
/.DS_Store

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Интернет-магазин: ЛК</title>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<header>
<img src="images/logo.jpg" alt="'Название магазина'">
<a href="newSite.html"><h1><b>"Название магазина"</b></h1></a>
<navbar>
<p><a href="Basket.html">Корзина</a></p>
<p><a href="Order.html">Заказы</a></p>
<p><a href="Favorites.html">Избранное</a></p>
<p><a href="Account.html">Личный кабинет</a></p>
</navbar>
</header>
<div class="account">
<img src="images/бананы.jpg">
<h3>Имя Фамилия</h3>
<p>Описание</p>
</div>
<footer>
<p>Помощь:</p>
<p>8 (800)-555-35-35</p>
<p>vk.com</p>
<p>tg.me</p>
<p>ozon-zon-zon@mail.joke</p>
</footer>
</body>
</html>

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Интернет-магазин: Корзина</title>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<header>
<img src="images/logo.jpg" alt="'Название магазина'">
<a href="newSite.html"><h1><b>"Название магазина"</b></h1></a>
<navbar>
<p><a href="Basket.html">Корзина</a></p>
<p><a href="Order.html">Заказы</a></p>
<p><a href="Favorites.html">Избранное</a></p>
<p><a href="Account.html">Личный кабинет</a></p>
</navbar>
</header>
<div class="basket">
<h1>Корзина</h1>
<ol>
<li><img src="images/glasses.jpg">Очки<p>349 руб.</p></li>
<li><img src="images/chery.jpg">Chery Tiggo 7 Pro Max 64gb 128mp<p>5 руб.</p></li>
<li><img src="images/vanadiy.jpg">Ванадий<p>2099 руб.</p></li>
</ol>
</div>
<div class="buy">
<a href="https://xn----7sbon6aucai8a.xn--p1ai/wa-data/public/shop/products/98/27/2798/images/6060/6060.970.jpg" target="_blank">
Оплатить
</a>
</div>
<footer>
<p>Помощь:</p>
<p>8 (800)-555-35-35</p>
<p>vk.com</p>
<p>tg.me</p>
<p>ozon-zon-zon@mail.joke</p>
</footer>
</body>
</html>

View File

@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Интернет-магазин: Избранное</title>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<header>
<img src="images/logo.jpg" alt="'Название магазина'">
<a href="newSite.html"><h1><b>"Название магазина"</b></h1></a>
<navbar>
<p><a href="Basket.html">Корзина</a></p>
<p><a href="Order.html">Заказы</a></p>
<p><a href="Favorites.html">Избранное</a></p>
<p><a href="Account.html">Личный кабинет</a></p>
</navbar>
</header>
<div class="favourites">
<h2>Избранное</h2>
<ul>
<li><img src ="images/masha.jpg">Женщина<p>бесценна</p></li>
<li><img src ="images/screwdriver.jpg">Отвертка<p>219 руб</p></li>
</ul>
</div>
<footer>
<p>Помощь:</p>
<p>8 (800)-555-35-35</p>
<p>vk.com</p>
<p>tg.me</p>
<p>ozon-zon-zon@mail.joke</p>
</footer>
</body>
</html>

View File

@@ -1,45 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Интернет-магазин: Заказы</title>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<header>
<img src="images/logo.jpg" alt="'Название магазина'">
<a href="newSite.html"><h1><b>"Название магазина"</b></h1></a>
<navbar>
<p><a href="Basket.html">Корзина</a></p>
<p><a href="Order.html">Заказы</a></p>
<p><a href="Favorites.html">Избранное</a></p>
<p><a href="Account.html">Личный кабинет</a></p>
</navbar>
</header>
<div class="orders"><h1>Заказы</h1></div>
<div class="typeOfOrder">
<div class="inProcess">
<h2>В процессе</h2>
<ul>
<li><img src="images/spoon.jpg">Ложка</li>
<li><img src="images/fork.jpg">Вилка</li>
<li><img src="images/knife.jpg">Нож</li>
</ul>
</div>
<div class="done">
<h2>Завершённые</h2>
<ul>
<li><img src="images/iron.jpg">Утюг</li>
<li><img src="images/bananas.jpg">Бананы</li>
</ul>
</div>
</div>
<footer>
<p>Помощь:</p>
<p>8 (800)-555-35-35</p>
<p>vk.com</p>
<p>tg.me</p>
<p>ozon-zon-zon@mail.joke</p>
</footer>
</body>
</html>

View File

@@ -1,228 +0,0 @@
body {
background-color: #edf7f6;
}
* {
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
header {
margin-bottom: 50px;
}
img {
width: 180px;
height: 150px;
margin-right: 10px;
}
h2 {
text-align: center;
}
footer {
margin-top: 50px;
text-align: center;
font-size: 14px;
}
a {
text-decoration: none;
color: black;
font-weight: bold;
}
header h1 a{
text-decoration: none;
color: black;
font-weight: bold;
text-align: left;
}
header img {
width: 200px;
height: auto;
}
navbar {
display: flex;
gap: 15px;
position: fixed;
top: 10px;
right: 35px;
}
.links a {
text-decoration: none;
color: black;
font-weight: bold;
}
.goods {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-bottom: 70px;
}
.goods p {
text-align: center;
width: 270px;
height: 285px;
border: 2px solid black;
border-radius: 10px;
}
.goods img {
width: 250px;
height: 250px;
object-fit: cover;
margin: 0 auto; /* Центрирование */
}
.typeOfOrder {
display: flex;
justify-content: space-around;
}
.typeOfOrder h2{
border: 2px solid black;
border-radius: 5px;
width: 180px;
margin-left: auto;
}
.inProcess h2{
background-color: yellow;
}
.done h2{
background-color: green;
}
.basket {
margin-left: 50px;
}
.basket h1 {
margin-left: 50px;
}
li {
position: relative;
border: 2px solid black;
border-radius: 10px;
width: 400px;
height: auto;
display: flex;
font-size: large;
margin-bottom: 15px;
padding: 10px;
}
.basket p {
position: absolute;
bottom: 5px;
right: 10px;
margin: 0;
font-weight: bold;
}
.orders h1 {
margin-left: 50px;
}
.favourites p {
position: absolute;
bottom: 5px;
right: 10px;
margin: 0;
font-weight: bold;
}
/*.buy {
position: fixed;
bottom: 20px;
right: 20px;
background-color: greenyellow;
text-align: center;
border: 2px solid black;
border-radius: 10px;
width: 200px; /* Фиксированный размер кнопки *
height: 70px;
display: flex;
justify-content: center;
}
.buy a {
font-size: xx-large;
text-decoration: none;
color: black;
font-weight: bold;
width: 100%; /* Растягиваем ссылку на всю кнопку *
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}*/
.buy {
position: fixed;
bottom: 20px;
right: 20px;
background-color: greenyellow;
text-align: center;
border: 2px solid black;
border-radius: 10px;
width: 200px;
height: 60px;
display: flex;
transition: background-color 0.3s ease-in-out;
}
.buy a {
text-decoration: none;
color: black;
font-size: xx-large;
font-weight: bold;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.buy:hover {
background-color: lightgreen;
}
.account {
text-align: center;
margin: 50px auto;
width: 300px;
padding: 20px;
border: 2px solid black;
border-radius: 15px;
background-color: #f9f9f9;
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.1);
}
.account img {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 20px; /* Закругленные углы */
border: 3px solid black;
}
.account h3 {
font-weight: bold;
margin-top: 10px;
font-size: x-large;
}
/*
.dialogue {
text-align: left;
margin-top: 10px;
font-size: 20px;
}*/

74
db.json Normal file
View File

@@ -0,0 +1,74 @@
{
"products": [
{
"id": "1a54",
"name": "новый товар",
"price": 102,
"image": "images/fork.jpg"
},
{
"id": "f4f8",
"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/gmail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

BIN
images/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

BIN
images/telegram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/vk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

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,36 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Интернет-магазин</title>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<header>
<img src="images/logo.jpg" alt="'Название магазина'">
<a href="newSite.html"><h1><b>"Название магазина"</b></h1></a>
<navbar>
<p><a href="Basket.html">Корзина</a></p>
<p><a href="Order.html">Заказы</a></p>
<p><a href="Favorites.html">Избранное</a></p>
<p><a href="Account.html">Личный кабинет</a></p>
</navbar>
</header>
<h2>Рекомендуемые товары:</h2>
<div class="goods">
<p><img src="images/iron.jpg">Утюг</p>
<p><img src="images/child.jpg">Ребёнок</p>
<p><img src="images/screwdriver.jpg">Отвертка</p>
<p><img src="images/skateboard.jpg">Скейтборд</p>
<p><img src="images/bananas.jpg">Бананы</p>
</div>
<footer>
<p>Помощь:</p>
<p>8 (800)-555-35-35</p>
<p>vk.com</p>
<p>tg.me</p>
<p>ozon-zon-zon@mail.joke</p>
<p><img src="images/бананы.jpg"></p>
</footer>
</body>
</html>

6335
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "ip",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "npm-run-all --parallel start json-server",
"start": "vite",
"build": "vite build",
"server": "http-server -p 3000 ./dist/",
"prod": "npm-run-all build server",
"lint": "eslint …",
"json-server": "json-server --watch db.json --port 5000"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap-icons": "1.11.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.4.1",
"eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.2",
"eslint-plugin-html": "8.1.2",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "5.2.3",
"http-server": "14.1.1",
"json-server": "^1.0.0-beta.3",
"npm-run-all": "4.1.5",
"vite": "6.2.0"
}
}

26
src/App.jsx Normal file
View File

@@ -0,0 +1,26 @@
import React, { useState } from 'react';
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() {
return (
<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

@@ -0,0 +1,22 @@
import React from 'react';
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 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

@@ -0,0 +1,72 @@
import React, { useState, useEffect } from 'react';
export default function ProductForm({ initial, onSave, onCancel }) {
const [form, setForm] = useState({ name: '', price: '', image: '' });
useEffect(() => {
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), image: form.image });
setForm({ name: '', price: '', image: '' });
};
return (
<form onSubmit={handleSubmit} className="card p-3 mb-4">
<div className="mb-2">
<label className="form-label">Название</label>
<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"
/>
</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

@@ -0,0 +1,18 @@
import React from 'react';
import ProductCard from './ProductCard';
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}
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 };
}

33
src/hooks/useProducts.jsx Normal file
View File

@@ -0,0 +1,33 @@
import { useState, useEffect } from 'react';
export default function useProducts() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('http://localhost:5000/products')
.then(res => res.json())
.then(setProducts);
}, []);
const add = async prod => {
const res = await fetch('http://localhost:5000/products', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(prod)
});
const newProd = await res.json();
setProducts([...products, newProd]);
};
const update = async prod => {
await fetch(`http://localhost:5000/products/${prod.id}`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(prod)
});
setProducts(products.map(p => p.id === prod.id ? prod : p));
};
const remove = async id => {
await fetch(`http://localhost:5000/products/${id}`, { method: 'DELETE' });
setProducts(products.filter(p => p.id !== id));
};
return { products, add, update, remove };
}

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

8
src/index.jsx Normal file
View File

@@ -0,0 +1,8 @@
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 />);

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

28
vite.config.js Normal file
View File

@@ -0,0 +1,28 @@
import { resolve } from 'path';
import { defineConfig } from "vite";
import react from '@vitejs/plugin-react';
export default defineConfig({
root: '.',
plugins: [react()],
build: {
outDir: 'dist',
rollupOptions: {
input: {
main: resolve(__dirname, "newSite.html"),
basketPage: resolve(__dirname, "Basket.html"),
favouritesPage: resolve(__dirname, "Favorites.html"),
orderPage: resolve(__dirname, "Order.html"),
accountPage: resolve(__dirname, "Account.html"),
},
},
},
server: {
open: '/',
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
});

BIN
Отчет2.docx Normal file

Binary file not shown.

BIN
Отчет3.docx Normal file

Binary file not shown.

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.