367 lines
13 KiB
TypeScript
367 lines
13 KiB
TypeScript
'use client';
|
||
|
||
import {
|
||
ProductDisplay,
|
||
CityPositionsTable,
|
||
PositionChart,
|
||
HistoryDisplay,
|
||
} from './components';
|
||
import { motion } from 'framer-motion';
|
||
import { useState, useEffect } from 'react';
|
||
import ClientWrapper from './ClientWrapper';
|
||
import {
|
||
ChartDataPoint,
|
||
CityPosition,
|
||
HistoryRecord,
|
||
SearchResponse,
|
||
} from '@/types';
|
||
import { FiBarChart2, FiActivity } from 'react-icons/fi';
|
||
|
||
// Data persistence removed – no localStorage interaction
|
||
|
||
export default function Home() {
|
||
// Добавляем состояние для данных
|
||
const [chartData, setChartData] = useState<ChartDataPoint[]>([]);
|
||
const [cityPositions, setCityPositions] = useState<CityPosition[]>([]);
|
||
const [competitorPositions, setCompetitorPositions] = useState<CityPosition[]>([]);
|
||
const [historyData, setHistoryData] = useState<HistoryRecord[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [searchPerformed, setSearchPerformed] = useState(false);
|
||
const [currentQuery, setCurrentQuery] = useState(''); // Текущий поисковый запрос
|
||
const [currentArticleId, setCurrentArticleId] = useState(''); // Текущий артикул товара
|
||
const [currentCompetitorId, setCurrentCompetitorId] = useState(''); // Текущий артикул конкурента
|
||
const [isComparisonMode, setIsComparisonMode] = useState(false); // Режим сравнения
|
||
|
||
// Helper to reset state when new search starts
|
||
const resetDataState = () => {
|
||
setChartData([]);
|
||
setCityPositions([]);
|
||
setCompetitorPositions([]);
|
||
setSearchPerformed(false);
|
||
setIsComparisonMode(false);
|
||
};
|
||
|
||
// Загрузка начальных данных истории (без localStorage)
|
||
useEffect(() => {
|
||
fetchHistory();
|
||
}, []);
|
||
|
||
// Получение данных истории
|
||
const fetchHistory = async (queryOverride?: string, articleOverride?: string) => {
|
||
try {
|
||
setIsLoading(true);
|
||
|
||
// Параметры запроса для фильтрации истории
|
||
const params = new URLSearchParams();
|
||
|
||
// Фильтруем строго по конкретному артикулу и по конкретному запросу
|
||
const articleId = articleOverride || currentArticleId;
|
||
const query = queryOverride || currentQuery;
|
||
|
||
if (articleId) {
|
||
params.append('articleId', articleId);
|
||
}
|
||
|
||
// Обязательно фильтруем по точному запросу (ключевые слова)
|
||
if (query) {
|
||
params.append('query', query);
|
||
}
|
||
|
||
const queryString = params.toString() ? `?${params.toString()}` : '';
|
||
const response = await fetch(`/api/history${queryString}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка при загрузке истории');
|
||
}
|
||
const data = await response.json();
|
||
setHistoryData(data);
|
||
|
||
// Если есть история, берем последнюю запись для отображения
|
||
if (data.length > 0) {
|
||
updateDisplayData(data[0]);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки истории:', error);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// Обновление данных для отображения
|
||
const updateDisplayData = (historyItem: HistoryRecord) => {
|
||
// Обновляем графические данные
|
||
const chartDataFromHistory = historyItem.positions.map((pos) => ({
|
||
city: pos.city,
|
||
myPosition: pos.pageRank === 1 ? pos.rank : (pos.pageRank - 1) * 100 + pos.rank,
|
||
// В истории нет данных конкурента, используем заглушку
|
||
competitorPosition:
|
||
pos.competitorRank && pos.competitorPageRank
|
||
? pos.competitorPageRank === 1
|
||
? pos.competitorRank
|
||
: (pos.competitorPageRank - 1) * 100 + pos.competitorRank
|
||
: 0,
|
||
}));
|
||
setChartData(chartDataFromHistory);
|
||
};
|
||
|
||
// Обработчик завершения поиска (обновлено под новую структуру)
|
||
const handleSearchComplete = (data: SearchResponse, searchQuery?: string) => {
|
||
// Сбрасываем предыдущие данные
|
||
resetDataState();
|
||
|
||
// Формируем позиции для таблицы - берем позиции основного товара
|
||
const myArticleId = data.myArticleId;
|
||
const myPositions = data.positions[myArticleId] || [];
|
||
|
||
setCityPositions(myPositions);
|
||
|
||
// Определяем режим сравнения и данные конкурента
|
||
const hasCompetitor = !!(data.competitorArticleId && data.positions[data.competitorArticleId]);
|
||
setIsComparisonMode(hasCompetitor);
|
||
setCurrentCompetitorId(data.competitorArticleId || '');
|
||
|
||
if (hasCompetitor) {
|
||
const competitorPositions = data.positions[data.competitorArticleId!] || [];
|
||
setCompetitorPositions(competitorPositions);
|
||
|
||
// Создаем chartData для сравнения с конкурентом
|
||
const chartDataFromResponse: ChartDataPoint[] = myPositions.map(myPos => {
|
||
const competitorPos = competitorPositions.find(cp => cp.city === myPos.city);
|
||
return {
|
||
city: myPos.city,
|
||
myPosition: myPos.position ?? 0,
|
||
competitorPosition: competitorPos?.position ?? 0,
|
||
};
|
||
});
|
||
|
||
setChartData(chartDataFromResponse);
|
||
|
||
// Save to localStorage (removed)
|
||
} else {
|
||
// Режим без конкурента - показываем только мои позиции
|
||
setCompetitorPositions([]);
|
||
const chartDataFromResponse: ChartDataPoint[] = myPositions.map(myPos => ({
|
||
city: myPos.city,
|
||
myPosition: myPos.position ?? 0,
|
||
competitorPosition: 0,
|
||
}));
|
||
|
||
setChartData(chartDataFromResponse);
|
||
|
||
// Save to localStorage (removed)
|
||
}
|
||
|
||
setSearchPerformed(true);
|
||
|
||
// Сохраняем артикул основного товара для фильтрации истории
|
||
if (data.products && data.products.length > 0) {
|
||
const mainProduct = data.products.find(p => p.article === myArticleId);
|
||
if (mainProduct) {
|
||
// Если сменился артикул, очищаем историю
|
||
if (currentArticleId !== mainProduct.article) {
|
||
setHistoryData([]);
|
||
}
|
||
setCurrentArticleId(mainProduct.article);
|
||
|
||
// Save query and article ID (removed)
|
||
if (searchQuery) setCurrentQuery(searchQuery);
|
||
}
|
||
}
|
||
|
||
// Обновляем историю после выполнения поиска, чтобы отобразить новые данные
|
||
// Устанавливаем небольшую задержку, чтобы данные успели сохраниться в БД
|
||
setTimeout(() => {
|
||
fetchHistory();
|
||
}, 300);
|
||
};
|
||
|
||
// Анимационные варианты
|
||
const containerVariants = {
|
||
hidden: { opacity: 0 },
|
||
visible: {
|
||
opacity: 1,
|
||
transition: {
|
||
staggerChildren: 0.2,
|
||
delayChildren: 0.3,
|
||
},
|
||
},
|
||
};
|
||
|
||
const itemVariants = {
|
||
hidden: { opacity: 0, y: 20 },
|
||
visible: {
|
||
opacity: 1,
|
||
y: 0,
|
||
transition: {
|
||
type: 'spring',
|
||
stiffness: 80,
|
||
},
|
||
},
|
||
};
|
||
|
||
// Заглушки для пустых состояний
|
||
const TablePlaceholder = () => (
|
||
<div className="bg-white/40 backdrop-blur-md border border-white/30 rounded-lg md:rounded-xl shadow-lg shadow-purple-900/5 p-3 md:p-4 h-full flex flex-col items-center justify-center">
|
||
<FiBarChart2 size={48} className="text-purple-400 mb-4" />
|
||
<p className="text-gray-600 text-center mb-2 font-medium">
|
||
Таблица позиций
|
||
</p>
|
||
<p className="text-gray-500 text-sm text-center">
|
||
Данные о позициях товаров по городам будут отображаться здесь
|
||
</p>
|
||
</div>
|
||
);
|
||
|
||
const ChartPlaceholder = () => (
|
||
<div className="bg-white/40 backdrop-blur-md border border-white/30 rounded-lg md:rounded-xl shadow-lg shadow-purple-900/5 p-3 md:p-4 h-full flex flex-col items-center justify-center">
|
||
<FiActivity size={48} className="text-purple-400 mb-4" />
|
||
<p className="text-gray-600 text-center mb-2 font-medium">
|
||
График позиций
|
||
</p>
|
||
<p className="text-gray-500 text-sm text-center">
|
||
Визуализация позиций товаров будет отображаться здесь
|
||
</p>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<ClientWrapper>
|
||
<motion.main
|
||
initial="hidden"
|
||
animate="visible"
|
||
variants={containerVariants}
|
||
className="h-screen overflow-hidden p-1 md:p-2"
|
||
>
|
||
<div className="max-w-full mx-auto h-full">
|
||
{/* Data Management Controls removed (no persistence) */}
|
||
|
||
{/* Mobile Layout */}
|
||
<div className="flex flex-col gap-3 pb-6 lg:hidden overflow-hidden">
|
||
{/* Mobile: Search section */}
|
||
<motion.div
|
||
className="min-h-[180px] max-h-[250px] flex-shrink-0"
|
||
variants={itemVariants}
|
||
>
|
||
<ProductDisplay
|
||
products={[]}
|
||
onSearchComplete={handleSearchComplete}
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* Mobile: Table section */}
|
||
<motion.div
|
||
className="min-h-[200px] max-h-[300px] flex-shrink-0"
|
||
variants={itemVariants}
|
||
>
|
||
{searchPerformed ? (
|
||
<CityPositionsTable
|
||
positions={cityPositions}
|
||
competitorPositions={competitorPositions}
|
||
isComparisonMode={isComparisonMode}
|
||
myArticleId={currentArticleId}
|
||
competitorArticleId={currentCompetitorId}
|
||
/>
|
||
) : (
|
||
<TablePlaceholder />
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* Mobile: Chart section */}
|
||
<motion.div
|
||
className="min-h-[250px] max-h-[350px] flex-shrink-0"
|
||
variants={itemVariants}
|
||
>
|
||
{searchPerformed ? (
|
||
<PositionChart
|
||
data={chartData}
|
||
myArticleId={currentArticleId}
|
||
competitorArticleId={currentCompetitorId}
|
||
/>
|
||
) : (
|
||
<ChartPlaceholder />
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* Mobile: History section */}
|
||
<motion.div
|
||
variants={itemVariants}
|
||
className="min-h-[150px] max-h-[250px] flex-shrink-0 mb-6"
|
||
>
|
||
<HistoryDisplay
|
||
history={historyData}
|
||
isLoading={isLoading}
|
||
searchPerformed={searchPerformed}
|
||
currentQuery={currentQuery}
|
||
/>
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* Desktop Layout */}
|
||
<div className="hidden lg:grid lg:grid-rows-[500px_1fr] lg:gap-1 h-full overflow-hidden">
|
||
{/* Desktop: Top section with three columns */}
|
||
<div className="grid lg:grid-cols-12 gap-2 h-full overflow-hidden">
|
||
{/* Товары и поиск - 3/12 */}
|
||
<motion.div
|
||
className="lg:col-span-3 h-full"
|
||
variants={itemVariants}
|
||
>
|
||
<ProductDisplay
|
||
products={[]}
|
||
onSearchComplete={handleSearchComplete}
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* Таблица - 3/12 */}
|
||
<motion.div
|
||
className="lg:col-span-3 h-full"
|
||
variants={itemVariants}
|
||
>
|
||
{searchPerformed ? (
|
||
<CityPositionsTable
|
||
positions={cityPositions}
|
||
competitorPositions={competitorPositions}
|
||
isComparisonMode={isComparisonMode}
|
||
myArticleId={currentArticleId}
|
||
competitorArticleId={currentCompetitorId}
|
||
/>
|
||
) : (
|
||
<TablePlaceholder />
|
||
)}
|
||
</motion.div>
|
||
|
||
{/* График - 6/12 */}
|
||
<motion.div
|
||
className="lg:col-span-6 h-full"
|
||
variants={itemVariants}
|
||
>
|
||
{searchPerformed ? (
|
||
<PositionChart
|
||
data={chartData}
|
||
myArticleId={currentArticleId}
|
||
competitorArticleId={currentCompetitorId}
|
||
/>
|
||
) : (
|
||
<ChartPlaceholder />
|
||
)}
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* Desktop: Bottom section - History */}
|
||
<motion.div
|
||
variants={itemVariants}
|
||
className="h-full overflow-hidden"
|
||
>
|
||
<HistoryDisplay
|
||
history={historyData}
|
||
isLoading={isLoading}
|
||
searchPerformed={searchPerformed}
|
||
currentQuery={currentQuery}
|
||
/>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
</motion.main>
|
||
</ClientWrapper>
|
||
);
|
||
}
|