278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
"use client"
|
||
|
||
import React, { createContext, useContext, useState, useEffect, ReactNode, useMemo, useRef } from "react" // Добавляем useRef
|
||
import { useToast } from "@/components/ui/use-toast"
|
||
import type { CartItemCreate } from "@/lib/cart"
|
||
// Объединяем импорты из cart-store
|
||
import cartStore, { Cart, createEmptyCart } from "@/lib/cart-store"
|
||
import { apiStatus } from "@/lib/api"
|
||
|
||
// Глобальная переменная для отслеживания последней синхронизации
|
||
let lastSyncTimestamp = 0
|
||
const SYNC_THROTTLE_MS = 5000 // Минимальное время между синхронизациями (5 секунд)
|
||
|
||
// Интерфейс контекста корзины
|
||
interface CartContextType {
|
||
cart: Cart
|
||
loading: boolean
|
||
error: string | null
|
||
itemCount: number
|
||
addToCart: (item: CartItemCreate) => Promise<boolean>
|
||
updateCartItem: (id: number, quantity: number) => Promise<boolean>
|
||
removeFromCart: (id: number) => Promise<boolean>
|
||
clearCart: () => Promise<boolean>
|
||
synchronizeCart: () => Promise<void>
|
||
isAuthenticated: boolean
|
||
}
|
||
|
||
// Создание контекста
|
||
const CartContext = createContext<CartContextType | undefined>(undefined)
|
||
|
||
// Удаляем дублирующийся импорт
|
||
|
||
// Провайдер контекста
|
||
export function CartProvider({ children }: { children: ReactNode }) {
|
||
// Инициализируем пустой корзиной, чтобы серверный и первый клиентский рендер совпадали
|
||
const [cart, setCart] = useState<Cart>(createEmptyCart())
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [syncInProgress, setSyncInProgress] = useState(false)
|
||
// Внутреннее состояние для отслеживания аутентификации
|
||
const [isAuthenticated, setIsAuthenticated] = useState(apiStatus.isAuthenticated);
|
||
const initialSyncDoneRef = useRef(false); // Флаг для отслеживания первичной синхронизации
|
||
const { toast } = useToast()
|
||
|
||
// Подписка на изменения корзины
|
||
useEffect(() => {
|
||
const unsubscribe = cartStore.subscribe(() => {
|
||
setCart(cartStore.getState())
|
||
})
|
||
return () => unsubscribe()
|
||
}, [])
|
||
|
||
// После монтирования на клиенте, устанавливаем актуальное состояние из cartStore
|
||
useEffect(() => {
|
||
setCart(cartStore.getState());
|
||
}, []);
|
||
|
||
// Следим за изменением глобального apiStatus.isAuthenticated
|
||
useEffect(() => {
|
||
// Обновляем внутреннее состояние только если оно действительно изменилось
|
||
if (apiStatus.isAuthenticated !== isAuthenticated) {
|
||
setIsAuthenticated(apiStatus.isAuthenticated);
|
||
// Сбрасываем флаг синхронизации при изменении статуса (например, при логауте)
|
||
if (!apiStatus.isAuthenticated) {
|
||
initialSyncDoneRef.current = false;
|
||
}
|
||
}
|
||
// Эта зависимость все еще может быть не идеальной, но лучше, чем ничего.
|
||
// В идеале, статус аутентификации должен приходить из AuthContext.
|
||
}, [apiStatus.isAuthenticated, isAuthenticated]);
|
||
|
||
// Синхронизация корзины при изменении статуса аутентификации на true
|
||
useEffect(() => {
|
||
// Запускаем синхронизацию только если пользователь стал аутентифицированным
|
||
// и первичная синхронизация для этой сессии еще не выполнялась.
|
||
if (isAuthenticated && !initialSyncDoneRef.current) {
|
||
const now = Date.now();
|
||
// Дополнительно проверяем троттлинг на всякий случай
|
||
if (!syncInProgress && now - lastSyncTimestamp > SYNC_THROTTLE_MS) {
|
||
console.log("Запуск первичной синхронизации корзины для аутентифицированного пользователя.");
|
||
synchronizeCart();
|
||
initialSyncDoneRef.current = true; // Отмечаем, что первичная синхронизация запущена
|
||
}
|
||
}
|
||
}, [isAuthenticated, syncInProgress]); // Зависим от внутреннего состояния isAuthenticated
|
||
|
||
// Синхронизация локальной корзины с сервером с дроттлингом
|
||
const synchronizeCart = async () => {
|
||
try {
|
||
// Если синхронизация уже идет, не запускаем новую
|
||
if (syncInProgress) return
|
||
|
||
setSyncInProgress(true)
|
||
setLoading(true)
|
||
|
||
// Фиксируем время последней синхронизации
|
||
lastSyncTimestamp = Date.now()
|
||
|
||
await cartStore.syncWithServer()
|
||
} catch (err) {
|
||
setError('Не удалось синхронизировать корзину')
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось синхронизировать корзину',
|
||
})
|
||
} finally {
|
||
setLoading(false)
|
||
setSyncInProgress(false)
|
||
}
|
||
}
|
||
|
||
// Добавление товара в корзину
|
||
const addToCart = async (item: CartItemCreate) => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const result = await cartStore.addToCart(item)
|
||
|
||
if (result) {
|
||
toast({
|
||
title: 'Товар добавлен',
|
||
description: 'Товар успешно добавлен в корзину',
|
||
})
|
||
return true
|
||
} else {
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось добавить товар в корзину',
|
||
})
|
||
return false
|
||
}
|
||
} catch (err) {
|
||
setError('Не удалось добавить товар в корзину')
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось добавить товар в корзину',
|
||
})
|
||
return false
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// Обновление количества товара в корзине
|
||
const updateCartItem = async (id: number, quantity: number) => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const result = await cartStore.updateCartItem(id, quantity)
|
||
|
||
if (!result) {
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось обновить товар в корзине',
|
||
})
|
||
}
|
||
return result
|
||
} catch (err) {
|
||
setError('Не удалось обновить товар в корзине')
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось обновить товар в корзине',
|
||
})
|
||
return false
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// Удаление товара из корзины
|
||
const removeFromCart = async (id: number) => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const result = await cartStore.removeFromCart(id)
|
||
|
||
if (result) {
|
||
toast({
|
||
title: 'Товар удален',
|
||
description: 'Товар успешно удален из корзины',
|
||
})
|
||
return true
|
||
} else {
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось удалить товар из корзины',
|
||
})
|
||
return false
|
||
}
|
||
} catch (err) {
|
||
setError('Не удалось удалить товар из корзины')
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось удалить товар из корзины',
|
||
})
|
||
return false
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// Очистка корзины
|
||
const clearCart = async () => {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const result = await cartStore.clearCart()
|
||
|
||
if (result) {
|
||
toast({
|
||
title: 'Корзина очищена',
|
||
description: 'Корзина успешно очищена',
|
||
})
|
||
return true
|
||
} else {
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось очистить корзину',
|
||
})
|
||
return false
|
||
}
|
||
} catch (err) {
|
||
setError('Не удалось очистить корзину')
|
||
toast({
|
||
variant: 'destructive',
|
||
title: 'Ошибка',
|
||
description: 'Не удалось очистить корзину',
|
||
})
|
||
return false
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// Вычисляем itemCount на основе состояния cart
|
||
const itemCount = useMemo(() => {
|
||
return cart.items.reduce((sum, item) => sum + item.quantity, 0);
|
||
}, [cart.items]);
|
||
|
||
const value = {
|
||
cart,
|
||
loading,
|
||
error,
|
||
addToCart,
|
||
updateCartItem,
|
||
removeFromCart,
|
||
clearCart,
|
||
synchronizeCart,
|
||
itemCount, // Используем вычисленное значение
|
||
isAuthenticated // Используем внутреннее состояние
|
||
}
|
||
|
||
return (
|
||
<CartContext.Provider value={value}>
|
||
{children}
|
||
</CartContext.Provider>
|
||
)
|
||
}
|
||
|
||
// Хук для использования контекста корзины
|
||
export function useCart() {
|
||
const context = useContext(CartContext)
|
||
|
||
if (context === undefined) {
|
||
throw new Error("useCart должен использоваться внутри CartProvider")
|
||
}
|
||
|
||
return context
|
||
}
|