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

277
src/pages/card.tsx Normal file
View File

@ -0,0 +1,277 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useState, useMemo } from "react";
import { useQuery, useLazyQuery } from "@apollo/client";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import CatalogSubscribe from "@/components/CatalogSubscribe";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import CartRecommended from "@/components/CartRecommended";
import InfoCard from "@/components/card/InfoCard";
import ProductImageGallery from "@/components/card/ProductImageGallery";
import ProductSortHeader from "@/components/card/ProductSortHeader";
import ProductList from "@/components/card/ProductList";
import ShowMoreOffers from "@/components/card/ShowMoreOffers";
import ProductCharacteristics from "@/components/card/ProductCharacteristics";
import ProductDescriptionTabs from "@/components/card/ProductDescriptionTabs";
import { SEARCH_PRODUCT_OFFERS, PARTS_INDEX_SEARCH_BY_ARTICLE, GET_ANALOG_OFFERS } from "@/lib/graphql";
import { useArticleImage } from "@/hooks/useArticleImage";
import { useRecommendedProducts } from "../hooks/useRecommendedProducts";
const INITIAL_OFFERS_COUNT = 4;
export default function CardPage() {
const router = useRouter();
const { article, brand, q, artId } = router.query;
const [searchQuery, setSearchQuery] = useState<string>("");
const [brandQuery, setBrandQuery] = useState<string>("");
const [sortBy, setSortBy] = useState<string>("price"); // price, quantity, delivery
const [visibleOffersCount, setVisibleOffersCount] = useState(INITIAL_OFFERS_COUNT);
const [analogsWithOffers, setAnalogsWithOffers] = useState<any[]>([]);
useEffect(() => {
if (article && typeof article === 'string') {
setSearchQuery(article.trim().toUpperCase());
}
if (brand && typeof brand === 'string') {
setBrandQuery(brand.trim());
}
setVisibleOffersCount(INITIAL_OFFERS_COUNT);
}, [article, brand]);
const { data, loading, error } = useQuery(SEARCH_PRODUCT_OFFERS, {
variables: {
articleNumber: searchQuery,
brand: brandQuery || ''
},
skip: !searchQuery,
errorPolicy: 'all'
});
// УБИРАЕМ ЗАПРОС К PARTSINDEX ДЛЯ ОПТИМИЗАЦИИ
// Теперь данные PartsIndex будут получаться только по требованию
// const { data: partsIndexData, loading: partsIndexLoading } = useQuery(PARTS_INDEX_SEARCH_BY_ARTICLE, {
// variables: {
// articleNumber: searchQuery,
// brandName: brandQuery || '',
// lang: 'ru'
// },
// skip: !searchQuery || !brandQuery,
// errorPolicy: 'ignore'
// });
const { imageUrl: mainImageUrl } = useArticleImage(artId as string, { enabled: !!artId });
const result = data?.searchProductOffers;
// Запрос для получения предложений аналогов
const [getAnalogOffers, { loading: analogsLoading }] = useLazyQuery(GET_ANALOG_OFFERS, {
onCompleted: (analogsData) => {
if (analogsData?.getAnalogOffers) {
setAnalogsWithOffers(analogsData.getAnalogOffers);
}
},
errorPolicy: 'ignore'
});
// Автоматически загружаем предложения для аналогов
useEffect(() => {
if (result?.analogs && result.analogs.length > 0) {
const analogsToLoad = result.analogs.slice(0, 5).map((analog: any) => ({
articleNumber: analog.articleNumber,
brand: analog.brand,
name: analog.name,
type: analog.type
}));
getAnalogOffers({ variables: { analogs: analogsToLoad } });
}
}, [result?.analogs, getAnalogOffers]);
// Рекомендуемые товары из той же категории с AutoEuro предложениями
const { recommendedProducts, isLoading: isLoadingRecommendedPrices } = useRecommendedProducts(
result?.name || '',
result?.articleNumber || searchQuery || '',
result?.brand || brandQuery || ''
);
// Если это поиск по параметру q, используем q как article
useEffect(() => {
if (q && typeof q === 'string' && !article) {
setSearchQuery(q.trim().toUpperCase());
const catalogFromUrl = router.query.catalog as string;
if (catalogFromUrl) {
const catalogToBrandMap: { [key: string]: string } = {
'AU1587': 'AUDI',
'VW1587': 'VOLKSWAGEN',
'BMW1587': 'BMW',
'MB1587': 'MERCEDES-BENZ',
};
setBrandQuery(catalogToBrandMap[catalogFromUrl] || '');
} else {
setBrandQuery('');
}
}
}, [q, article, router.query]);
// Собираем и сортируем все предложения (БЕЗ аналогов)
const allOffers = useMemo(() => {
if (!result) return [];
const offers: any[] = [];
// Добавляем только предложения основного товара (НЕ аналоги)
if (result.internalOffers) {
result.internalOffers.forEach((offer: any) => {
// Показываем только предложения с ценой больше 0
if (offer.price && offer.price > 0) {
offers.push({
...offer,
type: 'internal',
brand: result.brand,
articleNumber: result.articleNumber,
name: result.name,
isAnalog: false,
deliveryTime: offer.deliveryDays,
sortPrice: offer.price
});
}
});
}
if (result.externalOffers) {
result.externalOffers.forEach((offer: any) => {
// Показываем только предложения с ценой больше 0
if (offer.price && offer.price > 0) {
offers.push({
...offer,
type: 'external',
brand: offer.brand || result.brand,
articleNumber: offer.code || result.articleNumber,
name: offer.name || result.name,
isAnalog: false,
deliveryTime: offer.deliveryTime,
sortPrice: offer.price
});
}
});
}
// Сортировка предложений
const sortedOffers = [...offers].sort((a, b) => {
switch (sortBy) {
case 'price':
return a.sortPrice - b.sortPrice;
case 'quantity':
return (b.quantity || 0) - (a.quantity || 0);
case 'delivery':
return (a.deliveryTime || 999) - (b.deliveryTime || 999);
default:
return a.sortPrice - b.sortPrice;
}
});
return sortedOffers;
}, [result, sortBy]);
// Видимые предложения
const visibleOffers = allOffers.slice(0, visibleOffersCount);
const hasMoreOffers = allOffers.length > visibleOffersCount;
const handleShowMoreOffers = () => {
setVisibleOffersCount(prev => Math.min(prev + 4, allOffers.length));
};
const handleSortChange = (newSortBy: string) => {
setSortBy(newSortBy);
setVisibleOffersCount(INITIAL_OFFERS_COUNT); // Сбрасываем к начальному количеству
};
if (loading) {
return (
<>
<Head>
<title>Загрузка товара - Protek</title>
<link href="images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="images/webclip.png" rel="apple-touch-icon" />
</Head>
<main className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-red-600 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Загрузка данных товара...</p>
</div>
</main>
<Footer />
</>
);
}
return (
<>
<Head>
<title>{result ? `${result.brand} ${result.articleNumber} - ${result.name}` : `Карточка товара`} - Protek</title>
<meta name="description" content={`Подробная информация о товаре ${result?.name}`} />
<link href="https://fonts.googleapis.com" rel="preconnect" />
<link href="https://fonts.gstatic.com" rel="preconnect" crossOrigin="anonymous" />
<link href="/images/favicon.png" rel="shortcut icon" type="image/x-icon" />
<link href="/images/webclip.png" rel="apple-touch-icon" />
</Head>
<InfoCard
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}
name={result ? result.name : "деталь"}
productId={artId ? String(artId) : undefined}
price={allOffers.length > 0 ? Math.min(...allOffers.map(offer => offer.sortPrice)) : 0}
currency="RUB"
image={mainImageUrl || (result?.partsIndexImages && result.partsIndexImages.length > 0 ? result.partsIndexImages[0].url : undefined)}
/>
<section className="main">
<div className="w-layout-blockcontainer container w-container">
<div className="w-layout-vflex flex-block-14">
<div className="w-layout-hflex core-product-card-copy">
<ProductImageGallery
imageUrl={mainImageUrl}
partsIndexImages={result?.partsIndexImages?.map((img: any) => img.url) || []}
/>
<div className="w-layout-vflex flex-block-48">
<ProductSortHeader
brand={result ? result.brand : brandQuery}
articleNumber={result ? result.articleNumber : searchQuery}
name={result ? result.name : "деталь"}
sortBy={sortBy}
onSortChange={handleSortChange}
/>
<ProductList
offers={visibleOffers}
isLoading={loading}
/>
<ShowMoreOffers
hasMoreOffers={hasMoreOffers}
onShowMore={handleShowMoreOffers}
remainingCount={allOffers.length - visibleOffersCount}
/>
<div className="w-layout-vflex description-item">
<ProductCharacteristics
result={result}
/>
</div>
</div>
</div>
<CartRecommended
recommendedProducts={recommendedProducts}
isLoadingPrices={analogsLoading || isLoadingRecommendedPrices}
/>
</div>
</div>
</section>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}