Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active June 3, 2025 08:43
Show Gist options
  • Save sunmeat/82f6ffb0d6c719ff975696bfe9d08d22 to your computer and use it in GitHub Desktop.
Save sunmeat/82f6ffb0d6c719ff975696bfe9d08d22 to your computer and use it in GitHub Desktop.
react + php + mysql
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