Files
protekauto-frontend/src/contexts/CartContext.tsx

477 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { createContext, useContext, useReducer, useEffect, useState } from 'react'
import { CartState, CartContextType, CartItem, DeliveryInfo } from '@/types/cart'
// Начальное состояние корзины
const initialState: CartState = {
items: [],
summary: {
totalItems: 0,
totalPrice: 0,
totalDiscount: 0,
deliveryPrice: 39,
finalPrice: 0
},
delivery: {
type: 'Доставка курьером',
address: 'Калининградская область, Калиниград, улица Понартская, 5, кв./офис 1, Подъезд 1, этаж 1',
price: 39
},
orderComment: '',
isLoading: false
}
// Типы действий
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'id' | 'selected' | 'favorite'> }
| { type: 'ADD_ITEM_SUCCESS'; payload: { items: CartItem[]; summary: any } }
| { type: 'ADD_ITEM_ERROR'; payload: string }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'TOGGLE_SELECT'; payload: string }
| { type: 'TOGGLE_FAVORITE'; payload: string }
| { type: 'UPDATE_COMMENT'; payload: { id: string; comment: string } }
| { type: 'UPDATE_ORDER_COMMENT'; payload: string }
| { type: 'SELECT_ALL' }
| { type: 'REMOVE_ALL' }
| { type: 'REMOVE_SELECTED' }
| { type: 'UPDATE_DELIVERY'; payload: Partial<DeliveryInfo> }
| { type: 'CLEAR_CART' }
| { type: 'LOAD_CART'; payload: CartItem[] }
| { type: 'LOAD_FULL_STATE'; payload: { items: CartItem[]; delivery: DeliveryInfo; orderComment: string } }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string }
// Функция для генерации ID
const generateId = () => Math.random().toString(36).substr(2, 9)
// Утилитарная функция для парсинга количества в наличии
const parseStock = (stockStr: string | number | undefined): number => {
if (typeof stockStr === 'number') return stockStr;
if (typeof stockStr === 'string') {
const match = stockStr.match(/\d+/);
return match ? parseInt(match[0]) : 0;
}
return 0;
};
// Функция для расчета итогов
const calculateSummary = (items: CartItem[], deliveryPrice: number) => {
const selectedItems = items.filter(item => item.selected)
const totalItems = selectedItems.reduce((sum, item) => sum + item.quantity, 0)
const totalPrice = selectedItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
const totalDiscount = selectedItems.reduce((sum, item) => {
const discount = item.originalPrice ? (item.originalPrice - item.price) * item.quantity : 0
return sum + discount
}, 0)
// Доставка включена в стоимость товаров, поэтому добавляем её только если есть товары
const finalPrice = totalPrice + (totalPrice > 0 ? 0 : 0) // Доставка всегда включена в цену товаров
return {
totalItems,
totalPrice,
totalDiscount,
deliveryPrice,
finalPrice
}
}
// Редьюсер корзины
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM': {
const existingItemIndex = state.items.findIndex(
item =>
(item.productId && item.productId === action.payload.productId) ||
(item.offerKey && item.offerKey === action.payload.offerKey)
)
let newItems: CartItem[]
if (existingItemIndex >= 0) {
// Увеличиваем количество существующего товара
const existingItem = state.items[existingItemIndex];
const totalQuantity = existingItem.quantity + action.payload.quantity;
newItems = state.items.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: totalQuantity }
: item
)
} else {
// Добавляем новый товар
const newItem: CartItem = {
...action.payload,
id: generateId(),
selected: true,
favorite: false
}
newItems = [...state.items, newItem]
}
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.id !== action.payload)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'UPDATE_QUANTITY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: Math.max(1, action.payload.quantity) }
: item
)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'TOGGLE_SELECT': {
const newItems = state.items.map(item =>
item.id === action.payload
? { ...item, selected: !item.selected }
: item
)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'TOGGLE_FAVORITE': {
const newItems = state.items.map(item =>
item.id === action.payload
? { ...item, favorite: !item.favorite }
: item
)
return {
...state,
items: newItems
}
}
case 'UPDATE_COMMENT': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, comment: action.payload.comment }
: item
)
return {
...state,
items: newItems
}
}
case 'UPDATE_ORDER_COMMENT': {
return {
...state,
orderComment: action.payload
}
}
case 'SELECT_ALL': {
const allSelected = state.items.every(item => item.selected)
const newItems = state.items.map(item => ({
...item,
selected: !allSelected
}))
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'REMOVE_ALL': {
const newSummary = calculateSummary([], state.delivery.price)
return {
...state,
items: [],
summary: newSummary
}
}
case 'REMOVE_SELECTED': {
const newItems = state.items.filter(item => !item.selected)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'UPDATE_DELIVERY': {
const newDelivery = { ...state.delivery, ...action.payload }
const newSummary = calculateSummary(state.items, newDelivery.price)
return {
...state,
delivery: newDelivery,
summary: newSummary
}
}
case 'CLEAR_CART': {
const newSummary = calculateSummary([], state.delivery.price)
return {
...state,
items: [],
summary: newSummary
}
}
case 'LOAD_CART': {
const newSummary = calculateSummary(action.payload, state.delivery.price)
return {
...state,
items: action.payload,
summary: newSummary
}
}
case 'LOAD_FULL_STATE': {
const newSummary = calculateSummary(action.payload.items, action.payload.delivery.price || state.delivery.price)
return {
...state,
items: action.payload.items,
delivery: action.payload.delivery,
orderComment: action.payload.orderComment,
summary: newSummary
}
}
case 'SET_LOADING': {
return {
...state,
isLoading: action.payload
}
}
case 'SET_ERROR': {
return {
...state,
error: action.payload,
isLoading: false
}
}
default:
return state
}
}
// Создание контекста
const CartContext = createContext<CartContextType | undefined>(undefined)
// Провайдер корзины
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState)
const [isInitialized, setIsInitialized] = useState(false)
// Загрузка корзины из localStorage при инициализации
useEffect(() => {
if (typeof window === 'undefined') return
console.log('🔄 Загружаем состояние корзины из localStorage...')
const savedCartState = localStorage.getItem('cartState')
if (savedCartState) {
try {
const cartState = JSON.parse(savedCartState)
console.log('✅ Найдено сохраненное состояние корзины:', cartState)
// Загружаем полное состояние корзины
dispatch({ type: 'LOAD_FULL_STATE', payload: cartState })
} catch (error) {
console.error('❌ Ошибка загрузки корзины из localStorage:', error)
// Попытаемся загрузить старый формат (только товары)
const savedCart = localStorage.getItem('cart')
if (savedCart) {
try {
const cartItems = JSON.parse(savedCart)
console.log('✅ Найдены товары в старом формате:', cartItems)
dispatch({ type: 'LOAD_CART', payload: cartItems })
} catch (error) {
console.error('❌ Ошибка загрузки старой корзины:', error)
}
}
}
} else {
console.log(' Сохраненное состояние корзины не найдено')
}
setIsInitialized(true)
}, [])
// Сохранение полного состояния корзины в localStorage при изменении (только после инициализации)
useEffect(() => {
if (!isInitialized || typeof window === 'undefined') return
const stateToSave = {
items: state.items,
delivery: state.delivery,
orderComment: state.orderComment
}
console.log('💾 Сохраняем состояние корзины:', stateToSave)
localStorage.setItem('cartState', JSON.stringify(stateToSave))
// Сохраняем также старый формат для совместимости
localStorage.setItem('cart', JSON.stringify(state.items))
}, [state.items, state.delivery, state.orderComment, isInitialized])
// Функции для работы с корзиной
const addItem = async (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => {
// Проверяем наличие товара на складе перед добавлением
const existingItemIndex = state.items.findIndex(
existingItem =>
(existingItem.productId && existingItem.productId === item.productId) ||
(existingItem.offerKey && existingItem.offerKey === item.offerKey)
)
let totalQuantity = item.quantity;
if (existingItemIndex >= 0) {
const existingItem = state.items[existingItemIndex];
totalQuantity = existingItem.quantity + item.quantity;
}
// Проверяем наличие товара на складе
const availableStock = parseStock(item.stock);
if (availableStock > 0 && totalQuantity > availableStock) {
const errorMessage = `Недостаточно товара в наличии. Доступно: ${availableStock} шт., запрошено: ${totalQuantity} шт.`;
dispatch({ type: 'SET_ERROR', payload: errorMessage });
return { success: false, error: errorMessage };
}
// Если проверка прошла успешно, добавляем товар
dispatch({ type: 'ADD_ITEM', payload: item })
return { success: true }
}
const removeItem = (id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}
const updateQuantity = (id: string, quantity: number) => {
// Найдем товар для проверки наличия
const item = state.items.find(item => item.id === id);
if (item) {
const availableStock = parseStock(item.stock);
if (availableStock > 0 && quantity > availableStock) {
// Показываем ошибку, но не изменяем количество
dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` });
return;
}
}
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
}
const toggleSelect = (id: string) => {
dispatch({ type: 'TOGGLE_SELECT', payload: id })
}
const toggleFavorite = (id: string) => {
dispatch({ type: 'TOGGLE_FAVORITE', payload: id })
}
const updateComment = (id: string, comment: string) => {
dispatch({ type: 'UPDATE_COMMENT', payload: { id, comment } })
}
const updateOrderComment = (comment: string) => {
dispatch({ type: 'UPDATE_ORDER_COMMENT', payload: comment })
}
const selectAll = () => {
dispatch({ type: 'SELECT_ALL' })
}
const removeAll = () => {
dispatch({ type: 'REMOVE_ALL' })
}
const removeSelected = () => {
dispatch({ type: 'REMOVE_SELECTED' })
}
const updateDelivery = (delivery: Partial<DeliveryInfo>) => {
dispatch({ type: 'UPDATE_DELIVERY', payload: delivery })
}
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' })
// Очищаем localStorage при очистке корзины
if (typeof window !== 'undefined') {
localStorage.removeItem('cartState')
localStorage.removeItem('cart')
}
}
const clearError = () => {
dispatch({ type: 'SET_ERROR', payload: '' })
}
const contextValue: CartContextType = {
state,
addItem,
removeItem,
updateQuantity,
toggleSelect,
toggleFavorite,
updateComment,
updateOrderComment,
selectAll,
removeAll,
removeSelected,
updateDelivery,
clearCart,
clearError
}
return (
<CartContext.Provider value={contextValue}>
{children}
</CartContext.Provider>
)
}
// Хук для использования контекста корзины
export const useCart = (): CartContextType => {
const context = useContext(CartContext)
if (!context) {
throw new Error('useCart должен использоваться внутри CartProvider')
}
return context
}