822 lines
39 KiB
TypeScript
822 lines
39 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/router';
|
||
import Link from 'next/link';
|
||
import Image from 'next/image';
|
||
import { ShoppingBag, CreditCard, Truck, Check, Plus, X } from 'lucide-react';
|
||
import Header from '../components/Header';
|
||
import Footer from '../components/Footer';
|
||
import cartService, { Cart } from '../services/cart';
|
||
import { orderService } from '../services/orders';
|
||
import authService from '../services/auth';
|
||
import { userService, Address, AddressCreate } from '../services/users';
|
||
|
||
// Типы для формы оформления заказа
|
||
interface CheckoutForm {
|
||
shipping_address_id: number;
|
||
payment_method: string;
|
||
notes: string;
|
||
}
|
||
|
||
// Типы для формы нового адреса
|
||
interface AddressForm {
|
||
address_line1: string;
|
||
address_line2?: string;
|
||
city: string;
|
||
state: string;
|
||
postal_code: string;
|
||
country: string;
|
||
is_default: boolean;
|
||
}
|
||
|
||
export default function CheckoutPage() {
|
||
const router = useRouter();
|
||
const [cart, setCart] = useState<Cart | null>(null);
|
||
const [addresses, setAddresses] = useState<Address[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState(false);
|
||
const [orderId, setOrderId] = useState<number | null>(null);
|
||
const [showAddressForm, setShowAddressForm] = useState(false);
|
||
const [addressFormSubmitting, setAddressFormSubmitting] = useState(false);
|
||
const [addressFormError, setAddressFormError] = useState<string | null>(null);
|
||
const [formValidated, setFormValidated] = useState(false);
|
||
|
||
// Состояние формы заказа
|
||
const [form, setForm] = useState<CheckoutForm>({
|
||
shipping_address_id: 0,
|
||
payment_method: 'credit_card',
|
||
notes: ''
|
||
});
|
||
|
||
// Состояние формы нового адреса
|
||
const [addressForm, setAddressForm] = useState<AddressForm>({
|
||
address_line1: '',
|
||
address_line2: '',
|
||
city: '',
|
||
state: '',
|
||
postal_code: '',
|
||
country: 'Россия',
|
||
is_default: false
|
||
});
|
||
|
||
// Проверка валидности формы
|
||
useEffect(() => {
|
||
// Форма валидна, если выбран адрес доставки и способ оплаты
|
||
const isValid = form.shipping_address_id > 0 && !!form.payment_method;
|
||
setFormValidated(isValid);
|
||
}, [form.shipping_address_id, form.payment_method]);
|
||
|
||
// Загрузка корзины и адресов при монтировании компонента
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
try {
|
||
// Проверяем, авторизован ли пользователь
|
||
if (!authService.isAuthenticated()) {
|
||
router.push('/login?redirect=/checkout');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
|
||
// Загружаем корзину
|
||
const cartData = await cartService.getCart();
|
||
setCart(cartData);
|
||
|
||
// Если корзина пуста, перенаправляем на страницу корзины
|
||
if (cartData.items.length === 0) {
|
||
router.push('/cart');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Загружаем адреса пользователя
|
||
const userData = await userService.getCurrentUser();
|
||
if (userData.addresses && userData.addresses.length > 0) {
|
||
setAddresses(userData.addresses);
|
||
|
||
// Устанавливаем адрес по умолчанию, если он есть
|
||
const defaultAddress = userData.addresses.find(addr => addr.is_default);
|
||
if (defaultAddress) {
|
||
setForm(prev => ({ ...prev, shipping_address_id: defaultAddress.id }));
|
||
} else {
|
||
setForm(prev => ({ ...prev, shipping_address_id: userData.addresses[0].id }));
|
||
}
|
||
} else {
|
||
// Если у пользователя нет адресов, показываем форму добавления адреса
|
||
setShowAddressForm(true);
|
||
}
|
||
} catch (addressErr) {
|
||
console.error('Ошибка при загрузке адресов:', addressErr);
|
||
// Не показываем ошибку пользователю, просто предлагаем добавить адрес
|
||
setShowAddressForm(true);
|
||
}
|
||
|
||
setError(null);
|
||
} catch (err) {
|
||
console.error('Ошибка при загрузке данных:', err);
|
||
setError('Не удалось загрузить данные. Пожалуйста, попробуйте позже.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchData();
|
||
}, [router]);
|
||
|
||
// Обработчик изменения полей формы заказа
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value } = e.target;
|
||
setForm(prev => ({ ...prev, [name]: value }));
|
||
};
|
||
|
||
// Обработчик изменения полей формы адреса
|
||
const handleAddressFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||
const { name, value } = e.target;
|
||
|
||
// Для чекбокса используем checked, для остальных полей - value
|
||
if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox') {
|
||
const checked = e.target.checked;
|
||
setAddressForm(prev => ({
|
||
...prev,
|
||
[name]: checked
|
||
}));
|
||
} else {
|
||
setAddressForm(prev => ({
|
||
...prev,
|
||
[name]: value
|
||
}));
|
||
}
|
||
};
|
||
|
||
// Обработчик отправки формы заказа
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!formValidated) {
|
||
setError('Пожалуйста, заполните все обязательные поля');
|
||
// Если нет адресов, показываем форму добавления адреса
|
||
if (addresses.length === 0) {
|
||
setShowAddressForm(true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Проверяем, что в корзине есть товары
|
||
if (!cart || cart.items.length === 0) {
|
||
setError('Ваша корзина пуста. Добавьте товары в корзину, чтобы оформить заказ.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setSubmitting(true);
|
||
setError(null);
|
||
|
||
console.log('Отправка заказа на сервер:', {
|
||
shipping_address_id: form.shipping_address_id,
|
||
payment_method: form.payment_method,
|
||
notes: form.notes
|
||
});
|
||
|
||
// Создаем заказ
|
||
const orderResponse = await orderService.createOrder({
|
||
shipping_address_id: form.shipping_address_id,
|
||
payment_method: form.payment_method,
|
||
notes: form.notes
|
||
});
|
||
|
||
console.log('Получен ответ от сервера:', orderResponse);
|
||
|
||
// Извлекаем ID заказа из ответа
|
||
let orderId = null;
|
||
|
||
// Проверяем разные форматы ответа
|
||
const responseObj = orderResponse as any;
|
||
if (responseObj && responseObj.id) {
|
||
orderId = responseObj.id;
|
||
} else if (responseObj && responseObj.order && responseObj.order.id) {
|
||
orderId = responseObj.order.id;
|
||
}
|
||
|
||
// Устанавливаем флаг успешного создания заказа
|
||
setSuccess(true);
|
||
setOrderId(orderId);
|
||
|
||
// Очищаем корзину после успешного оформления заказа
|
||
try {
|
||
await cartService.clearCart();
|
||
} catch (clearErr) {
|
||
console.error('Ошибка при очистке корзины:', clearErr);
|
||
// Не показываем эту ошибку пользователю, так как заказ уже создан
|
||
}
|
||
|
||
// Проверяем, что ID заказа существует и является числом
|
||
if (orderId && !isNaN(orderId)) {
|
||
// Перенаправляем на страницу успешного оформления заказа
|
||
router.push(`/order-success?id=${orderId}`);
|
||
} else {
|
||
console.error('Ошибка: ID заказа отсутствует или некорректен', orderResponse);
|
||
// Перенаправляем на страницу заказов без указания ID
|
||
router.push('/account/orders');
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка при оформлении заказа:', err);
|
||
|
||
// Выводим подробную информацию об ошибке в консоль
|
||
if (err.response) {
|
||
console.error('Статус ответа:', err.response.status);
|
||
console.error('Данные ответа:', err.response.data);
|
||
}
|
||
|
||
// Проверяем, есть ли в ответе сервера сообщение об ошибке
|
||
let errorMessage = 'Не удалось оформить заказ. Пожалуйста, попробуйте позже.';
|
||
if (err.response && err.response.data) {
|
||
if (typeof err.response.data === 'string') {
|
||
errorMessage = err.response.data;
|
||
} else if (err.response.data.detail) {
|
||
if (Array.isArray(err.response.data.detail)) {
|
||
// Если detail - это массив ошибок валидации
|
||
const validationErrors = err.response.data.detail.map(error =>
|
||
`${error.loc.join('.')}: ${error.msg}`
|
||
).join('; ');
|
||
errorMessage = `Ошибка валидации: ${validationErrors}`;
|
||
} else {
|
||
errorMessage = err.response.data.detail;
|
||
}
|
||
}
|
||
}
|
||
|
||
setError(errorMessage);
|
||
|
||
// Прокручиваем страницу вверх, чтобы показать сообщение об ошибке
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Обработчик отправки формы нового адреса
|
||
const handleAddressSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault(); // Предотвращаем стандартное поведение формы
|
||
|
||
// Валидация формы
|
||
if (!addressForm.address_line1 || !addressForm.city || !addressForm.state || !addressForm.postal_code) {
|
||
setAddressFormError('Пожалуйста, заполните все обязательные поля');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setAddressFormSubmitting(true);
|
||
setAddressFormError(null);
|
||
|
||
console.log('Отправка адреса на сервер:', addressForm);
|
||
|
||
// Создаем новый адрес
|
||
const newAddress = await userService.addAddress(addressForm);
|
||
|
||
console.log('Получен ответ от сервера:', newAddress);
|
||
|
||
// Добавляем новый адрес в список и выбираем его
|
||
setAddresses(prev => [...prev, newAddress]);
|
||
setForm(prev => ({ ...prev, shipping_address_id: newAddress.id }));
|
||
|
||
// Скрываем форму добавления адреса
|
||
setShowAddressForm(false);
|
||
|
||
// Сбрасываем форму
|
||
setAddressForm({
|
||
address_line1: '',
|
||
address_line2: '',
|
||
city: '',
|
||
state: '',
|
||
postal_code: '',
|
||
country: 'Россия',
|
||
is_default: false
|
||
});
|
||
} catch (err) {
|
||
console.error('Ошибка при добавлении адреса:', err);
|
||
|
||
// Выводим подробную информацию об ошибке в консоль
|
||
if (err.response) {
|
||
console.error('Ответ сервера:', err.response.status, err.response.data);
|
||
}
|
||
|
||
// Проверяем, есть ли в ответе сервера сообщение об ошибке
|
||
let errorMessage = 'Не удалось добавить адрес. Пожалуйста, попробуйте позже.';
|
||
if (err.response && err.response.data) {
|
||
if (typeof err.response.data === 'string') {
|
||
errorMessage = err.response.data;
|
||
} else if (err.response.data.detail) {
|
||
if (Array.isArray(err.response.data.detail)) {
|
||
// Если detail - это массив ошибок валидации
|
||
const validationErrors = err.response.data.detail.map(error =>
|
||
`${error.loc.join('.')}: ${error.msg}`
|
||
).join('; ');
|
||
errorMessage = `Ошибка валидации: ${validationErrors}`;
|
||
} else {
|
||
errorMessage = err.response.data.detail;
|
||
}
|
||
}
|
||
}
|
||
|
||
setAddressFormError(errorMessage);
|
||
} finally {
|
||
setAddressFormSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Форматирование цены
|
||
const formatPrice = (price: number): string => {
|
||
return price.toLocaleString('ru-RU') + ' ₽';
|
||
};
|
||
|
||
// Функция для корректного отображения URL изображения
|
||
const getImageUrl = (imageUrl: string | undefined): string => {
|
||
if (!imageUrl) return '/placeholder-image.jpg';
|
||
|
||
// Проверяем, начинается ли URL с http или https
|
||
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||
return imageUrl;
|
||
}
|
||
|
||
// Формируем базовый URL для изображений
|
||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000/api';
|
||
const apiBaseUrl = baseUrl.replace(/\/api$/, ''); // Убираем '/api' в конце, если есть
|
||
|
||
// Если URL начинается с /, добавляем базовый URL API
|
||
if (imageUrl.startsWith('/')) {
|
||
return `${apiBaseUrl}${imageUrl}`;
|
||
}
|
||
|
||
// В остальных случаях добавляем базовый URL API и /
|
||
return `${apiBaseUrl}/${imageUrl}`;
|
||
};
|
||
|
||
// Форматирование адреса для отображения
|
||
const formatAddress = (address: Address): string => {
|
||
let formattedAddress = address.address_line1;
|
||
if (address.address_line2) {
|
||
formattedAddress += `, ${address.address_line2}`;
|
||
}
|
||
return `${formattedAddress}, ${address.city}, ${address.state}, ${address.postal_code}, ${address.country}`;
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen flex flex-col">
|
||
<Header />
|
||
|
||
<main className="flex-grow pt-24 pb-16">
|
||
<div className="container mx-auto px-4">
|
||
<h1 className="text-2xl md:text-3xl font-bold mb-8 text-center">Оформление заказа</h1>
|
||
|
||
{loading ? (
|
||
<div className="flex justify-center items-center h-64">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
|
||
</div>
|
||
) : error && !success ? (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||
{error}
|
||
</div>
|
||
) : success ? (
|
||
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-8 rounded mb-4 text-center">
|
||
<div className="flex justify-center mb-4">
|
||
<div className="bg-green-500 rounded-full p-2">
|
||
<Check className="w-8 h-8 text-white" />
|
||
</div>
|
||
</div>
|
||
<h2 className="text-xl font-semibold mb-2">Заказ успешно оформлен!</h2>
|
||
<p className="mb-4">Номер вашего заказа: {orderId}</p>
|
||
<p>Вы будете перенаправлены на страницу заказа через несколько секунд...</p>
|
||
</div>
|
||
) : cart ? (
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
{/* Форма оформления заказа */}
|
||
<div className="lg:col-span-2">
|
||
<form id="checkout-form" onSubmit={handleSubmit}>
|
||
{/* Адрес доставки */}
|
||
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
|
||
<div className="p-6">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h2 className="text-lg font-semibold flex items-center">
|
||
<Truck className="w-5 h-5 mr-2" />
|
||
Адрес доставки
|
||
</h2>
|
||
|
||
{!showAddressForm && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressForm(true)}
|
||
className="text-sm flex items-center text-indigo-600 hover:text-indigo-800"
|
||
>
|
||
<Plus className="w-4 h-4 mr-1" />
|
||
Добавить новый адрес
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{showAddressForm ? (
|
||
<div className="border rounded-md p-4 mb-4">
|
||
<div className="flex justify-between items-center mb-3">
|
||
<h3 className="font-medium">Новый адрес доставки</h3>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressForm(false)}
|
||
className="text-gray-500 hover:text-gray-700"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{addressFormError && (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-3 py-2 rounded mb-3 text-sm">
|
||
{addressFormError}
|
||
</div>
|
||
)}
|
||
|
||
<form onSubmit={handleAddressSubmit} className="space-y-3">
|
||
<div>
|
||
<label htmlFor="address_line1" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Адрес (строка 1)*
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="address_line1"
|
||
name="address_line1"
|
||
value={addressForm.address_line1}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Улица, дом"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="address_line2" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Адрес (строка 2)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="address_line2"
|
||
name="address_line2"
|
||
value={addressForm.address_line2}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Квартира, офис (необязательно)"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label htmlFor="city" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Город*
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="city"
|
||
name="city"
|
||
value={addressForm.city}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Город"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="state" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Область/Регион*
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="state"
|
||
name="state"
|
||
value={addressForm.state}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Область или регион"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div>
|
||
<label htmlFor="postal_code" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Почтовый индекс*
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="postal_code"
|
||
name="postal_code"
|
||
value={addressForm.postal_code}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Индекс"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label htmlFor="country" className="block text-sm font-medium text-gray-700 mb-1">
|
||
Страна*
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="country"
|
||
name="country"
|
||
value={addressForm.country}
|
||
onChange={handleAddressFormChange}
|
||
placeholder="Страна"
|
||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="is_default"
|
||
name="is_default"
|
||
checked={addressForm.is_default}
|
||
onChange={handleAddressFormChange}
|
||
className="h-4 w-4 text-black focus:ring-black border-gray-300 rounded"
|
||
/>
|
||
<label htmlFor="is_default" className="ml-2 block text-sm text-gray-700">
|
||
Использовать как адрес по умолчанию
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex justify-end pt-2">
|
||
<button
|
||
type="submit"
|
||
disabled={addressFormSubmitting}
|
||
className="bg-black text-white px-4 py-2 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{addressFormSubmitting ? (
|
||
<div className="flex items-center">
|
||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white mr-2"></div>
|
||
Сохранение...
|
||
</div>
|
||
) : (
|
||
'Сохранить адрес'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
) : null}
|
||
|
||
{addresses.length > 0 ? (
|
||
<div className="space-y-4">
|
||
{addresses.map(address => (
|
||
<div key={address.id} className="border rounded-md p-4">
|
||
<div className="flex items-start">
|
||
<input
|
||
type="radio"
|
||
id={`address-${address.id}`}
|
||
name="shipping_address_id"
|
||
value={address.id}
|
||
checked={form.shipping_address_id === address.id}
|
||
onChange={handleChange}
|
||
className="mt-1 mr-3"
|
||
/>
|
||
<label htmlFor={`address-${address.id}`} className="flex-grow cursor-pointer">
|
||
<div className="font-medium">Адрес доставки</div>
|
||
<div className="text-sm text-gray-600 mt-1">{formatAddress(address)}</div>
|
||
{address.is_default && (
|
||
<div className="text-sm text-green-600 mt-1">Адрес по умолчанию</div>
|
||
)}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : !showAddressForm ? (
|
||
<div className="text-center py-4">
|
||
<p className="text-gray-600 mb-4">У вас еще нет сохраненных адресов</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowAddressForm(true)}
|
||
className="text-blue-600 hover:text-blue-800 transition-colors"
|
||
>
|
||
Добавить новый адрес
|
||
</button>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Способ оплаты */}
|
||
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
|
||
<div className="p-6">
|
||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||
<CreditCard className="w-5 h-5 mr-2" />
|
||
Способ оплаты
|
||
</h2>
|
||
|
||
<div className="space-y-4">
|
||
<div className="border rounded-md p-4">
|
||
<div className="flex items-start">
|
||
<input
|
||
type="radio"
|
||
id="payment-card"
|
||
name="payment_method"
|
||
value="credit_card"
|
||
checked={form.payment_method === 'credit_card'}
|
||
onChange={handleChange}
|
||
className="mt-1 mr-3"
|
||
/>
|
||
<label htmlFor="payment-card" className="flex-grow cursor-pointer">
|
||
<div className="font-medium">Банковская карта</div>
|
||
<div className="text-sm text-gray-600 mt-1">Оплата картой при получении</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border rounded-md p-4">
|
||
<div className="flex items-start">
|
||
<input
|
||
type="radio"
|
||
id="payment-cash"
|
||
name="payment_method"
|
||
value="cash_on_delivery"
|
||
checked={form.payment_method === 'cash_on_delivery'}
|
||
onChange={handleChange}
|
||
className="mt-1 mr-3"
|
||
/>
|
||
<label htmlFor="payment-cash" className="flex-grow cursor-pointer">
|
||
<div className="font-medium">Наличные</div>
|
||
<div className="text-sm text-gray-600 mt-1">Оплата наличными при получении</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border rounded-md p-4">
|
||
<div className="flex items-start">
|
||
<input
|
||
type="radio"
|
||
id="payment-paypal"
|
||
name="payment_method"
|
||
value="paypal"
|
||
checked={form.payment_method === 'paypal'}
|
||
onChange={handleChange}
|
||
className="mt-1 mr-3"
|
||
/>
|
||
<label htmlFor="payment-paypal" className="flex-grow cursor-pointer">
|
||
<div className="font-medium">PayPal</div>
|
||
<div className="text-sm text-gray-600 mt-1">Оплата через PayPal</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border rounded-md p-4">
|
||
<div className="flex items-start">
|
||
<input
|
||
type="radio"
|
||
id="payment-bank"
|
||
name="payment_method"
|
||
value="bank_transfer"
|
||
checked={form.payment_method === 'bank_transfer'}
|
||
onChange={handleChange}
|
||
className="mt-1 mr-3"
|
||
/>
|
||
<label htmlFor="payment-bank" className="flex-grow cursor-pointer">
|
||
<div className="font-medium">Банковский перевод</div>
|
||
<div className="text-sm text-gray-600 mt-1">Оплата банковским переводом</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Комментарий к заказу */}
|
||
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-6">
|
||
<div className="p-6">
|
||
<h2 className="text-lg font-semibold mb-4">Комментарий к заказу</h2>
|
||
|
||
<textarea
|
||
name="notes"
|
||
value={form.notes}
|
||
onChange={handleChange}
|
||
placeholder="Дополнительная информация к заказу"
|
||
className="w-full border border-gray-300 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-black"
|
||
rows={3}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="lg:hidden">
|
||
<button
|
||
type="submit"
|
||
disabled={submitting || !formValidated}
|
||
className="w-full bg-black text-white py-3 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{submitting ? (
|
||
<div className="flex items-center justify-center">
|
||
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
|
||
Оформление...
|
||
</div>
|
||
) : (
|
||
'Оформить заказ'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Сводка заказа */}
|
||
<div className="lg:col-span-1">
|
||
<div className="bg-white rounded-lg shadow-md p-6 sticky top-24">
|
||
<h2 className="text-lg font-semibold mb-6">Ваш заказ</h2>
|
||
|
||
<div className="space-y-4 mb-6 max-h-80 overflow-y-auto">
|
||
{cart.items.map((item) => (
|
||
<div key={item.id} className="flex items-start py-2">
|
||
<div className="flex-shrink-0 w-16 h-16 mr-4 relative">
|
||
{item.product_image ? (
|
||
<Image
|
||
src={getImageUrl(item.product_image)}
|
||
alt={item.product_name}
|
||
fill
|
||
className="object-cover rounded-md"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full bg-gray-200 rounded-md flex items-center justify-center">
|
||
<ShoppingBag className="w-6 h-6 text-gray-400" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex-grow">
|
||
<h3 className="text-sm font-medium">{item.product_name}</h3>
|
||
<p className="text-xs text-gray-500">Вариант: {item.variant_name}</p>
|
||
<div className="flex justify-between mt-1">
|
||
<span className="text-xs text-gray-500">Кол-во: {item.quantity}</span>
|
||
<span className="text-sm font-medium">{item.total_price.toLocaleString('ru-RU')} ₽</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="border-t border-gray-200 pt-4 space-y-4">
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Товары ({cart.items_count})</span>
|
||
<span>{cart.total_amount.toLocaleString('ru-RU')} ₽</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-gray-600">Доставка</span>
|
||
<span>Бесплатно</span>
|
||
</div>
|
||
<div className="border-t border-gray-200 pt-4 flex justify-between font-semibold">
|
||
<span>Итого</span>
|
||
<span>{cart.total_amount.toLocaleString('ru-RU')} ₽</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="hidden lg:block">
|
||
<button
|
||
type="submit"
|
||
form="checkout-form"
|
||
disabled={submitting || !formValidated}
|
||
className="w-full bg-black text-white py-3 rounded-md hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{submitting ? (
|
||
<div className="flex items-center justify-center">
|
||
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white mr-2"></div>
|
||
Оформление...
|
||
</div>
|
||
) : (
|
||
'Оформить заказ'
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mt-6">
|
||
<Link href="/cart" className="text-center block text-gray-600 hover:text-gray-800 transition-colors">
|
||
Вернуться в корзину
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-16">
|
||
<div className="flex justify-center mb-6">
|
||
<ShoppingBag className="w-16 h-16 text-gray-400" />
|
||
</div>
|
||
<h2 className="text-xl font-semibold mb-2">Ваша корзина пуста</h2>
|
||
<p className="text-gray-600 mb-8">Добавьте товары в корзину, чтобы оформить заказ</p>
|
||
<Link href="/all-products" className="bg-black text-white px-6 py-3 rounded-md hover:bg-gray-800 transition-colors">
|
||
Перейти к покупкам
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|