diff --git a/src/components/BestPriceItem.tsx b/src/components/BestPriceItem.tsx index 95c8885..8054f0a 100644 --- a/src/components/BestPriceItem.tsx +++ b/src/components/BestPriceItem.tsx @@ -1,4 +1,7 @@ import React from "react"; +import { useCart } from "@/contexts/CartContext"; +import { useFavorites } from "@/contexts/FavoritesContext"; +import toast from "react-hot-toast"; interface BestPriceItemProps { image: string; @@ -7,6 +10,8 @@ interface BestPriceItemProps { oldPrice: string; title: string; brand: string; + article?: string; + productId?: string; onAddToCart?: (e: React.MouseEvent) => void; } @@ -17,14 +22,129 @@ const BestPriceItem: React.FC = ({ oldPrice, title, brand, + article, + productId, onAddToCart, }) => { + const { addItem } = useCart(); + const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); + + // Проверяем, есть ли товар в избранном + const isItemFavorite = isFavorite(productId, undefined, article, brand); + + // Функция для парсинга цены из строки + const parsePrice = (priceStr: string): number => { + const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.'); + return parseFloat(cleanPrice) || 0; + }; + + // Обработчик добавления в корзину + const handleAddToCart = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Если передан кастомный обработчик, используем его + if (onAddToCart) { + onAddToCart(e); + return; + } + + try { + const numericPrice = parsePrice(price); + + if (numericPrice <= 0) { + toast.error('Цена товара не найдена'); + return; + } + + // Добавляем товар в корзину + const result = await addItem({ + productId: productId, + name: title, + description: `${brand} - ${title}`, + brand: brand, + article: article, + price: numericPrice, + currency: 'RUB', + quantity: 1, + image: image, + supplier: 'Protek', + deliveryTime: '1 день', + isExternal: false + }); + + if (result.success) { + // Показываем успешный тоастер + toast.success( +
+
Товар добавлен в корзину!
+
{`${brand} - ${title}`}
+
, + { + duration: 3000, + } + ); + } else { + // Показываем ошибку + toast.error(result.error || 'Ошибка при добавлении товара в корзину'); + } + } catch (error) { + console.error('Ошибка добавления в корзину:', error); + toast.error('Ошибка при добавлении товара в корзину'); + } + }; + + // Обработчик клика по иконке избранного + const handleFavoriteClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (isItemFavorite) { + // Находим товар в избранном и удаляем + const favoriteItem = favorites.find((fav: any) => { + if (productId && fav.productId === productId) return true; + if (fav.article === article && fav.brand === brand) return true; + return false; + }); + + if (favoriteItem) { + removeFromFavorites(favoriteItem.id); + toast.success('Товар удален из избранного'); + } + } else { + // Добавляем в избранное + const numericPrice = parsePrice(price); + addToFavorites({ + productId, + name: title, + brand: brand, + article: article || '', + price: numericPrice, + currency: 'RUB', + image: image + }); + toast.success('Товар добавлен в избранное'); + } + }; + return (
-
+
- +
@@ -46,7 +166,13 @@ const BestPriceItem: React.FC = ({
{title}
- +
diff --git a/src/components/TopSalesItem.tsx b/src/components/TopSalesItem.tsx new file mode 100644 index 0000000..cb28a31 --- /dev/null +++ b/src/components/TopSalesItem.tsx @@ -0,0 +1,164 @@ +import React from "react"; +import { useCart } from "@/contexts/CartContext"; +import { useFavorites } from "@/contexts/FavoritesContext"; +import toast from "react-hot-toast"; + +interface TopSalesItemProps { + image: string; + price: string; + title: string; + brand: string; + article?: string; + productId?: string; + onAddToCart?: (e: React.MouseEvent) => void; +} + +const TopSalesItem: React.FC = ({ + image, + price, + title, + brand, + article, + productId, + onAddToCart, +}) => { + const { addItem } = useCart(); + const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); + + // Проверяем, есть ли товар в избранном + const isItemFavorite = isFavorite(productId, undefined, article, brand); + + // Функция для парсинга цены из строки + const parsePrice = (priceStr: string): number => { + const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.'); + return parseFloat(cleanPrice) || 0; + }; + + // Обработчик клика по корзине + const handleAddToCart = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (onAddToCart) { + onAddToCart(e); + return; + } + + try { + if (!article || !brand) { + toast.error('Недостаточно данных для добавления товара в корзину'); + return; + } + + const numericPrice = parsePrice(price); + + addItem({ + name: title, + brand: brand, + article: article, + description: title, + price: numericPrice, + quantity: 1, + currency: 'RUB', + image: image, + isExternal: true + }); + + toast.success('Товар добавлен в корзину'); + } catch (error) { + console.error('Ошибка добавления в корзину:', error); + toast.error('Ошибка добавления товара в корзину'); + } + }; + + // Обработчик клика по иконке избранного + const handleFavoriteClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (isItemFavorite) { + // Находим товар в избранном и удаляем + const favoriteItem = favorites.find((fav: any) => { + if (productId && fav.productId === productId) return true; + if (fav.article === article && fav.brand === brand) return true; + return false; + }); + + if (favoriteItem) { + removeFromFavorites(favoriteItem.id); + toast.success('Товар удален из избранного'); + } + } else { + // Добавляем в избранное + const numericPrice = parsePrice(price); + addToFavorites({ + productId, + name: title, + brand: brand, + article: article || '', + price: numericPrice, + currency: 'RUB', + image: image + }); + toast.success('Товар добавлен в избранное'); + } + }; + + return ( + + ); +}; + +export default TopSalesItem; \ No newline at end of file diff --git a/src/components/index/BestPriceSection.tsx b/src/components/index/BestPriceSection.tsx index c96245b..f86814d 100644 --- a/src/components/index/BestPriceSection.tsx +++ b/src/components/index/BestPriceSection.tsx @@ -1,44 +1,111 @@ import React from "react"; +import { useQuery } from "@apollo/client"; import BestPriceItem from "../BestPriceItem"; +import { GET_BEST_PRICE_PRODUCTS } from "../../lib/graphql"; -// Моковые данные для лучших цен -const bestPriceItems = [ - { - image: "images/162615.webp", - discount: "-35%", - price: "от 17 087 ₽", - oldPrice: "22 347 ₽", - title: 'Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60', - brand: "TYUMEN BATTERY", - }, - // ...добавьте еще 7 карточек для примера - ...Array(7).fill(0).map((_, i) => ({ - image: "images/162615.webp", - discount: "-35%", - price: `от ${(17087 + i * 1000).toLocaleString('ru-RU')} ₽`, - oldPrice: `${(22347 + i * 1000).toLocaleString('ru-RU')} ₽`, - title: `Товар №${i + 2}`, - brand: `Бренд ${i + 2}`, - })) -]; +interface BestPriceProductData { + id: string; + productId: string; + discount: number; + isActive: boolean; + sortOrder: number; + product: { + id: string; + name: string; + article?: string; + brand?: string; + retailPrice?: number; + images: { url: string; alt?: string }[]; + }; +} -const BestPriceSection: React.FC = () => ( -
-
-
-
-

ЛУЧШАЯ ЦЕНА!

-
Подборка лучших предложенийпо цене
- Показать все +const BestPriceSection: React.FC = () => { + const { data, loading, error } = useQuery(GET_BEST_PRICE_PRODUCTS); + + if (loading) { + return ( +
+
+
+
+

ЛУЧШАЯ ЦЕНА!

+
Загрузка...
+
+
-
- {bestPriceItems.map((item, i) => ( - - ))} +
+ ); + } + + if (error) { + console.error('Ошибка загрузки товаров с лучшей ценой:', error); + return ( +
+
+
+
+

ЛУЧШАЯ ЦЕНА!

+
Ошибка загрузки данных
+
+
+
+
+ ); + } + + const bestPriceProducts: BestPriceProductData[] = data?.bestPriceProducts || []; + + // Функция для форматирования цены + const formatPrice = (price?: number) => { + if (!price) return '—'; + return `от ${price.toLocaleString('ru-RU')} ₽`; + }; + + // Функция для расчета цены со скидкой + const calculateDiscountedPrice = (price?: number, discount?: number) => { + if (!price || !discount) return price; + return price * (1 - discount / 100); + }; + + // Преобразование данных для компонента BestPriceItem + const bestPriceItems = bestPriceProducts + .filter(item => item.isActive) + .sort((a, b) => a.sortOrder - b.sortOrder) + .slice(0, 8) // Ограничиваем до 8 товаров + .map(item => ({ + image: item.product.images?.[0]?.url || "images/162615.webp", // Fallback изображение + discount: `-${item.discount}%`, + price: formatPrice(calculateDiscountedPrice(item.product.retailPrice, item.discount)), + oldPrice: formatPrice(item.product.retailPrice), + title: item.product.name, + brand: item.product.brand || "", + article: item.product.article, + productId: item.product.id, + })); + + // Если нет товаров, не показываем секцию + if (bestPriceItems.length === 0) { + return null; + } + + return ( +
+
+
+
+

ЛУЧШАЯ ЦЕНА!

+
Подборка лучших предложенийпо цене
+ Показать все +
+
+ {bestPriceItems.map((item, i) => ( + + ))} +
-
-
-); + + ); +}; export default BestPriceSection; \ No newline at end of file diff --git a/src/components/index/ProductOfDaySection.tsx b/src/components/index/ProductOfDaySection.tsx index 02f3ca2..7bc16f8 100644 --- a/src/components/index/ProductOfDaySection.tsx +++ b/src/components/index/ProductOfDaySection.tsx @@ -1,67 +1,350 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; +import { useQuery } from '@apollo/client'; +import { GET_DAILY_PRODUCTS, PARTS_INDEX_SEARCH_BY_ARTICLE } from '@/lib/graphql'; +import Link from 'next/link'; -const ProductOfDaySection: React.FC = () => ( -
-
-
-
-
-
-
+interface DailyProduct { + id: string; + discount?: number; + isActive: boolean; + sortOrder: number; + product: { + id: string; + name: string; + slug: string; + article?: string; + brand?: string; + retailPrice?: number; + wholesalePrice?: number; + images: Array<{ + id: string; + url: string; + alt?: string; + order: number; + }>; + }; +} + +const ProductOfDaySection: React.FC = () => { + // Получаем текущую дату в формате YYYY-MM-DD + const today = new Date().toISOString().split('T')[0]; + + // Состояние для текущего слайда + const [currentSlide, setCurrentSlide] = useState(0); + const sliderRef = useRef(null); + + const { data, loading, error } = useQuery<{ dailyProducts: DailyProduct[] }>( + GET_DAILY_PRODUCTS, + { + variables: { displayDate: today }, + errorPolicy: 'all' + } + ); + + // Фильтруем только активные товары и сортируем по sortOrder + const activeProducts = React.useMemo(() => { + if (!data?.dailyProducts) return []; + return data.dailyProducts + .filter(item => item.isActive) + .sort((a, b) => a.sortOrder - b.sortOrder); + }, [data]); + + // Получаем данные из PartsIndex для текущего товара + const currentProduct = activeProducts[currentSlide]; + const { data: partsIndexData } = useQuery( + PARTS_INDEX_SEARCH_BY_ARTICLE, + { + variables: { + articleNumber: currentProduct?.product?.article || '', + brandName: currentProduct?.product?.brand || '', + lang: 'ru' + }, + skip: !currentProduct?.product?.article || !currentProduct?.product?.brand, + errorPolicy: 'ignore' + } + ); + + // Функция для расчета цены со скидкой + const calculateDiscountedPrice = (price: number, discount?: number) => { + if (!discount) return price; + return price * (1 - discount / 100); + }; + + // Функция для форматирования цены + const formatPrice = (price: number) => { + return new Intl.NumberFormat('ru-RU').format(Math.round(price)); + }; + + // Функция для получения изображения товара + const getProductImage = (product: DailyProduct['product']) => { + // Сначала пытаемся использовать собственные изображения товара + const productImage = product.images + ?.sort((a, b) => a.order - b.order) + ?.[0]; + + if (productImage) { + return { + url: productImage.url, + alt: productImage.alt || product.name, + source: 'internal' + }; + } + + // Если нет собственных изображений, используем PartsIndex + const partsIndexImage = partsIndexData?.partsIndexSearchByArticle?.images?.[0]; + if (partsIndexImage) { + return { + url: partsIndexImage, + alt: product.name, + source: 'partsindex' + }; + } + + return null; + }; + + // Обработчики для слайдера + const handlePrevSlide = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setCurrentSlide(prev => prev === 0 ? activeProducts.length - 1 : prev - 1); + }; + + const handleNextSlide = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setCurrentSlide(prev => prev === activeProducts.length - 1 ? 0 : prev + 1); + }; + + const handlePrevSlideTouch = (e: React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + setCurrentSlide(prev => prev === 0 ? activeProducts.length - 1 : prev - 1); + }; + + const handleNextSlideTouch = (e: React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + setCurrentSlide(prev => prev === activeProducts.length - 1 ? 0 : prev + 1); + }; + + const handleSlideIndicator = (index: number) => { + setCurrentSlide(index); + }; + + // Сброс слайда при изменении товаров + useEffect(() => { + setCurrentSlide(0); + }, [activeProducts]); + + // Если нет активных товаров дня, не показываем секцию + if (loading || error || activeProducts.length === 0) { + return null; + } + + const product = currentProduct.product; + const productImage = getProductImage(product); + + const originalPrice = product.retailPrice || product.wholesalePrice || 0; + const discountedPrice = calculateDiscountedPrice(originalPrice, currentProduct.discount); + const hasDiscount = currentProduct.discount && currentProduct.discount > 0; + + return ( +
+
+
+
+
+ {activeProducts.map((_, index) => ( +
+
+
+ ))}
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
-

ТОВАРЫ ДНЯ

-
-35%
-
-
-
-
-
от 17 087 ₽
-
22 347 ₽
+ + {/* Стрелки слайдера (показываем только если товаров больше 1) */} + {activeProducts.length > 1 && ( + <> +
+
+
+ + + +
+
+
+
+
+
+ + + +
+
+
+ + )} + + {/* Индикаторы слайдов */} + {activeProducts.length > 1 && ( +
+ {activeProducts.map((_, index) => ( +
handleSlideIndicator(index)} + onMouseDown={(e) => e.preventDefault()} + style={{ cursor: 'pointer', zIndex: 10 }} + >
+ ))}
-
Аккумуляторная батарея TYUMEN BATTERY "STANDARD", 6CT-60L, 60
-
+ )}
-
-
-
- -
+ +
+
+

ТОВАРЫ ДНЯ

+ {hasDiscount && ( +
-{currentProduct.discount}%
+ )}
-
-
- -
+ +
+
+
+
+ от {formatPrice(discountedPrice)} ₽ +
+ {hasDiscount && ( +
+ {formatPrice(originalPrice)} ₽ +
+ )} +
+
+ {product.brand && `${product.brand} `} + {product.name} +
+ {/* Счетчик товаров если их больше одного */} + {activeProducts.length > 1 && ( +
+ {currentSlide + 1} из {activeProducts.length} +
+ )} +
+ + {productImage && ( +
+ {productImage.alt} + {/* Метка источника изображения */} + {productImage.source === 'partsindex' && ( +
+ Parts Index +
+ )} +
+ )}
-
-
-
+ +
+ {/* Левая стрелка - предыдущий товар */} + {activeProducts.length > 1 ? ( +
e.preventDefault()} + onTouchStart={handlePrevSlideTouch} + style={{ cursor: 'pointer' }} + title="Предыдущий товар" + > +
+ + + +
+
+ ) : ( +
+
+ + + +
+
+ )} + + {/* Правая стрелка - следующий товар */} + {activeProducts.length > 1 ? ( +
e.preventDefault()} + onTouchStart={handleNextSlideTouch} + style={{ cursor: 'pointer' }} + title="Следующий товар" + > +
+ + + +
+
+ ) : ( +
+
+ + + +
+
+ )} + + {/* Индикаторы точки */} +
+ {activeProducts.length > 1 ? ( + activeProducts.map((_, index) => ( +
handleSlideIndicator(index)} + style={{ + cursor: 'pointer', + opacity: index === currentSlide ? 1 : 0.5, + backgroundColor: index === currentSlide ? 'currentColor' : 'rgba(128,128,128,0.5)' + }} + title={`Товар ${index + 1}`} + /> + )) + ) : ( + <> +
+ + )} +
-
-); + ); +}; export default ProductOfDaySection; \ No newline at end of file diff --git a/src/components/index/TopSalesSection.tsx b/src/components/index/TopSalesSection.tsx index 40ffc65..471d6ca 100644 --- a/src/components/index/TopSalesSection.tsx +++ b/src/components/index/TopSalesSection.tsx @@ -1,54 +1,122 @@ import React from "react"; -import ArticleCard from "../ArticleCard"; -import { PartsAPIArticle } from "@/types/partsapi"; +import { useQuery } from "@apollo/client"; +import TopSalesItem from "../TopSalesItem"; +import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql"; -// Моковые данные для топ продаж -const topSalesArticles: PartsAPIArticle[] = [ - { - artId: "1", - artArticleNr: "6CT-60L", - artSupBrand: "TYUMEN BATTERY", - supBrand: "TYUMEN BATTERY", - supId: 1, - productGroup: "Аккумуляторная батарея", - ptId: 1, - }, - { - artId: "2", - artArticleNr: "A0001", - artSupBrand: "Borsehung", - supBrand: "Borsehung", - supId: 2, - productGroup: "Масляный фильтр", - ptId: 2, - }, - // ...добавьте еще 6 статей для примера - ...Array(6).fill(0).map((_, i) => ({ - artId: `${i+3}`, - artArticleNr: `ART${i+3}`, - artSupBrand: `Brand${i+3}`, - supBrand: `Brand${i+3}`, - supId: i+3, - productGroup: `Product Group ${i+3}`, - ptId: i+3, - })) -]; +interface TopSalesProductData { + id: string; + productId: string; + isActive: boolean; + sortOrder: number; + product: { + id: string; + name: string; + article?: string; + brand?: string; + retailPrice?: number; + images: { url: string; alt?: string }[]; + }; +} -const TopSalesSection: React.FC = () => ( -
-
-
-
-

Топ продаж

+const TopSalesSection: React.FC = () => { + const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS); + + if (loading) { + return ( +
+
+
+
+

Топ продаж

+
+
+
Загрузка...
+
+
-
- {topSalesArticles.map((article, i) => ( - - ))} +
+ ); + } + + if (error) { + console.error('Ошибка загрузки топ продаж:', error); + return ( +
+
+
+
+

Топ продаж

+
+
+
Ошибка загрузки
+
+
+
+
+ ); + } + + // Фильтруем активные товары и сортируем по sortOrder + const activeTopSalesProducts = (data?.topSalesProducts || []) + .filter((item: TopSalesProductData) => item.isActive) + .sort((a: TopSalesProductData, b: TopSalesProductData) => a.sortOrder - b.sortOrder) + .slice(0, 8); // Ограничиваем до 8 товаров + + if (activeTopSalesProducts.length === 0) { + return ( +
+
+
+
+

Топ продаж

+
+
+
Нет товаров в топ продаж
+
+
+
+
+ ); + } + + return ( +
+
+
+
+

Топ продаж

+
+
+ {activeTopSalesProducts.map((item: TopSalesProductData) => { + const product = item.product; + const price = product.retailPrice + ? `от ${product.retailPrice.toLocaleString('ru-RU')} ₽` + : 'По запросу'; + + const image = product.images && product.images.length > 0 + ? product.images[0].url + : '/images/162615.webp'; // Fallback изображение + + const title = product.name; + const brand = product.brand || 'Неизвестный бренд'; + + return ( + + ); + })} +
-
-
-); +
+ ); +}; export default TopSalesSection; \ No newline at end of file diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts index 55782d7..6add222 100644 --- a/src/lib/graphql.ts +++ b/src/lib/graphql.ts @@ -1,5 +1,50 @@ import { gql } from '@apollo/client' +export const GET_BEST_PRICE_PRODUCTS = gql` + query GetBestPriceProducts { + bestPriceProducts { + id + productId + discount + isActive + sortOrder + product { + id + name + article + brand + retailPrice + images { + url + alt + } + } + } + } +` + +export const GET_TOP_SALES_PRODUCTS = gql` + query GetTopSalesProducts { + topSalesProducts { + id + productId + isActive + sortOrder + product { + id + name + article + brand + retailPrice + images { + url + alt + } + } + } + } +` + export const CHECK_CLIENT_BY_PHONE = gql` mutation CheckClientByPhone($phone: String!) { checkClientByPhone(phone: $phone) { @@ -1606,4 +1651,31 @@ export const GET_CATEGORY_PRODUCTS_WITH_OFFERS = gql` hasOffers } } +` + +// Запрос для получения товаров дня +export const GET_DAILY_PRODUCTS = gql` + query GetDailyProducts($displayDate: String!) { + dailyProducts(displayDate: $displayDate) { + id + discount + isActive + sortOrder + product { + id + name + slug + article + brand + retailPrice + wholesalePrice + images { + id + url + alt + order + } + } + } + } ` \ No newline at end of file