Удалены уведомления об удалении товара из избранного в компонентах BestPriceItem и TopSalesItem. Добавлен новый запрос GraphQL для получения новых поступлений, реализована логика загрузки и отображения данных в компоненте NewArrivalsSection. Обновлены компоненты ProfileHistoryItem и ProfileHistoryMain для поддержки новых пропсов и пагинации. Улучшено взаимодействие с пользователем через обработку кликов и отображение состояния загрузки.
This commit is contained in:
@ -109,7 +109,6 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
|
|||||||
|
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем в избранное
|
// Добавляем в избранное
|
||||||
|
148
src/components/Pagination.tsx
Normal file
148
src/components/Pagination.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
className?: string;
|
||||||
|
showPageInfo?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: React.FC<PaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
className = "",
|
||||||
|
showPageInfo = true
|
||||||
|
}) => {
|
||||||
|
const generatePageNumbers = () => {
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
const delta = 2; // Количество страниц вокруг текущей
|
||||||
|
|
||||||
|
if (totalPages <= 7) {
|
||||||
|
// Если страниц мало, показываем все
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Всегда показываем первую страницу
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
if (currentPage > delta + 2) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем страницы вокруг текущей
|
||||||
|
const start = Math.max(2, currentPage - delta);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + delta);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - delta - 1) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда показываем последнюю страницу
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageNumbers = generatePageNumbers();
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col items-center space-y-3 ${className}`}>
|
||||||
|
{/* Основные кнопки пагинации */}
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
{/* Предыдущая страница */}
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Номера страниц */}
|
||||||
|
{pageNumbers.map((page, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{page === '...' ? (
|
||||||
|
<span className="flex items-center justify-center w-10 h-10 text-gray-400">
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<circle cx="3" cy="10" r="1.5" />
|
||||||
|
<circle cx="10" cy="10" r="1.5" />
|
||||||
|
<circle cx="17" cy="10" r="1.5" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(page as number)}
|
||||||
|
className={`flex items-center justify-center w-10 h-10 text-sm font-medium border rounded-lg transition-colors ${
|
||||||
|
currentPage === page
|
||||||
|
? 'text-white bg-[#ec1c24] border-[#ec1c24] hover:bg-[#d91920]'
|
||||||
|
: 'text-gray-500 bg-white border-gray-200 hover:bg-gray-50 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Следующая страница */}
|
||||||
|
<button
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center justify-center w-10 h-10 text-sm font-medium text-gray-500 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
style={{ cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Информация о страницах */}
|
||||||
|
{showPageInfo && (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Страница {currentPage} из {totalPages}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
@ -77,7 +77,6 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
|
|||||||
});
|
});
|
||||||
if (favoriteItem) {
|
if (favoriteItem) {
|
||||||
removeFromFavorites(favoriteItem.id);
|
removeFromFavorites(favoriteItem.id);
|
||||||
toast.success('Товар удален из избранного');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const numericPrice = parsePrice(price);
|
const numericPrice = parsePrice(price);
|
||||||
|
@ -1,57 +1,81 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
import { useQuery } from '@apollo/client';
|
||||||
import ArticleCard from "../ArticleCard";
|
import ArticleCard from "../ArticleCard";
|
||||||
|
import CatalogProductCardSkeleton from "../CatalogProductCardSkeleton";
|
||||||
|
import { GET_NEW_ARRIVALS } from "@/lib/graphql";
|
||||||
import { PartsAPIArticle } from "@/types/partsapi";
|
import { PartsAPIArticle } from "@/types/partsapi";
|
||||||
|
|
||||||
// Моковые данные для новых поступлений
|
|
||||||
const newArrivalsArticles: 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,
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
|
|
||||||
const imagePath = "images/162615.webp";
|
|
||||||
|
|
||||||
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
const SCROLL_AMOUNT = 340; // px, ширина одной карточки + отступ
|
||||||
|
|
||||||
|
// Интерфейс для товара из GraphQL
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
article?: string;
|
||||||
|
brand?: string;
|
||||||
|
retailPrice?: number;
|
||||||
|
wholesalePrice?: number;
|
||||||
|
createdAt: string;
|
||||||
|
images: Array<{
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
order: number;
|
||||||
|
}>;
|
||||||
|
categories: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для преобразования Product в PartsAPIArticle
|
||||||
|
const transformProductToArticle = (product: Product, index: number): PartsAPIArticle => {
|
||||||
|
return {
|
||||||
|
artId: product.id,
|
||||||
|
artArticleNr: product.article || `PROD-${product.id}`,
|
||||||
|
artSupBrand: product.brand || 'Unknown Brand',
|
||||||
|
supBrand: product.brand || 'Unknown Brand',
|
||||||
|
supId: index + 1,
|
||||||
|
productGroup: product.categories?.[0]?.name || product.name,
|
||||||
|
ptId: index + 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const NewArrivalsSection: React.FC = () => {
|
const NewArrivalsSection: React.FC = () => {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Получаем новые поступления через GraphQL
|
||||||
|
const { data, loading, error } = useQuery(GET_NEW_ARRIVALS, {
|
||||||
|
variables: { limit: 8 }
|
||||||
|
});
|
||||||
|
|
||||||
const scrollLeft = () => {
|
const scrollLeft = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
scrollRef.current.scrollBy({ left: -SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollRight = () => {
|
const scrollRight = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
scrollRef.current.scrollBy({ left: SCROLL_AMOUNT, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Преобразуем данные для ArticleCard
|
||||||
|
const newArrivalsArticles = data?.newArrivals?.map((product: Product, index: number) =>
|
||||||
|
transformProductToArticle(product, index)
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
// Получаем изображения для товаров
|
||||||
|
const getProductImage = (product: Product): string => {
|
||||||
|
if (product.images && product.images.length > 0) {
|
||||||
|
return product.images[0].url;
|
||||||
|
}
|
||||||
|
return "/images/162615.webp"; // fallback изображение
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="main">
|
<section className="main">
|
||||||
<div className="w-layout-blockcontainer container w-container">
|
<div className="w-layout-blockcontainer container w-container">
|
||||||
@ -60,18 +84,71 @@ const NewArrivalsSection: React.FC = () => {
|
|||||||
<h2 className="heading-4">Новое поступление</h2>
|
<h2 className="heading-4">Новое поступление</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="carousel-row">
|
<div className="carousel-row">
|
||||||
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
|
<button
|
||||||
|
className="carousel-arrow carousel-arrow-left"
|
||||||
|
onClick={scrollLeft}
|
||||||
|
aria-label="Прокрутить влево"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
|
||||||
{newArrivalsArticles.map((article, i) => (
|
{loading ? (
|
||||||
<ArticleCard key={article.artId || i} article={{ ...article, artId: article.artId }} index={i} image={imagePath} />
|
// Показываем скелетоны во время загрузки
|
||||||
))}
|
Array(8).fill(0).map((_, index) => (
|
||||||
|
<CatalogProductCardSkeleton key={`skeleton-${index}`} />
|
||||||
|
))
|
||||||
|
) : error ? (
|
||||||
|
// Показываем сообщение об ошибке
|
||||||
|
<div className="error-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Не удалось загрузить новые поступления</p>
|
||||||
|
<p style={{ fontSize: '14px', marginTop: '8px' }}>
|
||||||
|
{error.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : newArrivalsArticles.length > 0 ? (
|
||||||
|
// Показываем товары
|
||||||
|
newArrivalsArticles.map((article: PartsAPIArticle, index: number) => {
|
||||||
|
const product = data.newArrivals[index];
|
||||||
|
const image = getProductImage(product);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArticleCard
|
||||||
|
key={article.artId || `article-${index}`}
|
||||||
|
article={article}
|
||||||
|
index={index}
|
||||||
|
image={image}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
// Показываем сообщение о том, что товаров нет
|
||||||
|
<div className="no-products-message" style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#666',
|
||||||
|
minWidth: '300px'
|
||||||
|
}}>
|
||||||
|
<p>Пока нет новых поступлений</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
|
|
||||||
|
<button
|
||||||
|
className="carousel-arrow carousel-arrow-right"
|
||||||
|
onClick={scrollRight}
|
||||||
|
aria-label="Прокрутить вправо"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/>
|
||||||
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface VehicleInfo {
|
interface VehicleInfo {
|
||||||
brand?: string;
|
brand?: string;
|
||||||
@ -15,6 +16,10 @@ interface ProfileHistoryItemProps {
|
|||||||
vehicleInfo?: VehicleInfo;
|
vehicleInfo?: VehicleInfo;
|
||||||
resultCount?: number;
|
resultCount?: number;
|
||||||
onDelete?: (id: string) => void;
|
onDelete?: (id: string) => void;
|
||||||
|
// Добавляем новые пропсы для поиска
|
||||||
|
searchType?: 'TEXT' | 'ARTICLE' | 'OEM' | 'VIN' | 'PLATE' | 'WIZARD' | 'PART_VEHICLES';
|
||||||
|
articleNumber?: string;
|
||||||
|
brand?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
||||||
@ -26,7 +31,12 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
vehicleInfo,
|
vehicleInfo,
|
||||||
resultCount,
|
resultCount,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
searchType,
|
||||||
|
articleNumber,
|
||||||
|
brand,
|
||||||
}) => {
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onDelete) {
|
if (onDelete) {
|
||||||
@ -34,6 +44,28 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleItemClick = () => {
|
||||||
|
// Определяем куда перенаправлять в зависимости от типа поиска
|
||||||
|
if (searchType === 'VIN' || searchType === 'PLATE') {
|
||||||
|
// Для VIN и госномера перенаправляем на vehicle-search-results
|
||||||
|
router.push(`/vehicle-search-results?q=${encodeURIComponent(name)}`);
|
||||||
|
} else if (searchType === 'ARTICLE' || searchType === 'OEM' || (searchType === 'TEXT' && articleNumber)) {
|
||||||
|
// Для поиска по артикулу/OEM или текстового поиска с артикулом
|
||||||
|
const searchBrand = brand || manufacturer || '';
|
||||||
|
const searchArticle = articleNumber || name;
|
||||||
|
router.push(`/search-result?article=${encodeURIComponent(searchArticle)}&brand=${encodeURIComponent(searchBrand)}`);
|
||||||
|
} else if (searchType === 'TEXT') {
|
||||||
|
// Для обычного текстового поиска
|
||||||
|
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||||
|
} else if (searchType === 'PART_VEHICLES') {
|
||||||
|
// Для поиска автомобилей по детали
|
||||||
|
router.push(`/vehicles-by-part?partNumber=${encodeURIComponent(name)}`);
|
||||||
|
} else {
|
||||||
|
// По умолчанию - обычный поиск
|
||||||
|
router.push(`/search?q=${encodeURIComponent(name)}&mode=parts`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getSearchTypeDisplay = (article: string) => {
|
const getSearchTypeDisplay = (article: string) => {
|
||||||
if (article.includes('TEXT')) return 'Текстовый поиск';
|
if (article.includes('TEXT')) return 'Текстовый поиск';
|
||||||
if (article.includes('ARTICLE')) return 'По артикулу';
|
if (article.includes('ARTICLE')) return 'По артикулу';
|
||||||
@ -48,7 +80,11 @@ const ProfileHistoryItem: React.FC<ProfileHistoryItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-1.5 w-full border border-gray-200 border-solid min-h-[1px] max-md:max-w-full" />
|
<div className="mt-1.5 w-full border border-gray-200 border-solid min-h-[1px] max-md:max-w-full" />
|
||||||
<div className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors">
|
<div
|
||||||
|
className="flex justify-between items-center px-5 pt-1.5 pb-2 mt-1.5 w-full bg-white rounded-lg max-md:max-w-full max-md:flex-col max-md:min-w-0 hover:bg-gray-50 transition-colors"
|
||||||
|
onClick={handleItemClick}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
|
<div className="flex flex-wrap flex-1 shrink gap-5 items-center self-stretch pr-5 my-auto w-full basis-0 max-md:max-w-full max-md:flex-col max-md:gap-2 max-md:p-0 max-md:min-w-0">
|
||||||
<div className="self-stretch my-auto w-40 max-md:w-full text-sm">
|
<div className="self-stretch my-auto w-40 max-md:w-full text-sm">
|
||||||
<div className="font-medium text-gray-900">{date}</div>
|
<div className="font-medium text-gray-900">{date}</div>
|
||||||
|
@ -3,6 +3,7 @@ import { useQuery, useMutation } from '@apollo/client';
|
|||||||
import ProfileHistoryItem from "./ProfileHistoryItem";
|
import ProfileHistoryItem from "./ProfileHistoryItem";
|
||||||
import SearchInput from "./SearchInput";
|
import SearchInput from "./SearchInput";
|
||||||
import ProfileHistoryTabs from "./ProfileHistoryTabs";
|
import ProfileHistoryTabs from "./ProfileHistoryTabs";
|
||||||
|
import Pagination from '../Pagination';
|
||||||
import {
|
import {
|
||||||
GET_PARTS_SEARCH_HISTORY,
|
GET_PARTS_SEARCH_HISTORY,
|
||||||
DELETE_SEARCH_HISTORY_ITEM,
|
DELETE_SEARCH_HISTORY_ITEM,
|
||||||
@ -19,6 +20,10 @@ const ProfileHistoryMain = () => {
|
|||||||
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
const [sortField, setSortField] = useState<"date" | "manufacturer" | "name">("date");
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
const [filteredItems, setFilteredItems] = useState<PartsSearchHistoryItem[]>([]);
|
||||||
|
|
||||||
|
// Состояние пагинации
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10); // Количество элементов на странице
|
||||||
|
|
||||||
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
|
const tabOptions = ["Все", "Сегодня", "Вчера", "Эта неделя", "Этот месяц"];
|
||||||
|
|
||||||
@ -26,7 +31,10 @@ const ProfileHistoryMain = () => {
|
|||||||
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
|
const { data, loading, error, refetch } = useQuery<{ partsSearchHistory: PartsSearchHistoryResponse }>(
|
||||||
GET_PARTS_SEARCH_HISTORY,
|
GET_PARTS_SEARCH_HISTORY,
|
||||||
{
|
{
|
||||||
variables: { limit: 100, offset: 0 },
|
variables: {
|
||||||
|
limit: 1000, // Загружаем больше для клиентской пагинации с фильтрами
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
console.log('История поиска загружена:', data);
|
console.log('История поиска загружена:', data);
|
||||||
@ -161,8 +169,32 @@ const ProfileHistoryMain = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setFilteredItems(filtered);
|
setFilteredItems(filtered);
|
||||||
|
// Сбрасываем страницу на первую при изменении фильтров
|
||||||
|
setCurrentPage(1);
|
||||||
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
}, [historyItems, search, activeTab, selectedManufacturer, sortField, sortOrder]);
|
||||||
|
|
||||||
|
// Вычисляем элементы для текущей страницы
|
||||||
|
const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentPageItems = filteredItems.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Обработчик изменения страницы
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
// Прокручиваем к началу списка при смене страницы
|
||||||
|
const historyContainer = document.querySelector('.flex.flex-col.mt-5.w-full.text-gray-400');
|
||||||
|
if (historyContainer) {
|
||||||
|
historyContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик изменения количества элементов на странице
|
||||||
|
const handleItemsPerPageChange = (newItemsPerPage: number) => {
|
||||||
|
setItemsPerPage(newItemsPerPage);
|
||||||
|
setCurrentPage(1); // Сбрасываем на первую страницу
|
||||||
|
};
|
||||||
|
|
||||||
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
const handleSort = (field: "date" | "manufacturer" | "name") => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
@ -287,6 +319,7 @@ const ProfileHistoryMain = () => {
|
|||||||
setSelectedManufacturer("Все");
|
setSelectedManufacturer("Все");
|
||||||
setSearch("");
|
setSearch("");
|
||||||
setActiveTab("Все");
|
setActiveTab("Все");
|
||||||
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
className="px-4 py-2 text-sm text-gray-600 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
@ -424,7 +457,7 @@ const ProfileHistoryMain = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredItems.map((item) => (
|
currentPageItems.map((item) => (
|
||||||
<ProfileHistoryItem
|
<ProfileHistoryItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
id={item.id}
|
id={item.id}
|
||||||
@ -441,18 +474,58 @@ const ProfileHistoryMain = () => {
|
|||||||
vehicleInfo={item.vehicleInfo}
|
vehicleInfo={item.vehicleInfo}
|
||||||
resultCount={item.resultCount}
|
resultCount={item.resultCount}
|
||||||
onDelete={handleDeleteItem}
|
onDelete={handleDeleteItem}
|
||||||
|
searchType={item.searchType}
|
||||||
|
articleNumber={item.articleNumber}
|
||||||
|
brand={item.brand}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Пагинация */}
|
||||||
{filteredItems.length > 0 && (
|
{filteredItems.length > 0 && (
|
||||||
<div className="mt-4 text-center text-sm text-gray-500">
|
<div className="mt-6 space-y-4">
|
||||||
Показано {filteredItems.length} из {historyItems.length} записей
|
{/* Селектор количества элементов на странице */}
|
||||||
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-2 sm:space-y-0">
|
||||||
<span className="ml-2 text-blue-600">
|
<div className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
(применены фильтры)
|
<span>Показывать по:</span>
|
||||||
</span>
|
<select
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={(e) => handleItemsPerPageChange(Number(e.target.value))}
|
||||||
|
className="px-2 py-1 border border-gray-200 rounded text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-[#ec1c24] focus:border-transparent"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={20}>20</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
</select>
|
||||||
|
<span>записей</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500 text-center sm:text-right">
|
||||||
|
Показано {startIndex + 1}-{Math.min(endIndex, filteredItems.length)} из {filteredItems.length} записей
|
||||||
|
{filteredItems.length !== historyItems.length && (
|
||||||
|
<span className="ml-1">
|
||||||
|
(всего {historyItems.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{(selectedManufacturer !== "Все" || search.trim() || activeTab !== "Все") && (
|
||||||
|
<span className="ml-2 text-blue-600">
|
||||||
|
(применены фильтры)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Компонент пагинации */}
|
||||||
|
{filteredItems.length > itemsPerPage && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
showPageInfo={true}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -134,8 +134,13 @@ const FavoritesProvider: React.FC<FavoritesProviderProps> = ({ children }) => {
|
|||||||
|
|
||||||
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
const [removeFavoriteMutation] = useMutation(REMOVE_FROM_FAVORITES, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Товар удален из избранного', {
|
toast('Товар удален из избранного', {
|
||||||
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
icon: <DeleteCartIcon size={20} color="#ec1c24" />,
|
||||||
|
style: {
|
||||||
|
background: '#6b7280', // Серый фон
|
||||||
|
color: '#fff', // Белый текст
|
||||||
|
},
|
||||||
|
duration: 3000,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -1678,4 +1678,31 @@ export const GET_DAILY_PRODUCTS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Запрос для получения новых поступлений
|
||||||
|
export const GET_NEW_ARRIVALS = gql`
|
||||||
|
query GetNewArrivals($limit: Int) {
|
||||||
|
newArrivals(limit: $limit) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
article
|
||||||
|
brand
|
||||||
|
retailPrice
|
||||||
|
wholesalePrice
|
||||||
|
createdAt
|
||||||
|
images {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
alt
|
||||||
|
order
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
Reference in New Issue
Block a user