301 lines
14 KiB
TypeScript
301 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/router';
|
||
import Link from 'next/link';
|
||
import Image from 'next/image';
|
||
import { Trash2, Plus, Minus, ShoppingBag } from 'lucide-react';
|
||
import Header from '../components/Header';
|
||
import Footer from '../components/Footer';
|
||
import cartService, { Cart, CartItem } from '../services/cart';
|
||
import authService from '../services/auth';
|
||
|
||
export default function CartPage() {
|
||
const router = useRouter();
|
||
const [cart, setCart] = useState<Cart | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// Загрузка корзины при монтировании компонента
|
||
useEffect(() => {
|
||
const fetchCart = async () => {
|
||
try {
|
||
// Проверяем, авторизован ли пользователь
|
||
if (!authService.isAuthenticated()) {
|
||
router.push('/login?redirect=/cart');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
const cartData = await cartService.getCart();
|
||
setCart(cartData);
|
||
setError(null);
|
||
} catch (err) {
|
||
console.error('Ошибка при загрузке корзины:', err);
|
||
setError('Не удалось загрузить корзину. Пожалуйста, попробуйте позже.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchCart();
|
||
}, [router]);
|
||
|
||
// Обработчик изменения количества товара
|
||
const handleQuantityChange = async (itemId: number, newQuantity: number) => {
|
||
if (newQuantity < 1) return;
|
||
|
||
try {
|
||
// Сохраняем текущий порядок элементов
|
||
const currentItems = cart?.items || [];
|
||
|
||
await cartService.updateCartItem(itemId, { quantity: newQuantity });
|
||
|
||
// Обновляем корзину после изменения, но сохраняем порядок
|
||
const updatedCart = await cartService.getCart();
|
||
|
||
// Сортируем элементы в том же порядке, что и были
|
||
if (updatedCart && currentItems.length > 0) {
|
||
const itemMap = new Map(currentItems.map(item => [item.id, item]));
|
||
const sortedItems = updatedCart.items.sort((a, b) => {
|
||
const indexA = currentItems.findIndex(item => item.id === a.id);
|
||
const indexB = currentItems.findIndex(item => item.id === b.id);
|
||
return indexA - indexB;
|
||
});
|
||
|
||
updatedCart.items = sortedItems;
|
||
}
|
||
|
||
setCart(updatedCart);
|
||
} catch (err) {
|
||
console.error('Ошибка при обновлении количества:', err);
|
||
setError('Не удалось обновить количество товара. Пожалуйста, попробуйте позже.');
|
||
}
|
||
};
|
||
|
||
// Обработчик удаления товара из корзины
|
||
const handleRemoveItem = async (itemId: number) => {
|
||
try {
|
||
// Сохраняем текущий порядок элементов
|
||
const currentItems = cart?.items.filter(item => item.id !== itemId) || [];
|
||
|
||
await cartService.removeFromCart(itemId);
|
||
|
||
// Обновляем корзину после удаления, но сохраняем порядок
|
||
const updatedCart = await cartService.getCart();
|
||
|
||
// Сортируем элементы в том же порядке, что и были
|
||
if (updatedCart && currentItems.length > 0) {
|
||
const sortedItems = updatedCart.items.sort((a, b) => {
|
||
const indexA = currentItems.findIndex(item => item.id === a.id);
|
||
const indexB = currentItems.findIndex(item => item.id === b.id);
|
||
return indexA - indexB;
|
||
});
|
||
|
||
updatedCart.items = sortedItems;
|
||
}
|
||
|
||
setCart(updatedCart);
|
||
} catch (err) {
|
||
console.error('Ошибка при удалении товара:', err);
|
||
setError('Не удалось удалить товар из корзины. Пожалуйста, попробуйте позже.');
|
||
}
|
||
};
|
||
|
||
// Обработчик очистки корзины
|
||
const handleClearCart = async () => {
|
||
try {
|
||
await cartService.clearCart();
|
||
// Обновляем корзину после очистки
|
||
const updatedCart = await cartService.getCart();
|
||
setCart(updatedCart);
|
||
} catch (err) {
|
||
console.error('Ошибка при очистке корзины:', err);
|
||
setError('Не удалось очистить корзину. Пожалуйста, попробуйте позже.');
|
||
}
|
||
};
|
||
|
||
// Переход к оформлению заказа
|
||
const handleCheckout = () => {
|
||
router.push('/checkout');
|
||
};
|
||
|
||
// Функция для корректного отображения 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}`;
|
||
};
|
||
|
||
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 ? (
|
||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||
{error}
|
||
</div>
|
||
) : cart && cart.items.length > 0 ? (
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
{/* Список товаров в корзине */}
|
||
<div className="lg:col-span-2">
|
||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||
<div className="p-6">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h2 className="text-lg font-semibold">Товары в корзине ({cart.items_count})</h2>
|
||
<button
|
||
onClick={handleClearCart}
|
||
className="text-sm text-red-600 hover:text-red-800 transition-colors"
|
||
>
|
||
Очистить корзину
|
||
</button>
|
||
</div>
|
||
|
||
<div className="divide-y divide-gray-200">
|
||
{cart.items.map((item) => (
|
||
<div key={item.id} className="py-6 flex flex-col md:flex-row">
|
||
{/* Изображение товара */}
|
||
<div className="flex-shrink-0 w-full md:w-24 h-24 mb-4 md:mb-0 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-8 h-8 text-gray-400" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Информация о товаре */}
|
||
<div className="md:ml-6 flex-grow">
|
||
<div className="flex flex-col md:flex-row md:justify-between">
|
||
<div>
|
||
<h3 className="text-base font-medium">
|
||
<Link href={`/product/${item.slug}`} className="hover:text-gray-600 transition-colors">
|
||
{item.product_name}
|
||
</Link>
|
||
</h3>
|
||
<p className="text-sm text-gray-500 mt-1">Вариант: {item.variant_name}</p>
|
||
</div>
|
||
<div className="mt-2 md:mt-0 text-right">
|
||
<p className="text-base font-medium">
|
||
{item.total_price.toLocaleString('ru-RU')} ₽
|
||
</p>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
{item.product_price.toLocaleString('ru-RU')} ₽ за шт.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Управление количеством и удаление */}
|
||
<div className="flex justify-between items-center mt-4">
|
||
<div className="flex items-center border border-gray-300 rounded-md">
|
||
<button
|
||
onClick={() => handleQuantityChange(item.id, item.quantity - 1)}
|
||
className="px-3 py-1 text-gray-600 hover:bg-gray-100 transition-colors"
|
||
disabled={item.quantity <= 1}
|
||
>
|
||
<Minus className="w-4 h-4" />
|
||
</button>
|
||
<span className="px-3 py-1 text-center w-10">{item.quantity}</span>
|
||
<button
|
||
onClick={() => handleQuantityChange(item.id, item.quantity + 1)}
|
||
className="px-3 py-1 text-gray-600 hover:bg-gray-100 transition-colors"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
<button
|
||
onClick={() => handleRemoveItem(item.id)}
|
||
className="text-red-600 hover:text-red-800 transition-colors"
|
||
>
|
||
<Trash2 className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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">
|
||
<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>
|
||
|
||
<button
|
||
onClick={handleCheckout}
|
||
className="w-full bg-black text-white py-3 rounded-md hover:bg-gray-800 transition-colors"
|
||
>
|
||
Оформить заказ
|
||
</button>
|
||
|
||
<div className="mt-6">
|
||
<Link href="/all-products" 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>
|
||
);
|
||
}
|