first commit

This commit is contained in:
Bivekich
2025-06-26 06:59:59 +03:00
commit d44874775c
450 changed files with 76635 additions and 0 deletions

View File

@ -0,0 +1,422 @@
'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: '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 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 + deliveryPrice
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) {
// Увеличиваем количество существующего товара
newItems = state.items.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: item.quantity + action.payload.quantity }
: 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 = (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => {
dispatch({ type: 'ADD_ITEM', payload: item })
}
const removeItem = (id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id })
}
const updateQuantity = (id: string, quantity: number) => {
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 contextValue: CartContextType = {
state,
addItem,
removeItem,
updateQuantity,
toggleSelect,
toggleFavorite,
updateComment,
updateOrderComment,
selectAll,
removeAll,
removeSelected,
updateDelivery,
clearCart
}
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
}

View File

@ -0,0 +1,229 @@
'use client'
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import toast from 'react-hot-toast'
import { GET_FAVORITES, ADD_TO_FAVORITES, REMOVE_FROM_FAVORITES, CLEAR_FAVORITES } from '@/lib/favorites-queries'
// Типы
export interface FavoriteItem {
id: string
clientId: string
productId?: string
offerKey?: string
name: string
brand: string
article: string
price?: number
currency?: string
image?: string
createdAt: string
}
export interface FavoriteInput {
productId?: string
offerKey?: string
name: string
brand: string
article: string
price?: number
currency?: string
image?: string
}
interface FavoritesState {
items: FavoriteItem[]
loading: boolean
error: string | null
}
interface FavoritesContextType {
favorites: FavoriteItem[]
loading: boolean
error: string | null
addToFavorites: (item: FavoriteInput) => Promise<void>
removeFromFavorites: (id: string) => Promise<void>
clearFavorites: () => Promise<void>
isFavorite: (productId?: string, offerKey?: string, article?: string, brand?: string) => boolean
}
// Reducer
type FavoritesAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_FAVORITES'; payload: FavoriteItem[] }
| { type: 'ADD_FAVORITE'; payload: FavoriteItem }
| { type: 'REMOVE_FAVORITE'; payload: string }
| { type: 'CLEAR_FAVORITES' }
const favoritesReducer = (state: FavoritesState, action: FavoritesAction): FavoritesState => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload }
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false }
case 'SET_FAVORITES':
return { ...state, items: action.payload, loading: false, error: null }
case 'ADD_FAVORITE':
return {
...state,
items: [action.payload, ...state.items.filter(item => item.id !== action.payload.id)],
loading: false,
error: null
}
case 'REMOVE_FAVORITE':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
loading: false,
error: null
}
case 'CLEAR_FAVORITES':
return { ...state, items: [], loading: false, error: null }
default:
return state
}
}
const initialState: FavoritesState = {
items: [],
loading: false,
error: null,
}
// Контекст
const FavoritesContext = createContext<FavoritesContextType | undefined>(undefined)
// Провайдер
interface FavoritesProviderProps {
children: ReactNode
}
const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(favoritesReducer, initialState)
// Запрос на получение избранного
const { data, loading, error, refetch } = useQuery(GET_FAVORITES, {
errorPolicy: 'all',
onCompleted: (data) => {
if (data?.favorites) {
dispatch({ type: 'SET_FAVORITES', payload: data.favorites })
}
},
onError: (error) => {
console.error('Ошибка загрузки избранного:', error)
dispatch({ type: 'SET_ERROR', payload: error.message })
}
})
// GraphQL мутации с toast уведомлениями
const [addFavoriteMutation] = useMutation(ADD_TO_FAVORITES, {
onCompleted: (data) => {
if (data?.addToFavorites) {
dispatch({ type: 'ADD_FAVORITE', payload: data.addToFavorites })
toast.success('Товар добавлен в избранное')
}
},
onError: (error) => {
console.error('Ошибка добавления в избранное:', error)
toast.error('Ошибка добавления в избранное')
dispatch({ type: 'SET_ERROR', payload: error.message })
}
})
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
onCompleted: () => {
toast.success('Товар удален из избранного')
},
onError: (error) => {
console.error('Ошибка удаления из избранного:', error)
toast.error('Ошибка удаления из избранного')
dispatch({ type: 'SET_ERROR', payload: error.message })
}
})
const [clearFavoritesMutation] = useMutation(CLEAR_FAVORITES, {
onCompleted: () => {
dispatch({ type: 'CLEAR_FAVORITES' })
toast.success('Избранное очищено')
},
onError: (error) => {
console.error('Ошибка очистки избранного:', error)
toast.error('Ошибка очистки избранного')
dispatch({ type: 'SET_ERROR', payload: error.message })
}
})
// Методы для работы с избранным
const addToFavorites = async (item: FavoriteInput) => {
try {
dispatch({ type: 'SET_LOADING', payload: true })
await addFavoriteMutation({
variables: { input: item }
})
} catch (error) {
console.error('Ошибка добавления в избранное:', error)
dispatch({ type: 'SET_ERROR', payload: 'Ошибка добавления в избранное' })
}
}
const removeFromFavorites = async (id: string) => {
try {
dispatch({ type: 'SET_LOADING', payload: true })
await removeFavoriteMutation({
variables: { id }
})
dispatch({ type: 'REMOVE_FAVORITE', payload: id })
} catch (error) {
console.error('Ошибка удаления из избранного:', error)
dispatch({ type: 'SET_ERROR', payload: 'Ошибка удаления из избранного' })
}
}
const clearFavorites = async () => {
try {
dispatch({ type: 'SET_LOADING', payload: true })
await clearFavoritesMutation()
} catch (error) {
console.error('Ошибка очистки избранного:', error)
dispatch({ type: 'SET_ERROR', payload: 'Ошибка очистки избранного' })
}
}
const isFavorite = (productId?: string, offerKey?: string, article?: string, brand?: string): boolean => {
return state.items.some(item => {
// Проверяем по разным комбинациям идентификаторов
if (productId && item.productId === productId) return true
if (offerKey && item.offerKey === offerKey) return true
if (article && brand && item.article === article && item.brand === brand) return true
return false
})
}
const value: FavoritesContextType = {
favorites: state.items,
loading: state.loading || loading,
error: state.error || error?.message || null,
addToFavorites,
removeFromFavorites,
clearFavorites,
isFavorite,
}
return (
<FavoritesContext.Provider value={value}>
{children}
</FavoritesContext.Provider>
)
}
// Хук для использования контекста
export const useFavorites = (): FavoritesContextType => {
const context = useContext(FavoritesContext)
if (!context) {
throw new Error('useFavorites must be used within a FavoritesProvider')
}
return context
}
export { FavoritesProvider }