Last active
June 3, 2025 08:43
-
-
Save sunmeat/82f6ffb0d6c719ff975696bfe9d08d22 to your computer and use it in GitHub Desktop.
react + php + mysql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mysql table structure: | |
USE 4115733_words; // your DB name here | |
CREATE TABLE cart ( | |
id INT AUTO_INCREMENT PRIMARY KEY, | |
product_id INT NOT NULL, | |
title VARCHAR(255) NOT NULL, | |
price DECIMAL(10, 2) NOT NULL, | |
image VARCHAR(255) NOT NULL | |
); | |
================================================================================================== | |
App.jsx: | |
import { useState, useEffect } from 'react'; | |
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; | |
import './App.css'; | |
const API_URL = 'http://sunmeat.atwebpages.com/react/api.php'; | |
const queryClient = new QueryClient(); | |
const truncateText = (text, maxLength) => | |
text.length > maxLength ? text.slice(0, maxLength) + '...' : text; | |
function ProductList({ addToCart }) { | |
const fetchProducts = async () => { | |
const response = await fetch('https://fakestoreapi.com/products?limit=12'); | |
if (!response.ok) throw new Error('Ошибка загрузки продуктов'); | |
return response.json(); | |
}; | |
const { data, error, isLoading } = useQuery({ | |
queryKey: ['products'], | |
queryFn: fetchProducts, | |
staleTime: 5 * 60 * 1000, | |
}); | |
if (isLoading) return <div className="loading">Загрузка...</div>; | |
if (error) return <div className="error">Ошибка: {error.message}</div>; | |
return ( | |
<div className="product-list"> | |
<h2>Наши товары:</h2> | |
<div className="products"> | |
{data.map((product) => ( | |
<div key={product.id} className="product-card"> | |
<img src={product.image} alt={product.title} className="product-image" /> | |
<h3>{truncateText(product.title, 30)}</h3> | |
<p className="price">${product.price}</p> | |
<button onClick={() => addToCart(product)} className="add-to-cart"> | |
Добавить в корзину | |
</button> | |
</div> | |
))} | |
</div> | |
</div> | |
); | |
} | |
function Cart({ cart, removeFromCart }) { | |
const total = cart.reduce((sum, item) => sum + Number(item.price), 0).toFixed(2); | |
return ( | |
<div className="cart"> | |
<h2>Корзина</h2> | |
{cart.length === 0 ? ( | |
<p>Корзина пуста</p> | |
) : ( | |
<div> | |
{cart.map((item) => ( | |
<div key={item.id} className="cart-item"> | |
<span>{truncateText(item.title, 30)}</span> | |
<span>${item.price}</span> | |
<button onClick={() => removeFromCart(item.id)} className="remove-from-cart"> | |
Удалить | |
</button> | |
</div> | |
))} | |
<p className="total">Итого: ${total}</p> | |
</div> | |
)} | |
</div> | |
); | |
} | |
function App() { | |
const [cart, setCart] = useState([]); | |
const [error, setError] = useState(null); | |
useEffect(() => { | |
const loadCart = async () => { | |
try { | |
const response = await fetch(API_URL); | |
if (!response.ok) throw new Error('Ошибка загрузки корзины'); | |
const data = await response.json(); | |
if (data.error) throw new Error(data.error); | |
setCart(data); | |
} catch (err) { | |
setError(err.message); | |
} | |
}; | |
loadCart(); | |
}, []); | |
const addToCart = async (product) => { | |
try { | |
const newItem = { | |
id: product.id, // product_id | |
title: product.title, | |
price: Number(product.price), | |
image: product.image, | |
}; | |
const response = await fetch(API_URL, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(newItem), | |
}); | |
if (!response.ok) throw new Error('Ошибка добавления в корзину'); | |
const result = await response.json(); | |
if (result.error) throw new Error(result.error); | |
// result.id — это автоинкрементное значение из БД | |
setCart((prevCart) => [ | |
...prevCart, | |
{ | |
...newItem, | |
product_id: newItem.id, | |
id: result.id, // id записи в таблице, нужен для удаления | |
}, | |
]); | |
} catch (err) { | |
setError(err.message); | |
} | |
}; | |
const removeFromCart = async (cartItemId) => { | |
try { | |
const response = await fetch(API_URL, { | |
method: 'DELETE', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ id: cartItemId }), | |
}); | |
if (!response.ok) throw new Error('Ошибка удаления из корзины'); | |
const result = await response.json(); | |
if (result.error) throw new Error(result.error); | |
setCart((prevCart) => prevCart.filter((item) => item.id !== cartItemId)); | |
} catch (err) { | |
setError(err.message); | |
} | |
}; | |
return ( | |
<QueryClientProvider client={queryClient}> | |
<div className="app"> | |
<h1>Интернет-магазин ReactExpress</h1> | |
{error && <div className="error">Ошибка: {error}</div>} | |
<ProductList addToCart={addToCart} /> | |
<Cart cart={cart} removeFromCart={removeFromCart} /> | |
</div> | |
</QueryClientProvider> | |
); | |
} | |
export default App; | |
================================================================================================== | |
App.css: | |
.app { | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 20px; | |
font-family: 'Arial', sans-serif; | |
background: linear-gradient(135deg, #f5f7fa, #c3cfe2); | |
min-height: 100vh; | |
} | |
h1 { | |
text-align: center; | |
color: #1a1a1a; | |
font-size: 2.5em; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); | |
margin-bottom: 30px; | |
} | |
.product-list { | |
margin-bottom: 40px; | |
} | |
.product-list h2 { | |
color: #1a1a1a; | |
margin-bottom: 20px; | |
font-size: 1.8em; | |
} | |
.products { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | |
gap: 20px; | |
} | |
.product-card { | |
border: 1px solid #ccc; | |
padding: 15px; | |
text-align: center; | |
background-color: #fff; | |
border-radius: 10px; | |
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
height: 350px; | |
display: flex; | |
flex-direction: column; | |
justify-content: space-between; | |
} | |
.product-card:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2); | |
} | |
.product-image { | |
max-width: 100%; | |
height: 160px; | |
object-fit: contain; | |
margin-bottom: 10px; | |
} | |
.price { | |
color: #27ae60; | |
font-weight: bold; | |
font-size: 1.2em; | |
margin: 10px 0; | |
} | |
.add-to-cart { | |
background-color: #2980b9; | |
color: white; | |
border: none; | |
padding: 10px 20px; | |
cursor: pointer; | |
border-radius: 5px; | |
font-size: 1em; | |
transition: background-color 0.3s ease; | |
} | |
.add-to-cart:hover { | |
background-color: #1f6391; | |
} | |
.cart { | |
border-top: 3px solid #ccc; | |
padding-top: 20px; | |
background-color: #fff; | |
border-radius: 10px; | |
padding: 20px; | |
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); | |
} | |
.cart h2 { | |
color: #1a1a1a; | |
margin-bottom: 20px; | |
font-size: 1.8em; | |
} | |
.cart-item { | |
display: grid; | |
grid-template-columns: 3fr 1fr 1fr; | |
align-items: center; | |
padding: 10px 0; | |
border-bottom: 1px solid #eee; | |
gap: 10px; | |
} | |
.cart-item span:nth-child(1) { | |
text-align: left; | |
font-size: 1em; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
.cart-item span:nth-child(2) { | |
text-align: right; | |
font-weight: bold; | |
color: #27ae60; | |
} | |
.remove-from-cart { | |
background-color: #c0392b; | |
color: white; | |
border: none; | |
padding: 8px 12px; | |
cursor: pointer; | |
border-radius: 5px; | |
font-size: 0.9em; | |
transition: background-color 0.3s ease; | |
} | |
.remove-from-cart:hover { | |
background-color: #992d22; | |
} | |
.total { | |
font-weight: bold; | |
text-align: right; | |
margin-top: 20px; | |
font-size: 1.3em; | |
color: #1a1a1a; | |
} | |
.loading, .error { | |
text-align: center; | |
padding: 20px; | |
color: #1a1a1a; | |
font-size: 1.2em; | |
} | |
.error { | |
color: #c0392b; | |
} | |
================================================================================================== | |
vite.config.js: // настройки сработают только для Vite, если CreateReactApp - нужно искать другой способ | |
import { defineConfig } from 'vite'; | |
import react from '@vitejs/plugin-react'; | |
export default defineConfig({ | |
base: './', | |
plugins: [react()], | |
}); | |
================================================================================================== | |
в терминале запустить команду npm run build (делаем релиз сборки, статический сайт) | |
содержимое папки dist в папке с проектом перенести в /sunmeat.atwebpages.com/react/ | |
- index.html | |
- vite.svg | |
- assets/index...css | |
- assets/index...js | |
================================================================================================== | |
/sunmeat.atwebpages.com/react/api.php: | |
<?php | |
ini_set('display_errors', 1); | |
error_reporting(E_ALL); | |
// CORS заголовки | |
header('Content-Type: application/json'); | |
header('Access-Control-Allow-Origin: *'); // разрешаем все домены или можно указать конкретный | |
header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS'); | |
header('Access-Control-Allow-Headers: Content-Type, Authorization'); | |
// обработка preflight запросов | |
// preflight запрос - проверка у сервера, разрешено ли ему принимать основной запрос с такими параметрами (метод, заголовки и тд) | |
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { | |
http_response_code(200); | |
exit; | |
} | |
// функция логирования | |
function logMessage($message) { | |
$timestamp = date('Y-m-d H:i:s'); | |
$logEntry = "[$timestamp] $message\n"; | |
file_put_contents('log.txt', $logEntry, FILE_APPEND | LOCK_EX); | |
} | |
// параметры подключения к базе данных | |
$host = 'fdb1033.awardspace.net'; | |
$dbname = '4115733_words'; | |
$username = '4115733_words'; | |
$password = 'password2025'; | |
try { | |
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password); | |
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | |
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); | |
} catch (PDOException $e) { | |
logMessage("Ошибка подключения к БД: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => 'Ошибка подключения к базе данных']); | |
exit; | |
} | |
// функция для создания таблицы если её не существует | |
function createCartTableIfNotExists($pdo) { | |
try { | |
$sql = "CREATE TABLE IF NOT EXISTS cart ( | |
id INT AUTO_INCREMENT PRIMARY KEY, | |
product_id INT NOT NULL, | |
title VARCHAR(255) NOT NULL, | |
price DECIMAL(10,2) NOT NULL, | |
image TEXT, | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
)"; | |
$pdo->exec($sql); | |
} catch (PDOException $e) { | |
logMessage("Ошибка создания таблицы: " . $e->getMessage()); | |
throw $e; | |
} | |
} | |
// создаём таблицу если нужно | |
try { | |
createCartTableIfNotExists($pdo); | |
} catch (PDOException $e) { | |
http_response_code(500); | |
echo json_encode(['error' => 'Ошибка инициализации базы данных']); | |
exit; | |
} | |
$method = $_SERVER['REQUEST_METHOD']; | |
logMessage("Получен запрос: $method"); | |
switch ($method) { | |
case 'GET': | |
try { | |
$stmt = $pdo->prepare('SELECT * FROM cart ORDER BY id DESC'); | |
$stmt->execute(); | |
$result = $stmt->fetchAll(); | |
logMessage("Возвращено записей: " . count($result)); | |
echo json_encode($result); | |
} catch (PDOException $e) { | |
logMessage("Ошибка GET запроса: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => 'Ошибка получения данных']); | |
} | |
break; | |
case 'POST': | |
try { | |
$input = file_get_contents('php://input'); | |
$data = json_decode($input, true); | |
if (json_last_error() !== JSON_ERROR_NONE) { | |
http_response_code(400); | |
echo json_encode(['error' => 'Неверный формат JSON']); | |
exit; | |
} | |
// валидация данных | |
if (!isset($data['id']) || !isset($data['title']) || !isset($data['price'])) { | |
http_response_code(400); | |
echo json_encode(['error' => 'Недостаточно данных (требуются: id, title, price)']); | |
exit; | |
} | |
// проверяем, что цена является числом | |
if (!is_numeric($data['price']) || $data['price'] < 0) { | |
http_response_code(400); | |
echo json_encode(['error' => 'Неверный формат цены']); | |
exit; | |
} | |
$stmt = $pdo->prepare('INSERT INTO cart (product_id, title, price, image) VALUES (:product_id, :title, :price, :image)'); | |
$result = $stmt->execute([ | |
':product_id' => (int)$data['id'], | |
':title' => trim($data['title']), | |
':price' => (float)$data['price'], | |
':image' => isset($data['image']) ? $data['image'] : null | |
]); | |
if ($result) { | |
logMessage("Добавлен товар с ID: " . $data['id']); | |
echo json_encode([ | |
'success' => true, | |
'id' => $pdo->lastInsertId(), | |
'message' => 'Товар добавлен в корзину' | |
]); | |
} else { | |
throw new Exception('Не удалось добавить товар'); | |
} | |
} catch (PDOException $e) { | |
logMessage("Ошибка POST запроса: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => 'Ошибка добавления товара']); | |
} catch (Exception $e) { | |
logMessage("Общая ошибка POST: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => $e->getMessage()]); | |
} | |
break; | |
case 'DELETE': | |
try { | |
$input = file_get_contents('php://input'); | |
$data = json_decode($input, true); | |
if (json_last_error() !== JSON_ERROR_NONE) { | |
http_response_code(400); | |
echo json_encode(['error' => 'Неверный формат JSON']); | |
exit; | |
} | |
if (!isset($data['id'])) { | |
http_response_code(400); | |
echo json_encode(['error' => 'ID записи не указан']); | |
exit; | |
} | |
$stmt = $pdo->prepare('DELETE FROM cart WHERE id = :id'); | |
$result = $stmt->execute([':id' => (int)$data['id']]); | |
$deletedRows = $stmt->rowCount(); | |
if ($deletedRows > 0) { | |
logMessage("Удалена запись с ID: " . $data['id']); | |
echo json_encode([ | |
'success' => true, | |
'deleted_count' => $deletedRows, | |
'message' => 'Товар удалён из корзины' | |
]); | |
} else { | |
logMessage("Запись с ID " . $data['id'] . " не найдена"); | |
http_response_code(404); | |
echo json_encode(['error' => 'Товар не найден в корзине']); | |
} | |
} catch (PDOException $e) { | |
logMessage("Ошибка DELETE запроса: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => 'Ошибка удаления товара']); | |
} catch (Exception $e) { | |
logMessage("Общая ошибка DELETE: " . $e->getMessage()); | |
http_response_code(500); | |
echo json_encode(['error' => $e->getMessage()]); | |
} | |
break; | |
default: | |
logMessage("Неподдерживаемый метод: $method"); | |
http_response_code(405); | |
echo json_encode(['error' => 'Метод не поддерживается']); | |
break; | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment