Добавлен компонент CartIcon и обновлены уведомления о добавлении товара в корзину во всех соответствующих компонентах. Изменены стили текста и иконки в уведомлениях для улучшения визуального восприятия.

This commit is contained in:
Bivekich
2025-07-05 18:38:12 +03:00
parent 78e17a94ab
commit a8c8ae60bb
11 changed files with 166 additions and 43 deletions

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { useCart } from "@/contexts/CartContext";
import toast from "react-hot-toast";
import CartIcon from "./CartIcon";
interface BestPriceCardProps {
bestOfferType: string;
@ -114,12 +115,12 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
// Показываем тоастер об успешном добавлении
toast.success(
<div>
<div className="font-semibold">Товар добавлен в корзину!</div>
<div className="text-sm text-gray-600">{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
</div>,
{
duration: 3000,
icon: '🛒',
icon: <CartIcon size={20} color="#fff" />,
}
);
} catch (error) {

View File

@ -0,0 +1,25 @@
import React from 'react';
interface CartIconProps {
size?: number;
color?: string;
}
const CartIcon: React.FC<CartIconProps> = ({ size = 24, color = '#fff' }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z"
fill={color}
/>
</svg>
);
};
export default CartIcon;

View File

@ -3,6 +3,7 @@ import CatalogProductCard from "./CatalogProductCard";
import { useArticleImage } from "@/hooks/useArticleImage";
import { useCart } from "@/contexts/CartContext";
import { toast } from "react-hot-toast";
import CartIcon from "./CartIcon";
interface CartRecommendedProps {
recommendedProducts?: any[];
@ -54,12 +55,12 @@ const RecommendedProductCard: React.FC<{
// Показываем успешный тоастер
toast.success(
<div>
<div className="font-semibold">Товар добавлен в корзину!</div>
<div className="text-sm text-gray-600">{item.name || `${item.brand} ${item.articleNumber}`}</div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{item.name || `${item.brand} ${item.articleNumber}`}</div>
</div>,
{
duration: 3000,
icon: '🛒',
icon: <CartIcon size={20} color="#fff" />,
}
);
} catch (error) {

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext";
import toast from "react-hot-toast";
import CartIcon from "./CartIcon";
const INITIAL_OFFERS_LIMIT = 5;
@ -154,12 +155,12 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
// Показываем тоастер вместо alert
toast.success(
<div>
<div className="font-semibold">Товар добавлен в корзину!</div>
<div className="text-sm text-gray-600">{`${brand} ${article} (${quantity} шт.)`}</div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} ${article} (${quantity} шт.)`}</div>
</div>,
{
duration: 3000,
icon: '🛒',
icon: <CartIcon size={20} color="#fff" />,
}
);
};

View File

@ -0,0 +1,25 @@
import React from 'react';
interface DeleteCartIconProps {
size?: number;
color?: string;
}
const DeleteCartIcon: React.FC<DeleteCartIconProps> = ({ size = 24, color = '#ec1c24' }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z"
fill={color}
/>
</svg>
);
};
export default DeleteCartIcon;

View File

@ -37,10 +37,8 @@ const Header: React.FC<HeaderProps> = ({ onOpenAuthModal = () => console.log('Au
// Если мы находимся на странице search-result, восстанавливаем поисковый запрос
if (router.pathname === '/search-result') {
const { article, brand } = router.query;
if (article && brand && typeof article === 'string' && typeof brand === 'string') {
// Формируем поисковый запрос из артикула и бренда
setSearchQuery(`${brand} ${article}`);
} else if (article && typeof article === 'string') {
if (article && typeof article === 'string') {
// Отображаем только артикул, без бренда
setSearchQuery(article);
}
}

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext";
import { toast } from "react-hot-toast";
import CartIcon from "../CartIcon";
interface ProductBuyBlockProps {
offer?: any;
@ -56,12 +57,12 @@ const ProductBuyBlock = ({ offer }: ProductBuyBlockProps) => {
// Показываем успешный тоастер
toast.success(
<div>
<div className="font-semibold">Товар добавлен в корзину!</div>
<div className="text-sm text-gray-600">{offer.name || `${offer.brand} ${offer.articleNumber}`}</div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{offer.name || `${offer.brand} ${offer.articleNumber}`}</div>
</div>,
{
duration: 3000,
icon: '🛒',
icon: <CartIcon size={20} color="#fff" />,
}
);
} catch (error) {

View File

@ -4,6 +4,7 @@ import React, { createContext, useContext, useReducer, useEffect, ReactNode } fr
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'
import DeleteCartIcon from '@/components/DeleteCartIcon'
// Типы
export interface FavoriteItem {
@ -133,7 +134,9 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
onCompleted: () => {
toast.success('Товар удален из избранного')
toast.success('Товар удален из избранного', {
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
})
},
onError: (error) => {
console.error('Ошибка удаления из избранного:', error)

View File

@ -60,8 +60,12 @@ export default function App({ Component, pageProps }: AppProps) {
},
success: {
duration: 3000,
style: {
background: '#22c55e', // Зеленый фон для успешных уведомлений
color: '#fff', // Белый текст
},
iconTheme: {
primary: '#4ade80',
primary: '#22c55e',
secondary: '#fff',
},
},

View File

@ -23,6 +23,7 @@ import { useProductPrices } from '@/hooks/useProductPrices';
import { PriceSkeleton } from '@/components/skeletons/ProductListSkeleton';
import { useCart } from '@/contexts/CartContext';
import toast from 'react-hot-toast';
import CartIcon from '@/components/CartIcon';
const mockData = Array(12).fill({
image: "",
@ -750,7 +751,16 @@ export default function Catalog() {
addItem(itemToAdd);
// Показываем уведомление
toast.success(`Товар "${entity.brand.name} ${entity.code}" добавлен в корзину за ${priceData.price.toLocaleString('ru-RU')}`);
toast.success(
<div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')}`}</div>
</div>,
{
duration: 3000,
icon: <CartIcon size={20} color="#fff" />,
}
);
} else {
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
}

View File

@ -606,12 +606,11 @@ export default function SearchResult() {
return true; // Показываем загружающиеся аналоги
}
const analogOffers = transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
);
// Проверяем, есть ли предложения у аналога
const hasInternalOffers = loadedAnalogData.internalOffers && loadedAnalogData.internalOffers.length > 0;
const hasExternalOffers = loadedAnalogData.externalOffers && loadedAnalogData.externalOffers.length > 0;
// Показываем аналог только если у него есть предложения
return analogOffers.length > 0;
return hasInternalOffers || hasExternalOffers;
});
// Если нет аналогов с предложениями, не показываем секцию
@ -625,11 +624,79 @@ export default function SearchResult() {
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
const analogOffers = loadedAnalogData
? transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
)
: [];
// Если данные аналога загружены, формируем предложения из всех его данных
const analogOffers = loadedAnalogData ? (() => {
const allAnalogOffers: any[] = [];
// Добавляем внутренние предложения
if (loadedAnalogData.internalOffers) {
loadedAnalogData.internalOffers.forEach((offer: any) => {
allAnalogOffers.push({
...offer,
type: 'internal',
brand: loadedAnalogData.brand,
articleNumber: loadedAnalogData.articleNumber,
name: loadedAnalogData.name,
isAnalog: true,
deliveryDuration: offer.deliveryDays
});
});
}
// Добавляем внешние предложения
if (loadedAnalogData.externalOffers) {
loadedAnalogData.externalOffers.forEach((offer: any) => {
allAnalogOffers.push({
...offer,
type: 'external',
brand: offer.brand || loadedAnalogData.brand,
articleNumber: offer.code || loadedAnalogData.articleNumber,
name: offer.name || loadedAnalogData.name,
isAnalog: true,
deliveryDuration: offer.deliveryTime
});
});
}
// Применяем фильтры только если они активны
const filteredAnalogOffers = allAnalogOffers.filter(offer => {
// Фильтр по бренду
if (selectedBrands.length > 0 && !selectedBrands.includes(offer.brand)) {
return false;
}
// Фильтр по цене
if (priceRange && (offer.price < priceRange[0] || offer.price > priceRange[1])) {
return false;
}
// Фильтр по сроку доставки
if (deliveryRange) {
const deliveryDays = offer.deliveryDuration;
if (deliveryDays < deliveryRange[0] || deliveryDays > deliveryRange[1]) {
return false;
}
}
// Фильтр по количеству наличия
if (quantityRange) {
const quantity = offer.quantity;
if (quantity < quantityRange[0] || quantity > quantityRange[1]) {
return false;
}
}
// Фильтр по поисковой строке
if (filterSearchTerm) {
const searchTerm = filterSearchTerm.toLowerCase();
const brandMatch = offer.brand.toLowerCase().includes(searchTerm);
const articleMatch = offer.articleNumber.toLowerCase().includes(searchTerm);
const nameMatch = offer.name.toLowerCase().includes(searchTerm);
if (!brandMatch && !articleMatch && !nameMatch) {
return false;
}
}
return true;
});
return transformOffersForCard(filteredAnalogOffers);
})() : [];
return (
<CoreProductCard
@ -647,20 +714,7 @@ export default function SearchResult() {
{(() => {
// Проверяем, есть ли еще аналоги с предложениями для загрузки
const remainingAnalogs = result.analogs.slice(visibleAnalogsCount);
const hasMoreAnalogsWithOffers = remainingAnalogs.some((analog: any) => {
const analogKey = `${analog.brand}-${analog.articleNumber}`;
const loadedAnalogData = loadedAnalogs[analogKey];
if (!loadedAnalogData) {
return true; // Могут быть предложения у незагруженных аналогов
}
const analogOffers = transformOffersForCard(
filteredOffers.filter(o => o.isAnalog && o.articleNumber === analog.articleNumber)
);
return analogOffers.length > 0;
});
const hasMoreAnalogsWithOffers = remainingAnalogs.length > 0;
return hasMoreAnalogsWithOffers && (
<div className="w-layout-hflex pagination">