Files
scan-sfera/src/app/page.tsx
2025-08-09 07:34:49 +03:00

367 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
);
}